create-rakta-app 0.1.8 → 0.2.0

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
@@ -1666,6 +1666,632 @@ function writeProjectFiles(projectRoot, generatedFiles) {
1666
1666
  // src/generator.ts
1667
1667
  import { readFileSync } from "fs";
1668
1668
 
1669
+ // src/starter.ts
1670
+ var STARTER_TYPES_CODE = `export interface SystemMetric {
1671
+ name: string;
1672
+ value: string | number;
1673
+ status: "nominal" | "warning" | "critical";
1674
+ }
1675
+
1676
+ export interface GameHighScore {
1677
+ name: string;
1678
+ score: number;
1679
+ date: string;
1680
+ }
1681
+
1682
+ export type AestheticUnit = "LENIS-MODERN" | "RETRO-CYBER" | "NEO-BRUTALIST";
1683
+ `;
1684
+ var STARTER_AUDIO_CODE = `let audioContext: AudioContext | null = null;
1685
+
1686
+ function getAudioContext(): AudioContext | null {
1687
+ if (typeof window === "undefined") return null;
1688
+
1689
+ const AudioContextCtor = window.AudioContext;
1690
+ if (!AudioContextCtor) return null;
1691
+
1692
+ audioContext ??= new AudioContextCtor();
1693
+ return audioContext;
1694
+ }
1695
+
1696
+ function playTone(frequency: number, durationMs: number, type: OscillatorType): void {
1697
+ const context = getAudioContext();
1698
+ if (!context) return;
1699
+
1700
+ const oscillator = context.createOscillator();
1701
+ const gain = context.createGain();
1702
+
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);
1707
+
1708
+ oscillator.connect(gain);
1709
+ gain.connect(context.destination);
1710
+ oscillator.start();
1711
+ oscillator.stop(context.currentTime + durationMs / 1000);
1712
+ }
1713
+
1714
+ export function playJumpSound(): void {
1715
+ playTone(560, 90, "triangle");
1716
+ }
1717
+
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
+ }
1727
+
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>
1788
+ </div>
1789
+ );
1790
+ }
1791
+ `;
1792
+ var STARTER_CORAL_OBSTACLE_CODE = `interface CoralObstacleProps {
1793
+ position: "TOP" | "BOTTOM";
1794
+ height: number;
1795
+ width?: number;
1796
+ paletteIndex?: number;
1797
+ variant?: number;
1798
+ scaleX?: number;
1799
+ }
1800
+
1801
+ export default function CoralObstacle({
1802
+ position,
1803
+ height,
1804
+ width = 64,
1805
+ paletteIndex = 0,
1806
+ variant = 0,
1807
+ scaleX = 1,
1808
+ }: CoralObstacleProps) {
1809
+ const fallbackPalette = {
1810
+ primary: "#f43f5e",
1811
+ secondary: "#fda4af",
1812
+ shadow: "#9f1239",
1813
+ polyps: "#ffe4e6",
1814
+ name: "rose",
1815
+ };
1816
+ const palettes = [
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" },
1822
+ ];
1823
+
1824
+ const palette = palettes[paletteIndex % palettes.length] ?? fallbackPalette;
1825
+ const coralId = \`coralGrad-\${palette.name}-\${variant}\`;
1826
+ const flipped = position === "TOP" ? "scale(1, -1) translate(0, -120)" : undefined;
1827
+
1828
+ return (
1829
+ <div
1830
+ className="absolute flex items-center justify-center pointer-events-none"
1831
+ style={{ height: \`\${height}px\`, width: \`\${width}px\`, transform: \`scaleX(\${scaleX})\` }}
1832
+ >
1833
+ <svg
1834
+ aria-hidden="true"
1835
+ focusable="false"
1836
+ viewBox="0 0 80 120"
1837
+ className="h-full w-full drop-shadow-[0_6px_14px_rgba(0,0,0,0.7)]"
1838
+ preserveAspectRatio="none"
1839
+ >
1840
+ <defs>
1841
+ <linearGradient id={coralId} x1="0%" y1="0%" x2="100%" y2="100%">
1842
+ <stop offset="0%" stopColor={palette.secondary} />
1843
+ <stop offset="52%" stopColor={palette.primary} />
1844
+ <stop offset="100%" stopColor={palette.shadow} />
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>
1853
+ </defs>
1854
+
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} />
1878
+ </g>
1879
+ </svg>
1880
+ </div>
1881
+ );
1882
+ }
1883
+ `;
1884
+ var STARTER_PAGE_CODE = `import { useCallback, useEffect, useRef, useState } from "react";
1885
+ import type { IconType } from "react-icons";
1886
+ import {
1887
+ LuActivity,
1888
+ LuCode,
1889
+ LuCpu,
1890
+ LuGauge,
1891
+ LuRocket,
1892
+ LuShieldCheck,
1893
+ LuZap,
1894
+ } from "react-icons/lu";
1895
+ import CoralObstacle from "./components/CoralObstacle";
1896
+ import ShrimpCharacter from "./components/ShrimpCharacter";
1897
+ import type { AestheticUnit } from "./types";
1898
+ import { playJumpSound, playScoreSound } from "./utils/audio";
1899
+
1900
+ type GameStatus = "IDLE" | "SWIMMING" | "JUMPING" | "DEAD";
1901
+ type SimSpeed = "NORMAL" | "FAST" | "TURBO";
1902
+ type StatusCard = { title: string; text: string; Icon: IconType };
1903
+ type FeatureCard = { number: string; title: string; text: string; Icon: IconType };
1904
+
1905
+ const GAME_WIDTH = 720;
1906
+ const GAME_HEIGHT = 280;
1907
+ const SHRIMP_X = 82;
1908
+ const SHRIMP_SIZE = 58;
1909
+ const GRAVITY = 0.82;
1910
+ const JUMP_FORCE = 15;
1911
+
1912
+ function getSpeedValue(speed: SimSpeed): number {
1913
+ if (speed === "TURBO") return 8.2;
1914
+ if (speed === "FAST") return 6.4;
1915
+ return 5.2;
1916
+ }
1917
+
1918
+ const statusCards: StatusCard[] = [
1919
+ { title: "Routing", text: "File-based routes with fast client navigation", Icon: LuZap },
1920
+ { title: "Metadata", text: "Default title, favicon, and SEO shell included", Icon: LuShieldCheck },
1921
+ { title: "Runtime", text: "Bun-powered dev server on localhost:3000", Icon: LuCpu },
1922
+ ];
1923
+
1924
+ const featureCards: FeatureCard[] = [
1925
+ { number: "01", title: "HOT RELOAD", text: "Pages, components, styles, layouts, stores, and route updates refresh immediately.", Icon: LuActivity },
1926
+ { number: "02", title: "REACT ICONS", text: "Starter icons use react-icons/lu only. No lucide-react dependency ships.", Icon: LuCode },
1927
+ { number: "03", title: "LOCAL FIRST", text: "The CLI prints and serves localhost:3000 for the frontend starter.", Icon: LuGauge },
1928
+ ];
1929
+
1930
+ export default function WelcomePage() {
1931
+ const [status, setStatus] = useState<GameStatus>("IDLE");
1932
+ const [score, setScore] = useState(0);
1933
+ const [bestScore, setBestScore] = useState(() => {
1934
+ if (typeof localStorage === "undefined") return 0;
1935
+ return parseInt(localStorage.getItem("rakta_shrimprun_highscore") || "0", 10);
1936
+ });
1937
+ const [playerY, setPlayerY] = useState(0);
1938
+ const [rotation, setRotation] = useState(0);
1939
+ const [obstacleX, setObstacleX] = useState(GAME_WIDTH + 120);
1940
+ const [obstacleHeight, setObstacleHeight] = useState(84);
1941
+ const [obstacleWidth, setObstacleWidth] = useState(58);
1942
+ const [obstaclePalette, setObstaclePalette] = useState(0);
1943
+ const [obstacleVariant, setObstacleVariant] = useState(0);
1944
+ const [obstacleScaleX, setObstacleScaleX] = useState(1);
1945
+ const [aestheticUnit, setAestheticUnit] = useState<AestheticUnit>("LENIS-MODERN");
1946
+ const [simSpeed, setSimSpeed] = useState<SimSpeed>("NORMAL");
1947
+ const [lowLatencyMode, setLowLatencyMode] = useState(true);
1948
+ const [configToast, setConfigToast] = useState<string | null>(null);
1949
+
1950
+ const statusRef = useRef(status);
1951
+ const velocityRef = useRef(0);
1952
+ const playerYRef = useRef(0);
1953
+ const obstacleXRef = useRef(GAME_WIDTH + 120);
1954
+ const scoreRef = useRef(0);
1955
+ const lastTimeRef = useRef(0);
1956
+ const animationRef = useRef<number | null>(null);
1957
+ const toastTimerRef = useRef<number | null>(null);
1958
+
1959
+ useEffect(() => {
1960
+ statusRef.current = status;
1961
+ }, [status]);
1962
+
1963
+ const showConfigToast = useCallback((message: string) => {
1964
+ setConfigToast(message);
1965
+ if (toastTimerRef.current !== null) {
1966
+ window.clearTimeout(toastTimerRef.current);
1967
+ }
1968
+ toastTimerRef.current = window.setTimeout(() => setConfigToast(null), 1400);
1969
+ }, []);
1970
+
1971
+ const resetObstacle = useCallback(() => {
1972
+ obstacleXRef.current = GAME_WIDTH + 80 + Math.random() * 220;
1973
+ setObstacleX(obstacleXRef.current);
1974
+ setObstacleHeight(58 + Math.floor(Math.random() * 82));
1975
+ setObstacleWidth(42 + Math.floor(Math.random() * 32));
1976
+ setObstaclePalette((value) => (value + 1) % 5);
1977
+ setObstacleVariant((value) => (value + 1) % 3);
1978
+ setObstacleScaleX(Math.random() > 0.5 ? 1 : -1);
1979
+ }, []);
1980
+
1981
+ const startRun = useCallback(() => {
1982
+ if (statusRef.current === "DEAD") {
1983
+ scoreRef.current = 0;
1984
+ setScore(0);
1985
+ playerYRef.current = 0;
1986
+ velocityRef.current = 0;
1987
+ setPlayerY(0);
1988
+ resetObstacle();
1989
+ }
1990
+
1991
+ statusRef.current = "SWIMMING";
1992
+ setStatus("SWIMMING");
1993
+ }, [resetObstacle]);
1994
+
1995
+ const jump = useCallback(() => {
1996
+ if (statusRef.current === "IDLE" || statusRef.current === "DEAD") {
1997
+ startRun();
1998
+ }
1999
+
2000
+ if (playerYRef.current <= 4) {
2001
+ velocityRef.current = JUMP_FORCE;
2002
+ setStatus("JUMPING");
2003
+ playJumpSound();
2004
+ }
2005
+ }, [startRun]);
2006
+
2007
+ useEffect(() => {
2008
+ function onKeyDown(event: KeyboardEvent): void {
2009
+ if (event.code === "Space") {
2010
+ event.preventDefault();
2011
+ jump();
2012
+ }
2013
+ }
2014
+
2015
+ window.addEventListener("keydown", onKeyDown);
2016
+ return () => window.removeEventListener("keydown", onKeyDown);
2017
+ }, [jump]);
2018
+
2019
+ useEffect(() => {
2020
+ function frame(timestamp: number): void {
2021
+ const delta = Math.min(timestamp - lastTimeRef.current, 32) / 16.67;
2022
+ lastTimeRef.current = timestamp;
2023
+
2024
+ if (statusRef.current === "SWIMMING" || statusRef.current === "JUMPING") {
2025
+ velocityRef.current -= GRAVITY * delta;
2026
+ playerYRef.current = Math.max(0, playerYRef.current + velocityRef.current * delta);
2027
+
2028
+ if (playerYRef.current === 0 && velocityRef.current < 0) {
2029
+ velocityRef.current = 0;
2030
+ setStatus("SWIMMING");
2031
+ }
2032
+
2033
+ obstacleXRef.current -= getSpeedValue(simSpeed) * (lowLatencyMode ? 1.08 : 0.92) * delta;
2034
+
2035
+ if (obstacleXRef.current < -90) {
2036
+ resetObstacle();
2037
+ scoreRef.current += 7;
2038
+ setScore(scoreRef.current);
2039
+ playScoreSound();
2040
+ }
2041
+
2042
+ scoreRef.current += 1;
2043
+ setScore(scoreRef.current);
2044
+
2045
+ const shrimpLeft = SHRIMP_X + 8;
2046
+ const shrimpRight = SHRIMP_X + SHRIMP_SIZE - 8;
2047
+ const shrimpBottom = GAME_HEIGHT - 54 - playerYRef.current;
2048
+ const shrimpTop = shrimpBottom - SHRIMP_SIZE + 12;
2049
+ const obstacleLeft = obstacleXRef.current + 8;
2050
+ const obstacleRight = obstacleXRef.current + obstacleWidth - 8;
2051
+ const obstacleTop = GAME_HEIGHT - 48 - obstacleHeight;
2052
+ const obstacleBottom = GAME_HEIGHT - 48;
2053
+ const didCollide =
2054
+ shrimpLeft < obstacleRight &&
2055
+ shrimpRight > obstacleLeft &&
2056
+ shrimpTop < obstacleBottom &&
2057
+ shrimpBottom > obstacleTop;
2058
+
2059
+ if (didCollide) {
2060
+ statusRef.current = "DEAD";
2061
+ setStatus("DEAD");
2062
+ setRotation(180);
2063
+ const nextBest = Math.max(bestScore, scoreRef.current);
2064
+ setBestScore(nextBest);
2065
+ localStorage.setItem("rakta_shrimprun_highscore", String(nextBest));
2066
+ } else {
2067
+ setRotation(playerYRef.current > 0 ? -8 : 0);
2068
+ }
2069
+
2070
+ setPlayerY(playerYRef.current);
2071
+ setObstacleX(obstacleXRef.current);
2072
+ }
2073
+
2074
+ animationRef.current = requestAnimationFrame(frame);
2075
+ }
2076
+
2077
+ animationRef.current = requestAnimationFrame(frame);
2078
+ return () => {
2079
+ if (animationRef.current !== null) cancelAnimationFrame(animationRef.current);
2080
+ };
2081
+ }, [bestScore, lowLatencyMode, obstacleHeight, obstacleWidth, resetObstacle, simSpeed]);
2082
+
2083
+ const obstacleSizeClass =
2084
+ obstacleHeight < 82 ? "KECIL" : obstacleHeight < 120 ? "SEDANG" : "BESAR";
2085
+ const obstaclePos = "BOTTOM" as const;
2086
+
2087
+ return (
2088
+ <div className="rakta-welcome min-h-screen bg-[#050505] text-white">
2089
+ <div className="scanline" />
2090
+ <main className="rakta-shell mx-auto flex w-full max-w-7xl flex-col gap-10 px-6 py-8 md:px-10">
2091
+ <nav className="rakta-nav flex items-center justify-between border-b border-surface-stroke pb-5">
2092
+ <div className="flex items-center gap-3 font-mono text-sm font-extrabold tracking-tight">
2093
+ <span className="grid h-8 w-8 place-items-center border border-brand-pink bg-brand-pink text-black">
2094
+ R
2095
+ </span>
2096
+ Rakta.js
2097
+ </div>
2098
+ <div className="hidden items-center gap-6 font-mono text-[11px] font-bold uppercase tracking-wider text-[#b5b5b5] md:flex">
2099
+ <a href="#features" className="transition hover:text-white">Features</a>
2100
+ <a href="#shrimprun" className="transition hover:text-white">ShrimpRun</a>
2101
+ <a href="#start" className="transition hover:text-white">Start</a>
2102
+ </div>
2103
+ </nav>
2104
+
2105
+ <section className="rakta-hero grid min-h-[540px] items-center gap-10 border-b border-surface-stroke py-12 md:grid-cols-[1.08fr_0.92fr]">
2106
+ <div>
2107
+ <p className="mb-5 font-mono text-[11px] font-bold uppercase tracking-[0.32em] text-brand-pink">
2108
+ THE RED ROUTER FRAMEWORK
2109
+ </p>
2110
+ <h1 className="max-w-4xl text-6xl font-black uppercase leading-[0.88] tracking-normal text-white md:text-8xl">
2111
+ Small in size. Fierce in speed.
2112
+ </h1>
2113
+ <p className="mt-7 max-w-2xl text-sm leading-7 text-[#b5b5b5] md:text-base">
2114
+ Rakta.js is ready. React, Bun, file routes, hot reload, metadata, favicon,
2115
+ and the ShrimpRun starter are already wired for your first route.
2116
+ </p>
2117
+ <div className="rakta-actions mt-8 flex flex-wrap gap-3">
2118
+ <a
2119
+ href="#shrimprun"
2120
+ 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"
2121
+ >
2122
+ <LuRocket className="h-4 w-4" />
2123
+ Play ShrimpRun
2124
+ </a>
2125
+ <a
2126
+ href="#start"
2127
+ 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"
2128
+ >
2129
+ <LuCode className="h-4 w-4" />
2130
+ Start Building
2131
+ </a>
2132
+ </div>
2133
+ </div>
2134
+
2135
+ <div className="rakta-status-grid grid gap-3 border border-surface-stroke bg-surface-card p-5">
2136
+ {statusCards.map(({ title, text, Icon }) => (
2137
+ <div key={title} className="flex gap-4 border border-white/5 bg-black p-4">
2138
+ <Icon className="mt-1 h-5 w-5 text-brand-pink" />
2139
+ <div>
2140
+ <h2 className="font-mono text-xs font-extrabold uppercase">{title}</h2>
2141
+ <p className="mt-1 text-xs leading-5 text-[#b5b5b5]">{text}</p>
2142
+ </div>
2143
+ </div>
2144
+ ))}
2145
+ </div>
2146
+ </section>
2147
+
2148
+ <section id="features" className="rakta-feature-grid grid grid-cols-1 gap-0 border border-surface-stroke md:grid-cols-3">
2149
+ {featureCards.map(({ number, title, text, Icon }) => (
2150
+ <div key={title} className="border-b border-surface-stroke p-7 md:border-b-0 md:border-r last:md:border-r-0">
2151
+ <span className="font-mono text-[10px] font-bold text-brand-pink">{number}</span>
2152
+ <Icon className="my-8 h-6 w-6 text-white" />
2153
+ <h2 className="font-mono text-xl font-extrabold uppercase">{title}</h2>
2154
+ <p className="mt-3 text-xs leading-6 text-[#b5b5b5]">{text}</p>
2155
+ </div>
2156
+ ))}
2157
+ </section>
2158
+
2159
+ <section id="shrimprun" className="rakta-game-section grid gap-5">
2160
+ <div>
2161
+ <p className="font-mono text-[10px] font-bold uppercase tracking-[0.28em] text-brand-pink">
2162
+ SHRIMPRUN SIMULATION
2163
+ </p>
2164
+ <h2 className="mt-3 text-4xl font-black uppercase md:text-6xl">
2165
+ Avoid the coral.
2166
+ </h2>
2167
+ </div>
2168
+
2169
+ <button
2170
+ type="button"
2171
+ onClick={jump}
2172
+ className="rakta-game-field relative h-[280px] overflow-hidden border-2 border-surface-stroke bg-black text-left outline-none transition focus-visible:border-brand-pink"
2173
+ aria-label="ShrimpRun game area. Press Space or click to jump."
2174
+ >
2175
+ <div className="bg-grid-glow absolute inset-0 opacity-80" />
2176
+ <div className="absolute inset-x-0 bottom-0 h-12 border-t border-cyan-400/20 bg-cyan-400/5" />
2177
+ <div className="seaweed-waving-left-1 absolute bottom-8 left-9 h-16 w-3 bg-emerald-500/70" />
2178
+ <div className="seaweed-waving-left-2 absolute bottom-8 left-16 h-24 w-3 bg-cyan-500/60" />
2179
+ <div className="seaweed-waving-right-1 absolute bottom-8 right-20 h-20 w-3 bg-emerald-500/70" />
2180
+
2181
+ <div className="absolute left-6 top-5 z-20 flex gap-4 font-mono text-[11px] font-bold uppercase">
2182
+ <span>Score: {score}</span>
2183
+ <span className="text-brand-pink">Best: {bestScore}</span>
2184
+ <span>{status}</span>
2185
+ </div>
2186
+
2187
+ <div
2188
+ className="absolute z-20"
2189
+ style={{ left: SHRIMP_X, bottom: 48 + playerY }}
2190
+ >
2191
+ <ShrimpCharacter status={status} playerY={0} rotation={rotation} />
2192
+ </div>
2193
+
2194
+ <div className="absolute z-10" style={{ left: obstacleX, bottom: 48 }}>
2195
+ <CoralObstacle
2196
+ position={obstaclePos}
2197
+ height={obstacleHeight}
2198
+ width={obstacleWidth}
2199
+ paletteIndex={obstaclePalette}
2200
+ variant={obstacleVariant}
2201
+ scaleX={obstacleScaleX}
2202
+ />
2203
+ </div>
2204
+
2205
+ {status === "IDLE" && (
2206
+ <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">
2207
+ Press Space or click to start
2208
+ </div>
2209
+ )}
2210
+ {status === "DEAD" && (
2211
+ <div className="absolute inset-0 z-30 grid place-items-center bg-black/75 text-center font-mono">
2212
+ <div>
2213
+ <p className="text-3xl font-black text-brand-pink">SYSTEM IMPACT</p>
2214
+ <p className="mt-2 text-xs uppercase text-[#b5b5b5]">Click to reboot the run</p>
2215
+ </div>
2216
+ </div>
2217
+ )}
2218
+ </button>
2219
+
2220
+ <div className="rakta-game-controls flex flex-col justify-between gap-4 font-mono text-[10px] text-zinc-500 md:flex-row md:items-center">
2221
+ <div className="flex flex-wrap items-center gap-3">
2222
+ <span className="font-extrabold text-white">SHRIMPRUN {aestheticUnit.replace("-", " ")}</span>
2223
+ <span>CORAL:</span>
2224
+ <span className="border border-brand-pink/30 bg-brand-pink/5 px-2 py-0.5 font-bold text-brand-pink">
2225
+ {obstacleSizeClass}
2226
+ </span>
2227
+ </div>
2228
+
2229
+ <div className="flex flex-wrap items-center gap-3">
2230
+ {(["LENIS-MODERN", "RETRO-CYBER", "NEO-BRUTALIST"] as AestheticUnit[]).map((unit) => (
2231
+ <button
2232
+ type="button"
2233
+ key={unit}
2234
+ onClick={() => {
2235
+ setAestheticUnit(unit);
2236
+ showConfigToast(\`STYLE SET: \${unit}\`);
2237
+ playScoreSound();
2238
+ }}
2239
+ className={\`px-2 py-1 font-bold transition \${aestheticUnit === unit ? "bg-white text-black" : "border border-surface-stroke text-zinc-500 hover:text-white"}\`}
2240
+ >
2241
+ {unit}
2242
+ </button>
2243
+ ))}
2244
+ {(["NORMAL", "FAST", "TURBO"] as const).map((speed) => (
2245
+ <button
2246
+ type="button"
2247
+ key={speed}
2248
+ onClick={() => {
2249
+ setSimSpeed(speed);
2250
+ showConfigToast(\`SPEED SET: \${speed}\`);
2251
+ playScoreSound();
2252
+ }}
2253
+ 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"}\`}
2254
+ >
2255
+ {speed}
2256
+ </button>
2257
+ ))}
2258
+ <button
2259
+ type="button"
2260
+ onClick={() => {
2261
+ setLowLatencyMode((value) => !value);
2262
+ showConfigToast(\`LATENCY MODE: \${!lowLatencyMode ? "LOW" : "STANDARD"}\`);
2263
+ playJumpSound();
2264
+ }}
2265
+ className={\`px-2 py-1 font-bold transition \${lowLatencyMode ? "bg-cyan-400 text-black" : "border border-surface-stroke text-zinc-500"}\`}
2266
+ >
2267
+ LOW LATENCY {lowLatencyMode ? "ON" : "OFF"}
2268
+ </button>
2269
+ </div>
2270
+ </div>
2271
+
2272
+ {configToast && (
2273
+ <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">
2274
+ {configToast}
2275
+ </div>
2276
+ )}
2277
+ </section>
2278
+
2279
+ <section id="start" className="rakta-start border-t border-surface-stroke py-10">
2280
+ <p className="font-mono text-[10px] font-bold uppercase tracking-[0.28em] text-brand-pink">
2281
+ NEXT MOVE
2282
+ </p>
2283
+ <h2 className="mt-3 text-3xl font-black uppercase md:text-5xl">Edit app/page and ship.</h2>
2284
+ <p className="mt-4 max-w-2xl text-sm leading-7 text-[#b5b5b5]">
2285
+ This starter is intentionally immediate: no popups, no placeholder flow,
2286
+ and no hidden modal dependencies. Your first route is already alive.
2287
+ </p>
2288
+ </section>
2289
+ </main>
2290
+ </div>
2291
+ );
2292
+ }
2293
+ `;
2294
+
1669
2295
  // src/types.ts
1670
2296
  var CSS_DISPLAY = {
1671
2297
  tailwind: "Tailwind CSS",
@@ -1703,14 +2329,18 @@ var PROJECT_MODE_DISPLAY = {
1703
2329
  fullstack: "Fullstack app (frontend + backend + database)",
1704
2330
  "frontend-only": "Frontend only (no backend, no database)"
1705
2331
  };
2332
+ var PROJECT_LANGUAGE_DISPLAY = {
2333
+ typescript: "TypeScript / TSX",
2334
+ javascript: "JavaScript / JSX"
2335
+ };
1706
2336
 
1707
2337
  // src/generator.ts
1708
2338
  var DEFAULT_METADATA_TITLE = "Rakta.js | Small in size. Fierce in speed. Alive in every route";
1709
2339
  var FAVICON_BYTES = readFileSync(new URL("../assets/favicon.ico", import.meta.url));
1710
2340
  function getRootFiles(projectConfig) {
1711
- const { projectName, projectMode } = projectConfig;
2341
+ const { projectName, projectMode, useTypeScript } = projectConfig;
1712
2342
  const workspaces = projectMode === "fullstack" ? ["frontend", "backend", "shared"] : [];
1713
- return [
2343
+ const files = [
1714
2344
  {
1715
2345
  path: "package.json",
1716
2346
  content: JSON.stringify({
@@ -1725,36 +2355,16 @@ function getRootFiles(projectConfig) {
1725
2355
  "build:backend": "cd backend && bun run build",
1726
2356
  build: "bun run build:frontend && bun run build:backend",
1727
2357
  start: "cd backend && bun run start",
1728
- typecheck: "cd frontend && bun run typecheck && cd ../backend && bun run typecheck"
2358
+ ...useTypeScript ? {
2359
+ typecheck: "cd frontend && bun run typecheck && cd ../backend && bun run typecheck"
2360
+ } : {}
1729
2361
  } : {
1730
2362
  dev: "rakta dev",
1731
2363
  build: "rakta build",
1732
2364
  start: "rakta start",
1733
- typecheck: "tsc --noEmit"
1734
- },
1735
- description: `${projectName} \u2014 built with Rakta.js`
1736
- }, null, 2)
1737
- },
1738
- {
1739
- path: "tsconfig.base.json",
1740
- content: JSON.stringify({
1741
- compilerOptions: {
1742
- target: "ESNext",
1743
- module: "ESNext",
1744
- moduleResolution: "Bundler",
1745
- jsx: "react-jsx",
1746
- lib: ["ESNext", "DOM", "DOM.Iterable"],
1747
- strict: true,
1748
- noUncheckedIndexedAccess: true,
1749
- exactOptionalPropertyTypes: true,
1750
- skipLibCheck: true,
1751
- esModuleInterop: true,
1752
- allowSyntheticDefaultImports: true,
1753
- resolveJsonModule: true,
1754
- verbatimModuleSyntax: true,
1755
- isolatedModules: true
2365
+ ...useTypeScript ? { typecheck: "tsc --noEmit" } : {}
1756
2366
  },
1757
- exclude: ["node_modules", "dist", "**/dist/**"]
2367
+ description: `${projectName} \xE2\u20AC\u201D built with Rakta.js`
1758
2368
  }, null, 2)
1759
2369
  },
1760
2370
  {
@@ -1799,11 +2409,38 @@ dist/
1799
2409
  content: generateProjectReadme(projectConfig)
1800
2410
  }
1801
2411
  ];
2412
+ if (useTypeScript) {
2413
+ files.splice(1, 0, {
2414
+ path: "tsconfig.base.json",
2415
+ content: JSON.stringify({
2416
+ compilerOptions: {
2417
+ target: "ESNext",
2418
+ module: "ESNext",
2419
+ moduleResolution: "Bundler",
2420
+ jsx: "react-jsx",
2421
+ lib: ["ESNext", "DOM", "DOM.Iterable"],
2422
+ strict: true,
2423
+ noUncheckedIndexedAccess: true,
2424
+ exactOptionalPropertyTypes: true,
2425
+ skipLibCheck: true,
2426
+ esModuleInterop: true,
2427
+ allowSyntheticDefaultImports: true,
2428
+ resolveJsonModule: true,
2429
+ verbatimModuleSyntax: true,
2430
+ isolatedModules: true
2431
+ },
2432
+ exclude: ["node_modules", "dist", "**/dist/**"]
2433
+ }, null, 2)
2434
+ });
2435
+ }
2436
+ return files;
1802
2437
  }
1803
2438
  function getFrontendOnlyFiles(projectConfig) {
1804
- const { projectName, cssFramework } = projectConfig;
2439
+ const { projectName, cssFramework, useTypeScript } = projectConfig;
1805
2440
  const styleFileName = cssFramework === "sass" ? "globals.scss" : "globals.css";
1806
- return [
2441
+ const pageExtension = useTypeScript ? "tsx" : "jsx";
2442
+ const scriptExtension = useTypeScript ? "ts" : "js";
2443
+ const files = [
1807
2444
  {
1808
2445
  path: "package.json",
1809
2446
  content: JSON.stringify({
@@ -1816,47 +2453,27 @@ function getFrontendOnlyFiles(projectConfig) {
1816
2453
  build: "rakta build",
1817
2454
  start: "rakta start",
1818
2455
  routes: "rakta routes",
1819
- typecheck: "tsc --noEmit"
2456
+ ...useTypeScript ? { typecheck: "tsc --noEmit" } : {}
1820
2457
  },
1821
2458
  dependencies: {
1822
- raktajs: "^0.1.8",
2459
+ raktajs: "^0.2.0",
1823
2460
  react: "^19.2.7",
1824
2461
  "react-dom": "^19.2.7",
2462
+ "react-icons": "^5.7.0",
1825
2463
  ...getCssDependencies(cssFramework)
1826
2464
  },
1827
- devDependencies: {
2465
+ devDependencies: useTypeScript ? {
1828
2466
  "@types/react": "^19.2.17",
1829
2467
  "@types/react-dom": "^19.2.3",
1830
2468
  typescript: "^6.0.3",
1831
2469
  ...getCssDevDependencies(cssFramework)
2470
+ } : {
2471
+ ...getCssDevDependencies(cssFramework)
1832
2472
  }
1833
2473
  }, null, 2)
1834
2474
  },
1835
2475
  {
1836
- path: "tsconfig.json",
1837
- content: JSON.stringify({
1838
- extends: "./tsconfig.base.json",
1839
- compilerOptions: {
1840
- outDir: "./dist",
1841
- rootDir: "./",
1842
- types: ["react", "react-dom"]
1843
- },
1844
- include: [
1845
- "rakta-env.d.ts",
1846
- "app/**/*",
1847
- "components/**/*",
1848
- "styles/**/*",
1849
- "rakta.config.ts"
1850
- ],
1851
- exclude: ["node_modules", "dist"]
1852
- }, null, 2)
1853
- },
1854
- {
1855
- path: "rakta-env.d.ts",
1856
- content: generateFrontendOnlyRaktaEnv()
1857
- },
1858
- {
1859
- path: "rakta.config.ts",
2476
+ path: `rakta.config.${scriptExtension}`,
1860
2477
  content: `import { defineRaktaConfig } from "raktajs";
1861
2478
 
1862
2479
  export default defineRaktaConfig({
@@ -1873,32 +2490,40 @@ export default defineRaktaConfig({
1873
2490
  `
1874
2491
  },
1875
2492
  {
1876
- path: "app/layout.tsx",
2493
+ path: `app/layout.${pageExtension}`,
1877
2494
  content: generateFrontendOnlyLayout()
1878
2495
  },
1879
2496
  {
1880
- path: "app/page.tsx",
2497
+ path: `app/page.${pageExtension}`,
1881
2498
  content: generateFrontendOnlyPage(projectName)
1882
2499
  },
1883
2500
  {
1884
- path: "app/loading.tsx",
2501
+ path: `app/loading.${pageExtension}`,
1885
2502
  content: generateFrontendOnlyLoading()
1886
2503
  },
1887
2504
  {
1888
- path: "app/error.tsx",
2505
+ path: `app/error.${pageExtension}`,
1889
2506
  content: generateFrontendOnlyError()
1890
2507
  },
1891
2508
  {
1892
- path: "app/notFound.tsx",
2509
+ path: `app/notFound.${pageExtension}`,
1893
2510
  content: generateFrontendOnlyNotFound()
1894
2511
  },
1895
2512
  {
1896
- path: "app/components/raktaShrimpMascot.tsx",
1897
- content: generateShrimpMascotComponent()
2513
+ path: `app/components/CoralObstacle.${pageExtension}`,
2514
+ content: STARTER_CORAL_OBSTACLE_CODE
2515
+ },
2516
+ {
2517
+ path: `app/components/ShrimpCharacter.${pageExtension}`,
2518
+ content: STARTER_SHRIMP_CHARACTER_CODE
2519
+ },
2520
+ {
2521
+ path: `app/utils/audio.${scriptExtension}`,
2522
+ content: STARTER_AUDIO_CODE
1898
2523
  },
1899
2524
  {
1900
- path: "app/components/shrimpRunGame.tsx",
1901
- content: generateShrimpRunGameComponent()
2525
+ path: `app/types.${scriptExtension}`,
2526
+ content: STARTER_TYPES_CODE
1902
2527
  },
1903
2528
  {
1904
2529
  path: `styles/${styleFileName}`,
@@ -1913,6 +2538,31 @@ export default defineRaktaConfig({
1913
2538
  content: FAVICON_BYTES
1914
2539
  }
1915
2540
  ];
2541
+ if (useTypeScript) {
2542
+ files.splice(1, 0, {
2543
+ path: "tsconfig.json",
2544
+ content: JSON.stringify({
2545
+ extends: "./tsconfig.base.json",
2546
+ compilerOptions: {
2547
+ outDir: "./dist",
2548
+ rootDir: "./",
2549
+ types: ["react", "react-dom"]
2550
+ },
2551
+ include: [
2552
+ "rakta-env.d.ts",
2553
+ "app/**/*",
2554
+ "components/**/*",
2555
+ "styles/**/*",
2556
+ "rakta.config.ts"
2557
+ ],
2558
+ exclude: ["node_modules", "dist"]
2559
+ }, null, 2)
2560
+ }, {
2561
+ path: "rakta-env.d.ts",
2562
+ content: generateFrontendOnlyRaktaEnv()
2563
+ });
2564
+ }
2565
+ return processFilesForLanguage(files, useTypeScript);
1916
2566
  }
1917
2567
  function getFullstackFrontendFiles(projectConfig) {
1918
2568
  const { projectName, cssFramework } = projectConfig;
@@ -1934,7 +2584,7 @@ function getFullstackFrontendFiles(projectConfig) {
1934
2584
  typecheck: "tsc --noEmit"
1935
2585
  },
1936
2586
  dependencies: {
1937
- raktajs: "^0.1.8",
2587
+ raktajs: "^0.2.0",
1938
2588
  react: "^19.2.7",
1939
2589
  "react-dom": "^19.2.7",
1940
2590
  ...getCssDependencies(cssFramework)
@@ -1974,7 +2624,7 @@ export default defineRaktaConfig({
1974
2624
  appName: "${projectName}",
1975
2625
  seo: {
1976
2626
  defaultTitle: "${DEFAULT_METADATA_TITLE}",
1977
- defaultDescription: "Built with Rakta.js \u2014 Small in size. Fierce in speed. Alive in every route.",
2627
+ defaultDescription: "Built with Rakta.js \xE2\u20AC\u201D Small in size. Fierce in speed. Alive in every route.",
1978
2628
  },
1979
2629
  render: {
1980
2630
  defaultMode: "csr",
@@ -2230,7 +2880,7 @@ export type UserSchema = typeof userSchema;
2230
2880
  },
2231
2881
  {
2232
2882
  path: `frontend/styles/${styleFileName}`,
2233
- content: getCssGlobals(cssFramework)
2883
+ content: getFrontendOnlyCssGlobals(cssFramework)
2234
2884
  },
2235
2885
  {
2236
2886
  path: "frontend/public/.gitkeep",
@@ -2250,6 +2900,28 @@ export type UserSchema = typeof userSchema;
2250
2900
  }
2251
2901
  ];
2252
2902
  }
2903
+ function stripTypeScriptSyntax(code) {
2904
+ 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(/\)\s*:\s*[^({=>]+(?=\s*\{)/g, ")").replace(/\}\s*:\s*[A-Za-z_$][\w$]*(?=\s*\))/g, "}").replace(/(const|let)\s+([A-Za-z_$][\w$]*)\s*:\s*[^=;]+=/g, "$1 $2 =");
2905
+ }
2906
+ function processFilesForLanguage(files, useTypeScript) {
2907
+ if (useTypeScript) {
2908
+ return files;
2909
+ }
2910
+ return files.filter((file) => !file.path.endsWith(".d.ts") && !file.path.endsWith("types.ts") && !file.path.endsWith("types.js")).map((file) => {
2911
+ if (typeof file.content !== "string") {
2912
+ return file;
2913
+ }
2914
+ let path = file.path;
2915
+ if (path.endsWith(".tsx"))
2916
+ path = path.replace(/\.tsx$/, ".jsx");
2917
+ else if (path.endsWith(".ts"))
2918
+ path = path.replace(/\.ts$/, ".js");
2919
+ return {
2920
+ path,
2921
+ content: stripTypeScriptSyntax(file.content)
2922
+ };
2923
+ });
2924
+ }
2253
2925
  function getBackendFiles(projectConfig) {
2254
2926
  return [
2255
2927
  ...getBackendCommonFiles(projectConfig),
@@ -2672,430 +3344,238 @@ function getCssDevDependencies(cssFramework) {
2672
3344
  }
2673
3345
  }
2674
3346
  function getFrontendOnlyCssGlobals(cssFramework) {
2675
- const cssImport = cssFramework === "tailwind" ? `@import "tailwindcss";
3347
+ 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");
3348
+ @import "tailwindcss";
2676
3349
 
2677
3350
  ` : cssFramework === "bootstrap" ? `@import url("https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css");
2678
3351
 
2679
- ` : cssFramework === "sass" ? `$color-primary: #dc2626;
3352
+ ` : cssFramework === "sass" ? `$color-primary: #e11d48;
2680
3353
  $color-background: #050505;
2681
- $color-foreground: #f8fafc;
3354
+ $color-foreground: #fafafa;
2682
3355
 
2683
3356
  ` : "";
2684
- return `${cssImport}:root {
2685
- --color-primary: #dc2626;
2686
- --color-background: #050505;
2687
- --color-foreground: #f8fafc;
2688
- --color-surface: #0e111a;
2689
- --color-border: rgba(255, 255, 255, 0.1);
2690
- --color-muted: #94a3b8;
2691
- color-scheme: dark;
3357
+ return `${cssImport}@theme {
3358
+ --font-sans: "Geist", ui-sans-serif, system-ui, sans-serif;
3359
+ --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, monospace;
3360
+ --color-brand-pink: #e11d48;
3361
+ --color-brand-green: #00ff00;
3362
+ --color-surface-bg: #000000;
3363
+ --color-surface-card: #0d0d0d;
3364
+ --color-surface-stroke: #1f1f1f;
2692
3365
  }
2693
3366
 
2694
- html {
2695
- scroll-behavior: smooth;
3367
+ :root {
3368
+ color-scheme: dark;
3369
+ background: #050505;
3370
+ color: #fafafa;
3371
+ font-family: var(--font-sans);
2696
3372
  }
2697
3373
 
3374
+ * { box-sizing: border-box; }
3375
+ html { scroll-behavior: smooth; }
2698
3376
  body {
2699
3377
  margin: 0;
2700
3378
  min-width: 320px;
2701
3379
  min-height: 100vh;
2702
- color: var(--color-foreground);
2703
- background: var(--color-background);
2704
- font-family:
2705
- ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
2706
- Roboto, sans-serif;
3380
+ background: #050505;
3381
+ color: #fafafa;
3382
+ overflow-x: hidden;
2707
3383
  -webkit-font-smoothing: antialiased;
2708
3384
  }
3385
+ button, a { font: inherit; }
3386
+ a { color: inherit; text-decoration: none; }
3387
+ #rakta-root { min-height: 100vh; }
2709
3388
 
2710
- *,
2711
- *::before,
2712
- *::after {
2713
- box-sizing: border-box;
2714
- }
2715
-
2716
- @keyframes shrimpLegs {
2717
- 0% {
2718
- transform: translateY(0);
2719
- }
2720
-
2721
- 50% {
2722
- transform: translateY(2px);
2723
- }
2724
- }
2725
-
2726
- #rakta-root {
3389
+ .rakta-welcome {
2727
3390
  min-height: 100vh;
3391
+ background: #050505;
3392
+ color: #fafafa;
2728
3393
  }
2729
-
2730
- main {
2731
- width: min(1120px, calc(100% - 32px));
2732
- min-height: 100vh;
3394
+ .rakta-shell {
3395
+ width: min(100% - 32px, 1280px);
2733
3396
  margin: 0 auto;
2734
- padding: 56px 0;
2735
- }
2736
-
2737
- section {
2738
- margin: 0 0 24px;
2739
- border: 1px solid rgba(248, 113, 113, 0.18);
2740
- border-radius: 18px;
2741
- background:
2742
- linear-gradient(135deg, rgba(220, 38, 38, 0.16), transparent 34%),
2743
- #0e111a;
2744
- box-shadow: 0 24px 80px rgba(127, 29, 29, 0.2);
2745
- padding: clamp(24px, 4vw, 44px);
3397
+ padding: 32px 0;
2746
3398
  }
2747
-
2748
- p {
2749
- margin: 0 0 12px;
2750
- color: #cbd5e1;
2751
- line-height: 1.7;
2752
- }
2753
-
2754
- h1,
2755
- h2 {
2756
- margin: 0 0 12px;
2757
- color: #ffffff;
2758
- line-height: 1;
3399
+ .rakta-nav {
3400
+ display: flex;
3401
+ align-items: center;
3402
+ justify-content: space-between;
3403
+ border-bottom: 1px solid #1f1f1f;
3404
+ padding-bottom: 20px;
3405
+ }
3406
+ .rakta-hero {
3407
+ display: grid;
3408
+ min-height: 540px;
3409
+ grid-template-columns: minmax(0, 1.08fr) minmax(320px, 0.92fr);
3410
+ align-items: center;
3411
+ gap: 40px;
3412
+ border-bottom: 1px solid #1f1f1f;
3413
+ padding: 48px 0;
2759
3414
  }
2760
-
2761
- h1 {
2762
- max-width: 760px;
2763
- font-size: clamp(42px, 8vw, 84px);
3415
+ .rakta-hero h1 {
3416
+ max-width: 900px;
3417
+ margin: 0;
3418
+ color: #fff;
3419
+ font-size: clamp(3.5rem, 8vw, 6rem);
3420
+ font-weight: 900;
3421
+ line-height: 0.88;
2764
3422
  letter-spacing: 0;
3423
+ text-transform: uppercase;
2765
3424
  }
2766
-
2767
- h2 {
2768
- font-size: clamp(28px, 4vw, 44px);
3425
+ .rakta-hero p,
3426
+ .rakta-start p {
3427
+ color: #b5b5b5;
2769
3428
  }
2770
-
2771
- click {
3429
+ .rakta-actions {
3430
+ display: flex;
3431
+ flex-wrap: wrap;
3432
+ gap: 12px;
3433
+ margin-top: 32px;
3434
+ }
3435
+ .rakta-actions a {
2772
3436
  display: inline-flex;
3437
+ height: 44px;
2773
3438
  align-items: center;
2774
- justify-content: center;
2775
- min-height: 42px;
2776
- margin: 16px 12px 0 0;
2777
- border: 1px solid rgba(248, 113, 113, 0.42);
2778
- border-radius: 12px;
2779
- padding: 0 16px;
2780
- color: #fecaca;
2781
- font-weight: 700;
2782
- text-decoration: none;
2783
- cursor: pointer;
2784
- transition:
2785
- transform 160ms ease,
2786
- border-color 160ms ease,
2787
- background 160ms ease;
3439
+ gap: 8px;
3440
+ border: 1px solid #e11d48;
3441
+ padding: 0 20px;
3442
+ font-family: var(--font-mono);
3443
+ font-size: 12px;
3444
+ font-weight: 800;
3445
+ text-transform: uppercase;
2788
3446
  }
2789
-
2790
- click:hover {
2791
- border-color: #f87171;
2792
- background: rgba(220, 38, 38, 0.12);
2793
- transform: translateY(-1px);
3447
+ .rakta-actions a:first-child {
3448
+ background: #e11d48;
3449
+ color: #fff;
2794
3450
  }
2795
-
2796
- button {
2797
- font: inherit;
3451
+ .rakta-status-grid,
3452
+ .rakta-feature-grid {
3453
+ display: grid;
3454
+ gap: 12px;
3455
+ border: 1px solid #1f1f1f;
3456
+ background: #0d0d0d;
3457
+ padding: 20px;
3458
+ }
3459
+ .rakta-status-grid > div,
3460
+ .rakta-feature-grid > div {
3461
+ border: 1px solid rgba(255, 255, 255, 0.06);
3462
+ background: #000;
3463
+ padding: 16px;
3464
+ }
3465
+ .rakta-feature-grid {
3466
+ grid-template-columns: repeat(3, minmax(0, 1fr));
3467
+ gap: 0;
3468
+ padding: 0;
3469
+ }
3470
+ .rakta-feature-grid > div {
3471
+ min-height: 220px;
3472
+ border-color: #1f1f1f;
3473
+ padding: 28px;
3474
+ }
3475
+ .rakta-game-section {
3476
+ display: grid;
3477
+ gap: 20px;
3478
+ }
3479
+ .rakta-game-section h2,
3480
+ .rakta-start h2 {
3481
+ margin: 12px 0 0;
3482
+ font-size: clamp(2.25rem, 6vw, 4rem);
3483
+ font-weight: 900;
3484
+ line-height: 0.95;
3485
+ text-transform: uppercase;
2798
3486
  }
2799
-
2800
- .relative {
3487
+ .rakta-game-field {
2801
3488
  position: relative;
2802
- }
2803
-
2804
- .absolute {
2805
- position: absolute;
2806
- }
2807
-
2808
- .inset-0 {
2809
- inset: 0;
2810
- }
2811
-
2812
- .bottom-0 {
2813
- bottom: 0;
2814
- }
2815
-
2816
- .left-0 {
2817
- left: 0;
2818
- }
2819
-
2820
- .top-2 {
2821
- top: 8px;
2822
- }
2823
-
2824
- .right-3 {
2825
- right: 12px;
2826
- }
2827
-
2828
- .block {
2829
3489
  display: block;
3490
+ width: 100%;
3491
+ height: 280px;
3492
+ overflow: hidden;
3493
+ border: 2px solid #1f1f1f;
3494
+ background: #000;
3495
+ color: inherit;
3496
+ cursor: pointer;
2830
3497
  }
2831
-
2832
- .flex {
3498
+ .rakta-game-controls {
2833
3499
  display: flex;
2834
- }
2835
-
2836
- .flex-col {
2837
- flex-direction: column;
2838
- }
2839
-
2840
- .flex-wrap {
2841
3500
  flex-wrap: wrap;
2842
- }
2843
-
2844
- .items-center {
2845
3501
  align-items: center;
2846
- }
2847
-
2848
- .items-start {
2849
- align-items: flex-start;
2850
- }
2851
-
2852
- .justify-center {
2853
- justify-content: center;
2854
- }
2855
-
2856
- .gap-2 {
2857
- gap: 8px;
2858
- }
2859
-
2860
- .gap-4 {
3502
+ justify-content: space-between;
2861
3503
  gap: 16px;
2862
- }
2863
-
2864
- .gap-8 {
2865
- gap: 32px;
2866
- }
2867
-
2868
- .w-full {
2869
- width: 100%;
2870
- }
2871
-
2872
- .max-w-full {
2873
- max-width: 100%;
2874
- }
2875
-
2876
- .overflow-hidden {
2877
- overflow: hidden;
2878
- }
2879
-
2880
- .select-none {
2881
- user-select: none;
2882
- }
2883
-
2884
- .cursor-pointer {
2885
- cursor: pointer;
2886
- }
2887
-
2888
- .text-left {
2889
- text-align: left;
2890
- }
2891
-
2892
- .text-sm {
2893
- font-size: 14px;
2894
- }
2895
-
2896
- .text-lg {
2897
- font-size: 18px;
2898
- }
2899
-
2900
- .text-xl {
2901
- font-size: 20px;
2902
- }
2903
-
2904
- .font-bold {
2905
- font-weight: 800;
2906
- }
2907
-
2908
- .font-semibold {
2909
- font-weight: 700;
2910
- }
2911
-
2912
- .font-mono {
2913
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
2914
- }
2915
-
2916
- .text-white {
2917
- color: #fff;
2918
- }
2919
-
2920
- .text-slate-400 {
2921
- color: #94a3b8;
2922
- }
2923
-
2924
- .text-red-600 {
2925
- color: #dc2626;
2926
- }
2927
-
2928
- .bg-red-600 {
2929
- background: #dc2626;
2930
- }
2931
-
2932
- .bg-black\\/75 {
2933
- background: rgba(0, 0, 0, 0.75);
2934
- }
2935
-
2936
- .pointer-events-none {
2937
- pointer-events: none;
2938
- }
2939
-
2940
- .rounded-sm {
2941
- border-radius: 2px;
2942
- }
2943
-
2944
- .rounded-lg {
2945
- border-radius: 8px;
2946
- }
2947
-
2948
- .min-h-5 {
2949
- min-height: 20px;
2950
- }
2951
-
2952
- .tabular-nums {
2953
- font-variant-numeric: tabular-nums;
2954
- }
2955
-
2956
- button[aria-label^="ShrimpRun"] {
2957
- border: 2px solid rgba(248, 113, 113, 0.28);
2958
- border-radius: 16px;
2959
- background:
2960
- linear-gradient(180deg, rgba(15, 23, 42, 0.96), rgba(7, 10, 18, 0.98)),
2961
- radial-gradient(circle at 18% 22%, rgba(248, 113, 113, 0.16), transparent 30%);
2962
- box-shadow:
2963
- inset 0 0 0 1px rgba(255, 255, 255, 0.04),
2964
- 0 20px 60px rgba(0, 0, 0, 0.28);
2965
- }
2966
-
2967
- button[aria-label^="ShrimpRun"] > .bottom-0.left-0 {
2968
- background: linear-gradient(90deg, #7f1d1d, #dc2626, #fecaca);
2969
- }
2970
-
2971
- button:not([aria-label]) {
2972
- border: 0;
2973
- border-radius: 10px;
2974
- background: #dc2626;
2975
- color: #fff;
3504
+ font-family: var(--font-mono);
3505
+ font-size: 10px;
3506
+ color: #71717a;
3507
+ }
3508
+ .rakta-game-controls button {
3509
+ border: 1px solid #1f1f1f;
3510
+ padding: 4px 8px;
3511
+ color: #a1a1aa;
2976
3512
  cursor: pointer;
2977
- font-weight: 800;
2978
- }
2979
-
2980
- .shrimp-sprite {
2981
- filter: drop-shadow(0 8px 12px rgba(0, 0, 0, 0.32));
2982
3513
  }
2983
-
2984
- .shrimp-run-obstacle {
2985
- border-radius: 8px 8px 2px 2px;
2986
- background:
2987
- radial-gradient(circle at 50% 8px, #fecaca 0 4px, transparent 5px),
2988
- linear-gradient(135deg, #fb7185 0%, #dc2626 46%, #7f1d1d 100%);
2989
- box-shadow:
2990
- inset -6px -8px 0 rgba(69, 10, 10, 0.32),
2991
- 0 0 0 2px rgba(254, 202, 202, 0.18);
2992
- }
2993
-
2994
- .shrimp-run-obstacle::before,
2995
- .shrimp-run-obstacle::after {
2996
- position: absolute;
2997
- bottom: 8px;
2998
- width: 10px;
2999
- height: 22px;
3000
- border-radius: 999px 999px 4px 4px;
3001
- background: #ef4444;
3002
- content: "";
3514
+ .rakta-start {
3515
+ border-top: 1px solid #1f1f1f;
3516
+ padding: 40px 0;
3003
3517
  }
3004
3518
 
3005
- .shrimp-run-obstacle::before {
3006
- left: -7px;
3007
- transform: rotate(-18deg);
3008
- }
3009
-
3010
- .shrimp-run-obstacle::after {
3011
- right: -7px;
3012
- transform: rotate(18deg);
3013
- }
3014
-
3015
- @media (max-width: 720px) {
3016
- main {
3017
- width: min(100% - 20px, 1120px);
3018
- padding: 24px 0;
3519
+ @media (max-width: 768px) {
3520
+ .rakta-hero,
3521
+ .rakta-feature-grid {
3522
+ grid-template-columns: 1fr;
3019
3523
  }
3020
-
3021
- section {
3022
- padding: 20px;
3524
+ .rakta-nav > div:last-child {
3525
+ display: none;
3023
3526
  }
3024
3527
  }
3025
- `;
3026
- }
3027
- function getCssGlobals(cssFramework) {
3028
- const sharedBase = `
3029
- :root {
3030
- --color-primary: #dc2626;
3031
- --color-background: #050505;
3032
- --color-foreground: #f8fafc;
3033
- }
3034
3528
 
3035
- * {
3036
- box-sizing: border-box;
3037
- }
3529
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
3530
+ ::-webkit-scrollbar-track { background: #000000; }
3531
+ ::-webkit-scrollbar-thumb { background: #e11d48; border-radius: 0; }
3532
+ ::-webkit-scrollbar-thumb:hover { background: #be123c; }
3038
3533
 
3039
- body {
3040
- margin: 0;
3041
- color: var(--color-foreground);
3042
- background: var(--color-background);
3043
- font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
3534
+ @keyframes scanline {
3535
+ 0% { transform: translateY(-100%); }
3536
+ 100% { transform: translateY(100vh); }
3044
3537
  }
3045
-
3046
- .page-shell {
3047
- width: min(100% - 2rem, 960px);
3048
- margin: 0 auto;
3049
- padding: 4rem 0;
3538
+ .scanline {
3539
+ position: fixed;
3540
+ top: 0;
3541
+ left: 0;
3542
+ z-index: 999;
3543
+ width: 100%;
3544
+ height: 120px;
3545
+ pointer-events: none;
3546
+ background: linear-gradient(0deg, rgba(225, 29, 72, 0.08) 0%, rgba(225, 29, 72, 0) 100%);
3547
+ opacity: 0.8;
3548
+ animation: scanline 8s linear infinite;
3050
3549
  }
3051
3550
 
3052
- .hero-card,
3053
- .feature-card {
3054
- border: 1px solid rgba(255, 255, 255, 0.1);
3055
- background: #0e111a;
3056
- border-radius: 24px;
3057
- padding: 2rem;
3058
- margin-bottom: 1.5rem;
3551
+ .bg-grid-glow {
3552
+ background-size: 40px 40px;
3553
+ background-image:
3554
+ linear-gradient(to right, rgba(255, 255, 255, 0.04) 1px, transparent 1px),
3555
+ linear-gradient(to bottom, rgba(255, 255, 255, 0.04) 1px, transparent 1px);
3059
3556
  }
3060
3557
 
3061
- .eyebrow {
3062
- color: #dc2626;
3063
- font-weight: 700;
3064
- letter-spacing: 0.12em;
3065
- font-size: 0.75rem;
3066
- text-transform: uppercase;
3558
+ @keyframes seaweed-wave-1 {
3559
+ 0% { transform: skewX(-14deg) rotate(-8deg) scaleY(0.96); }
3560
+ 50% { transform: skewX(0deg) rotate(0deg) scaleY(1.04); }
3561
+ 100% { transform: skewX(14deg) rotate(8deg) scaleY(0.96); }
3067
3562
  }
3068
-
3069
- .button-row {
3070
- display: flex;
3071
- gap: 0.75rem;
3072
- flex-wrap: wrap;
3073
- margin-top: 1rem;
3563
+ @keyframes seaweed-wave-2 {
3564
+ 0% { transform: skewX(10deg) rotate(6deg) scaleY(1.04); }
3565
+ 50% { transform: skewX(-2deg) rotate(-2deg) scaleY(0.96); }
3566
+ 100% { transform: skewX(-10deg) rotate(-6deg) scaleY(1.04); }
3074
3567
  }
3075
-
3076
- a {
3077
- color: #dc2626;
3568
+ .seaweed-waving-left-1,
3569
+ .seaweed-waving-right-1 {
3570
+ transform-origin: bottom center !important;
3571
+ animation: seaweed-wave-1 3.2s infinite ease-in-out alternate !important;
3078
3572
  }
3079
-
3080
- button {
3081
- cursor: pointer;
3573
+ .seaweed-waving-left-2,
3574
+ .seaweed-waving-right-2 {
3575
+ transform-origin: bottom center !important;
3576
+ animation: seaweed-wave-2 3.8s infinite ease-in-out alternate !important;
3082
3577
  }
3083
3578
  `;
3084
- switch (cssFramework) {
3085
- case "tailwind":
3086
- return `@import "tailwindcss";
3087
- ${sharedBase}`;
3088
- case "bootstrap":
3089
- return `@import url("https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css");
3090
- ${sharedBase}`;
3091
- case "sass":
3092
- return `$color-primary: #dc2626;
3093
- $color-background: #050505;
3094
- $color-foreground: #f8fafc;
3095
- ${sharedBase}`;
3096
- case "none":
3097
- return sharedBase;
3098
- }
3099
3579
  }
3100
3580
  function generateFrontendOnlyRaktaEnv() {
3101
3581
  return `declare module "*.css";
@@ -3115,14 +3595,6 @@ declare global {
3115
3595
  click: RaktaClickAttributes;
3116
3596
  }
3117
3597
  }
3118
-
3119
- const useCallback: typeof import("react").useCallback;
3120
- const useEffect: typeof import("react").useEffect;
3121
- const useRef: typeof import("react").useRef;
3122
- const useState: typeof import("react").useState;
3123
-
3124
- const ShrimpRunGame: typeof import("./app/components/shrimpRunGame").default;
3125
- const RaktaShrimpMascot: typeof import("./app/components/raktaShrimpMascot").default;
3126
3598
  }
3127
3599
 
3128
3600
  declare module "react/jsx-runtime" {
@@ -3165,54 +3637,8 @@ export default function RootLayout({ children }: RootLayoutProps) {
3165
3637
  }
3166
3638
  `;
3167
3639
  }
3168
- function generateFrontendOnlyPage(projectName) {
3169
- return `import ShrimpRunGame from "./components/shrimpRunGame";
3170
-
3171
- export default function HomePage() {
3172
- return (
3173
- <main className="mx-auto flex min-h-screen w-full max-w-4xl flex-col gap-6 px-4 py-16 sm:px-6 lg:px-8">
3174
- <section className="rounded-3xl border border-white/10 bg-[#0e111a] p-8 shadow-2xl shadow-red-950/20">
3175
- <p className="mb-2 text-xs font-bold uppercase tracking-[0.3em] text-red-600">
3176
- THE RED ROUTER FRAMEWORK
3177
- </p>
3178
- <h1 className="mb-3 text-4xl font-extrabold leading-tight text-white md:text-6xl">
3179
- Welcome to ${projectName}
3180
- </h1>
3181
- <p className="max-w-2xl text-base leading-7 text-slate-400">
3182
- Built with Rakta.js \u2014 Small in size. Fierce in speed. Alive in every route.
3183
- </p>
3184
-
3185
- <div className="mt-6 flex flex-wrap gap-3">
3186
- <click
3187
- to="/offline"
3188
- className="rounded-xl border border-red-500/40 px-4 py-2 text-sm font-semibold text-red-400 transition hover:border-red-400 hover:text-red-300"
3189
- >
3190
- Open offline page
3191
- </click>
3192
- <click
3193
- to="/dashboard"
3194
- className="rounded-xl bg-red-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-red-700"
3195
- >
3196
- View dashboard
3197
- </click>
3198
- </div>
3199
- </section>
3200
-
3201
- <section className="rounded-3xl border border-white/10 bg-[#0e111a] p-8 shadow-2xl shadow-red-950/20">
3202
- <p className="mb-2 text-xs font-bold uppercase tracking-[0.3em] text-red-600">
3203
- SHRIMPRUN
3204
- </p>
3205
- <h2 className="mb-1 text-2xl font-bold text-white">Play ShrimpRun</h2>
3206
- <p className="mb-6 text-sm leading-6 text-slate-400">
3207
- Press Space or click the game area to jump. Avoid the red obstacles!
3208
- </p>
3209
-
3210
- <ShrimpRunGame />
3211
- </section>
3212
- </main>
3213
- );
3214
- }
3215
- `;
3640
+ function generateFrontendOnlyPage(_projectName) {
3641
+ return STARTER_PAGE_CODE;
3216
3642
  }
3217
3643
  function generateFrontendOnlyLoading() {
3218
3644
  return `export default function Loading() {
@@ -3282,564 +3708,6 @@ function generateFrontendOnlyNotFound() {
3282
3708
  }
3283
3709
  `;
3284
3710
  }
3285
- function generateShrimpMascotComponent() {
3286
- return `interface RaktaShrimpMascotProps {
3287
- readonly isJumping: boolean;
3288
- readonly isDead: boolean;
3289
- readonly style?: import("react").CSSProperties;
3290
- }
3291
-
3292
- /**
3293
- * RaktaShrimpMascot \u2014 The animated shrimp hero of ShrimpRun.
3294
- * Drawn entirely with inline SVG. No external assets required.
3295
- */
3296
- export default function RaktaShrimpMascot({
3297
- isJumping,
3298
- isDead,
3299
- style,
3300
- }: RaktaShrimpMascotProps) {
3301
- const bodyColor = isDead ? "#6b7280" : "#dc2626";
3302
- const eyeColor = isDead ? "#374151" : "#fff";
3303
- const legAnimation =
3304
- isJumping || isDead ? "none" : "shrimpLegs 0.3s steps(2) infinite";
3305
-
3306
- return (
3307
- <svg
3308
- viewBox="0 0 48 48"
3309
- width="48"
3310
- height="48"
3311
- style={{ display: "block", ...style }}
3312
- aria-label={
3313
- isDead ? "dead shrimp" : isJumping ? "shrimp jumping" : "running shrimp"
3314
- }
3315
- role="img"
3316
- >
3317
- <style>{\`
3318
- @keyframes shrimpLegs {
3319
- 0% { transform: translateY(0); }
3320
- 50% { transform: translateY(2px); }
3321
- }
3322
- \`}</style>
3323
-
3324
- {/* Body */}
3325
- <path
3326
- d="M8 30 Q10 14 24 12 Q38 10 40 22 Q42 32 32 36 Q20 40 8 30Z"
3327
- fill={bodyColor}
3328
- />
3329
-
3330
- {/* Shell segments */}
3331
- <path
3332
- d="M12 28 Q16 20 24 18 Q30 17 34 22"
3333
- stroke="#b91c1c"
3334
- strokeWidth="1.5"
3335
- fill="none"
3336
- opacity={isDead ? 0.3 : 0.6}
3337
- />
3338
- <path
3339
- d="M14 32 Q19 24 28 22 Q33 21 36 26"
3340
- stroke="#b91c1c"
3341
- strokeWidth="1.5"
3342
- fill="none"
3343
- opacity={isDead ? 0.3 : 0.6}
3344
- />
3345
-
3346
- {/* Eye */}
3347
- <circle cx="34" cy="18" r="4" fill="#1e293b" />
3348
- <circle cx="35" cy="17" r="2" fill={eyeColor} />
3349
-
3350
- {isDead && (
3351
- <>
3352
- <line
3353
- x1="32"
3354
- y1="16"
3355
- x2="36"
3356
- y2="20"
3357
- stroke="#374151"
3358
- strokeWidth="1.5"
3359
- />
3360
- <line
3361
- x1="36"
3362
- y1="16"
3363
- x2="32"
3364
- y2="20"
3365
- stroke="#374151"
3366
- strokeWidth="1.5"
3367
- />
3368
- </>
3369
- )}
3370
-
3371
- {/* Antennae */}
3372
- <line
3373
- x1="34"
3374
- y1="14"
3375
- x2="40"
3376
- y2="6"
3377
- stroke={bodyColor}
3378
- strokeWidth="1.5"
3379
- strokeLinecap="round"
3380
- />
3381
- <line
3382
- x1="32"
3383
- y1="13"
3384
- x2="36"
3385
- y2="4"
3386
- stroke={bodyColor}
3387
- strokeWidth="1.5"
3388
- strokeLinecap="round"
3389
- />
3390
-
3391
- {/* Tail */}
3392
- <path
3393
- d="M10 30 Q4 26 6 20"
3394
- stroke={bodyColor}
3395
- strokeWidth="2"
3396
- fill="none"
3397
- strokeLinecap="round"
3398
- />
3399
- <path
3400
- d="M10 30 Q2 30 4 36"
3401
- stroke={bodyColor}
3402
- strokeWidth="2"
3403
- fill="none"
3404
- strokeLinecap="round"
3405
- />
3406
- <path
3407
- d="M10 30 Q6 34 8 40"
3408
- stroke={bodyColor}
3409
- strokeWidth="2"
3410
- fill="none"
3411
- strokeLinecap="round"
3412
- />
3413
-
3414
- {/* Legs */}
3415
- <g
3416
- style={{
3417
- animation: legAnimation,
3418
- transformOrigin: "24px 36px",
3419
- }}
3420
- >
3421
- <line
3422
- x1="18"
3423
- y1="36"
3424
- x2="14"
3425
- y2="44"
3426
- stroke={bodyColor}
3427
- strokeWidth="1.5"
3428
- strokeLinecap="round"
3429
- />
3430
- <line
3431
- x1="22"
3432
- y1="38"
3433
- x2="18"
3434
- y2="46"
3435
- stroke={bodyColor}
3436
- strokeWidth="1.5"
3437
- strokeLinecap="round"
3438
- />
3439
- <line
3440
- x1="26"
3441
- y1="38"
3442
- x2="24"
3443
- y2="46"
3444
- stroke={bodyColor}
3445
- strokeWidth="1.5"
3446
- strokeLinecap="round"
3447
- />
3448
- <line
3449
- x1="30"
3450
- y1="37"
3451
- x2="28"
3452
- y2="45"
3453
- stroke={bodyColor}
3454
- strokeWidth="1.5"
3455
- strokeLinecap="round"
3456
- />
3457
- </g>
3458
- </svg>
3459
- );
3460
- }
3461
- `;
3462
- }
3463
- function generateShrimpRunGameComponent() {
3464
- return `import { useCallback, useEffect, useRef, useState } from "react";
3465
- import RaktaShrimpMascot from "./raktaShrimpMascot";
3466
- // \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3467
-
3468
- type GameStatus = "idle" | "running" | "dead";
3469
-
3470
- interface ObstacleState {
3471
- readonly id: number;
3472
- readonly xPosition: number;
3473
- readonly width: number;
3474
- readonly height: number;
3475
- }
3476
-
3477
- interface ShrimpState {
3478
- readonly yPosition: number;
3479
- readonly velocityY: number;
3480
- readonly isJumping: boolean;
3481
- }
3482
-
3483
- // \u2500\u2500\u2500 Constants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3484
-
3485
- const CANVAS_WIDTH = 640;
3486
- const CANVAS_HEIGHT = 160;
3487
- const GROUND_STRIP_HEIGHT = 4;
3488
- const SHRIMP_START_X = 60;
3489
- const SHRIMP_WIDTH = 48;
3490
- const SHRIMP_HEIGHT = 48;
3491
- const GRAVITY = 1.4;
3492
- const JUMP_VELOCITY = -18;
3493
- const INITIAL_OBSTACLE_SPEED = 5;
3494
- const SPEED_INCREMENT_PER_SCORE = 0.003;
3495
- const OBSTACLE_SPAWN_INTERVAL_MS = 1600;
3496
- const SCORE_TICK_MS = 80;
3497
- const COLLISION_MARGIN = 8;
3498
- const MAX_CONCURRENT_OBSTACLES = 3;
3499
- const FRAME_SKIP_THRESHOLD_MS = 100;
3500
-
3501
- // \u2500\u2500\u2500 Physics helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3502
-
3503
- function getObstacleSpeed(currentScore: number): number {
3504
- return INITIAL_OBSTACLE_SPEED + currentScore * SPEED_INCREMENT_PER_SCORE;
3505
- }
3506
-
3507
- function checkCollision(
3508
- shrimpYPosition: number,
3509
- obstacle: ObstacleState,
3510
- ): boolean {
3511
- const shrimpLeft = SHRIMP_START_X + COLLISION_MARGIN;
3512
- const shrimpRight = SHRIMP_START_X + SHRIMP_WIDTH - COLLISION_MARGIN;
3513
- const shrimpTop =
3514
- CANVAS_HEIGHT -
3515
- GROUND_STRIP_HEIGHT -
3516
- shrimpYPosition -
3517
- SHRIMP_HEIGHT +
3518
- COLLISION_MARGIN;
3519
- const shrimpBottom = CANVAS_HEIGHT - GROUND_STRIP_HEIGHT - shrimpYPosition;
3520
-
3521
- const obstacleLeft = obstacle.xPosition + COLLISION_MARGIN;
3522
- const obstacleRight = obstacle.xPosition + obstacle.width - COLLISION_MARGIN;
3523
- const obstacleTop = CANVAS_HEIGHT - GROUND_STRIP_HEIGHT - obstacle.height;
3524
- const obstacleBottom = CANVAS_HEIGHT - GROUND_STRIP_HEIGHT;
3525
-
3526
- return (
3527
- shrimpLeft < obstacleRight &&
3528
- shrimpRight > obstacleLeft &&
3529
- shrimpTop < obstacleBottom &&
3530
- shrimpBottom > obstacleTop
3531
- );
3532
- }
3533
-
3534
- // \u2500\u2500\u2500 Component \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3535
-
3536
- /**
3537
- * ShrimpRun \u2014 Default Rakta.js interactive starter game.
3538
- *
3539
- * Like the Chrome offline Dino game, but the dinosaur is an animated shrimp.
3540
- * Press Space or click the game canvas to jump. Avoid the red obstacles!
3541
- *
3542
- * Features:
3543
- * - React state only \u2014 no external game library
3544
- * - requestAnimationFrame game loop
3545
- * - Physics: gravity + jump velocity
3546
- * - Score that increases over time
3547
- * - Speed ramps up as score grows
3548
- * - Collision detection with margin
3549
- * - High score tracked in component state
3550
- * - Keyboard (Space) and click/tap support
3551
- * - Accessible button game canvas
3552
- * - SVG shrimp mascot \u2014 no external assets
3553
- */
3554
- export default function ShrimpRunGame() {
3555
- const [gameStatus, setGameStatus] = useState<GameStatus>("idle");
3556
- const [score, setScore] = useState(0);
3557
- const [highScore, setHighScore] = useState(0);
3558
- const [obstacles, setObstacles] = useState<ObstacleState[]>([]);
3559
- const [shrimp, setShrimp] = useState<ShrimpState>({
3560
- yPosition: 0,
3561
- velocityY: 0,
3562
- isJumping: false,
3563
- });
3564
-
3565
- const gameStatusRef = useRef<GameStatus>("idle");
3566
- const scoreRef = useRef(0);
3567
- const shrimpRef = useRef<ShrimpState>({
3568
- yPosition: 0,
3569
- velocityY: 0,
3570
- isJumping: false,
3571
- });
3572
- const obstaclesRef = useRef<ObstacleState[]>([]);
3573
- const obstacleIdRef = useRef(0);
3574
- const animationFrameRef = useRef<number>(0);
3575
- const lastObstacleTimeRef = useRef(0);
3576
- const lastScoreTickRef = useRef(0);
3577
-
3578
- const jump = useCallback((): void => {
3579
- if (gameStatusRef.current === "dead") {
3580
- return;
3581
- }
3582
-
3583
- if (gameStatusRef.current === "idle") {
3584
- gameStatusRef.current = "running";
3585
- setGameStatus("running");
3586
- }
3587
-
3588
- if (!shrimpRef.current.isJumping) {
3589
- const nextShrimp: ShrimpState = {
3590
- ...shrimpRef.current,
3591
- velocityY: JUMP_VELOCITY,
3592
- isJumping: true,
3593
- };
3594
-
3595
- shrimpRef.current = nextShrimp;
3596
- setShrimp(nextShrimp);
3597
- }
3598
- }, []);
3599
-
3600
- const resetGame = useCallback((): void => {
3601
- const freshShrimp: ShrimpState = {
3602
- yPosition: 0,
3603
- velocityY: 0,
3604
- isJumping: false,
3605
- };
3606
-
3607
- shrimpRef.current = freshShrimp;
3608
- obstaclesRef.current = [];
3609
- obstacleIdRef.current = 0;
3610
- scoreRef.current = 0;
3611
- gameStatusRef.current = "idle";
3612
- lastObstacleTimeRef.current = 0;
3613
- lastScoreTickRef.current = 0;
3614
-
3615
- setShrimp(freshShrimp);
3616
- setObstacles([]);
3617
- setScore(0);
3618
- setGameStatus("idle");
3619
- }, []);
3620
-
3621
- useEffect(() => {
3622
- let previousTimestamp = 0;
3623
-
3624
- function gameTick(timestamp: number): void {
3625
- if (gameStatusRef.current !== "running") {
3626
- animationFrameRef.current = requestAnimationFrame(gameTick);
3627
- return;
3628
- }
3629
-
3630
- const deltaTime = timestamp - previousTimestamp;
3631
- previousTimestamp = timestamp;
3632
-
3633
- if (deltaTime > FRAME_SKIP_THRESHOLD_MS) {
3634
- animationFrameRef.current = requestAnimationFrame(gameTick);
3635
- return;
3636
- }
3637
-
3638
- const currentShrimp = shrimpRef.current;
3639
- let nextVelocityY = currentShrimp.velocityY + GRAVITY;
3640
- let nextYPosition = currentShrimp.yPosition - nextVelocityY;
3641
-
3642
- if (nextYPosition <= 0) {
3643
- nextYPosition = 0;
3644
- nextVelocityY = 0;
3645
- }
3646
-
3647
- const nextShrimp: ShrimpState = {
3648
- yPosition: nextYPosition,
3649
- velocityY: nextVelocityY,
3650
- isJumping: nextYPosition > 0,
3651
- };
3652
-
3653
- shrimpRef.current = nextShrimp;
3654
- setShrimp(nextShrimp);
3655
-
3656
- const obstacleSpeed = getObstacleSpeed(scoreRef.current);
3657
-
3658
- const movedObstacles = obstaclesRef.current
3659
- .map(
3660
- (obstacle): ObstacleState => ({
3661
- ...obstacle,
3662
- xPosition: obstacle.xPosition - obstacleSpeed,
3663
- }),
3664
- )
3665
- .filter((obstacle) => obstacle.xPosition + obstacle.width > -10);
3666
-
3667
- if (
3668
- timestamp - lastObstacleTimeRef.current > OBSTACLE_SPAWN_INTERVAL_MS &&
3669
- movedObstacles.length < MAX_CONCURRENT_OBSTACLES
3670
- ) {
3671
- const obstacleHeight = 30 + Math.floor(Math.random() * 30);
3672
- const obstacleWidth = 20 + Math.floor(Math.random() * 20);
3673
-
3674
- movedObstacles.push({
3675
- id: obstacleIdRef.current,
3676
- xPosition: CANVAS_WIDTH + 20,
3677
- width: obstacleWidth,
3678
- height: obstacleHeight,
3679
- });
3680
-
3681
- obstacleIdRef.current += 1;
3682
- lastObstacleTimeRef.current = timestamp;
3683
- }
3684
-
3685
- obstaclesRef.current = movedObstacles;
3686
- setObstacles([...movedObstacles]);
3687
-
3688
- for (const obstacle of movedObstacles) {
3689
- if (checkCollision(nextShrimp.yPosition, obstacle)) {
3690
- gameStatusRef.current = "dead";
3691
- setGameStatus("dead");
3692
- setHighScore((previousHighScore: number) =>
3693
- Math.max(previousHighScore, scoreRef.current),
3694
- );
3695
- animationFrameRef.current = requestAnimationFrame(gameTick);
3696
- return;
3697
- }
3698
- }
3699
-
3700
- if (timestamp - lastScoreTickRef.current > SCORE_TICK_MS) {
3701
- scoreRef.current += 1;
3702
- setScore(scoreRef.current);
3703
- lastScoreTickRef.current = timestamp;
3704
- }
3705
-
3706
- animationFrameRef.current = requestAnimationFrame(gameTick);
3707
- }
3708
-
3709
- animationFrameRef.current = requestAnimationFrame(gameTick);
3710
-
3711
- return () => {
3712
- cancelAnimationFrame(animationFrameRef.current);
3713
- };
3714
- }, []);
3715
-
3716
- useEffect(() => {
3717
- function handleKeyDown(keyboardEvent: KeyboardEvent): void {
3718
- if (keyboardEvent.code === "Space") {
3719
- keyboardEvent.preventDefault();
3720
- jump();
3721
- }
3722
- }
3723
-
3724
- window.addEventListener("keydown", handleKeyDown);
3725
-
3726
- return () => {
3727
- window.removeEventListener("keydown", handleKeyDown);
3728
- };
3729
- }, [jump]);
3730
-
3731
- const shrimpBottomOffset = GROUND_STRIP_HEIGHT + shrimp.yPosition;
3732
- const isDead = gameStatus === "dead";
3733
- const isIdle = gameStatus === "idle";
3734
- const isRunning = gameStatus === "running";
3735
-
3736
- return (
3737
- <div className="flex flex-col items-start gap-4 py-4">
3738
- <div className="flex flex-wrap items-center gap-8">
3739
- <span className="font-mono text-xl font-bold tabular-nums text-red-600">
3740
- Score: {score}
3741
- </span>
3742
- {highScore > 0 && (
3743
- <span className="text-sm text-slate-400">Best: {highScore}</span>
3744
- )}
3745
- </div>
3746
-
3747
- <button
3748
- type="button"
3749
- className="relative block max-w-full cursor-pointer select-none overflow-hidden rounded-2xl border-2 border-red-600/30 bg-[#0e111a] p-0 text-left focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500"
3750
- aria-label="ShrimpRun game area. Click or press Space to jump."
3751
- onClick={jump}
3752
- onKeyDown={(keyboardEvent: import("react").KeyboardEvent) => {
3753
- if (keyboardEvent.code === "Space") {
3754
- keyboardEvent.preventDefault();
3755
- jump();
3756
- }
3757
- }}
3758
- style={{
3759
- width: CANVAS_WIDTH,
3760
- height: CANVAS_HEIGHT,
3761
- }}
3762
- >
3763
- <span
3764
- className="absolute bottom-0 left-0 w-full rounded-sm bg-red-600"
3765
- style={{
3766
- height: GROUND_STRIP_HEIGHT,
3767
- }}
3768
- />
3769
-
3770
- <span
3771
- className="absolute shrimp-sprite"
3772
- style={{
3773
- left: SHRIMP_START_X,
3774
- bottom: shrimpBottomOffset,
3775
- width: SHRIMP_WIDTH,
3776
- height: SHRIMP_HEIGHT,
3777
- transform: isDead
3778
- ? "rotate(18deg) translateY(8px)"
3779
- : shrimp.isJumping
3780
- ? "rotate(-8deg) translateY(-4px)"
3781
- : "rotate(0deg)",
3782
- transition: "transform 120ms ease",
3783
- }}
3784
- >
3785
- <RaktaShrimpMascot isJumping={shrimp.isJumping} isDead={isDead} />
3786
- </span>
3787
-
3788
- {obstacles.map((obstacle) => (
3789
- <span
3790
- key={obstacle.id}
3791
- className="absolute shrimp-run-obstacle"
3792
- style={{
3793
- left: obstacle.xPosition,
3794
- bottom: GROUND_STRIP_HEIGHT,
3795
- width: obstacle.width,
3796
- height: obstacle.height,
3797
- }}
3798
- />
3799
- ))}
3800
-
3801
- {isIdle && (
3802
- <span className="pointer-events-none absolute inset-0 flex items-center justify-center text-sm text-slate-400">
3803
- Press Space or click to start
3804
- </span>
3805
- )}
3806
-
3807
- {isDead && (
3808
- <span className="absolute inset-0 flex flex-col items-center justify-center gap-2 bg-black/75">
3809
- <span className="text-lg font-bold tracking-widest text-red-600">
3810
- GAME OVER
3811
- </span>
3812
- <span className="text-sm text-slate-400">Score: {score}</span>
3813
- </span>
3814
- )}
3815
-
3816
- {isRunning && score > 0 && score % 50 === 0 && (
3817
- <span className="absolute right-3 top-2 text-xs font-bold tracking-widest text-red-600 opacity-80">
3818
- {score}!
3819
- </span>
3820
- )}
3821
- </button>
3822
-
3823
- <p className="min-h-5 text-sm text-slate-400">
3824
- {isIdle && "\uD83E\uDD90 Click or press Space to make the shrimp jump!"}
3825
- {isRunning && "\uD83E\uDD90 Don't hit the obstacles!"}
3826
- {isDead && "The shrimp got cooked. Try again!"}
3827
- </p>
3828
-
3829
- {isDead && (
3830
- <button
3831
- type="button"
3832
- className="w-fit rounded-lg bg-red-600 px-6 py-2 font-semibold text-white transition hover:bg-red-700 active:bg-red-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500"
3833
- onClick={resetGame}
3834
- >
3835
- Restart
3836
- </button>
3837
- )}
3838
- </div>
3839
- );
3840
- }
3841
- `;
3842
- }
3843
3711
  function generateFullstackHomePage(projectName) {
3844
3712
  return `import React from "react";
3845
3713
 
@@ -3850,7 +3718,7 @@ export default function HomePage() {
3850
3718
  <p className="eyebrow">THE RED ROUTER FRAMEWORK</p>
3851
3719
  <h1>Welcome to ${projectName}</h1>
3852
3720
  <p>
3853
- Built with Rakta.js \u2014 Small in size. Fierce in speed. Alive in every route.
3721
+ Built with Rakta.js \xE2\u20AC\u201D Small in size. Fierce in speed. Alive in every route.
3854
3722
  </p>
3855
3723
  <div className="button-row">
3856
3724
  <a href="/about">About</a>
@@ -4061,7 +3929,7 @@ function generateProjectReadme(projectConfig) {
4061
3929
  if (projectMode === "frontend-only") {
4062
3930
  return `# ${projectName}
4063
3931
 
4064
- Built with Rakta.js \u2014 Small in size. Fierce in speed. Alive in every route.
3932
+ Built with Rakta.js \xE2\u20AC\u201D Small in size. Fierce in speed. Alive in every route.
4065
3933
 
4066
3934
  ## Stack
4067
3935
 
@@ -4080,12 +3948,12 @@ bun run dev
4080
3948
 
4081
3949
  ## ShrimpRun
4082
3950
 
4083
- Your starter includes ShrimpRun \u2014 an interactive game where a shrimp dodges obstacles. Press Space or click to jump!
3951
+ Your starter includes ShrimpRun \xE2\u20AC\u201D an interactive game where a shrimp dodges obstacles. Press Space or click to jump!
4084
3952
  `;
4085
3953
  }
4086
3954
  return `# ${projectName}
4087
3955
 
4088
- Built with Rakta.js \u2014 Small in size. Fierce in speed. Alive in every route.
3956
+ Built with Rakta.js \xE2\u20AC\u201D Small in size. Fierce in speed. Alive in every route.
4089
3957
 
4090
3958
  ## Stack
4091
3959
 
@@ -4200,6 +4068,24 @@ async function promptCssFramework() {
4200
4068
  });
4201
4069
  return getPromptValue(promptResult);
4202
4070
  }
4071
+ async function promptProjectLanguage() {
4072
+ const promptResult = await select({
4073
+ message: "Choose a language:",
4074
+ options: [
4075
+ {
4076
+ value: "typescript",
4077
+ label: PROJECT_LANGUAGE_DISPLAY.typescript,
4078
+ hint: "recommended"
4079
+ },
4080
+ {
4081
+ value: "javascript",
4082
+ label: PROJECT_LANGUAGE_DISPLAY.javascript
4083
+ }
4084
+ ],
4085
+ initialValue: "typescript"
4086
+ });
4087
+ return getPromptValue(promptResult);
4088
+ }
4203
4089
  async function promptRenderMode() {
4204
4090
  const promptResult = await select({
4205
4091
  message: "Choose a render mode:",
@@ -4312,12 +4198,16 @@ async function promptDatabase() {
4312
4198
  }
4313
4199
  async function runPrompts(projectName) {
4314
4200
  const projectMode = await promptProjectMode();
4201
+ const language = await promptProjectLanguage();
4315
4202
  const cssFramework = await promptCssFramework();
4316
4203
  const renderMode = await promptRenderMode();
4204
+ const useTypeScript = language === "typescript";
4317
4205
  if (projectMode === "frontend-only") {
4318
4206
  return {
4319
4207
  projectName,
4320
4208
  projectMode,
4209
+ language,
4210
+ useTypeScript,
4321
4211
  cssFramework,
4322
4212
  renderMode,
4323
4213
  backendFramework: "gaman",
@@ -4329,6 +4219,8 @@ async function runPrompts(projectName) {
4329
4219
  return {
4330
4220
  projectName,
4331
4221
  projectMode,
4222
+ language,
4223
+ useTypeScript,
4332
4224
  cssFramework,
4333
4225
  renderMode,
4334
4226
  backendFramework,
@@ -4346,24 +4238,25 @@ function getProjectNameFromArgs(cliArgs) {
4346
4238
  }
4347
4239
  function formatFullstackCommands() {
4348
4240
  return [
4349
- `${import_picocolors.default.dim("# Terminal 1")}`,
4350
- "cd frontend",
4351
- "bun install",
4352
- "bun run dev",
4241
+ import_picocolors.default.dim("# Terminal 1"),
4242
+ import_picocolors.default.cyan("cd frontend"),
4243
+ import_picocolors.default.cyan("bun install"),
4244
+ import_picocolors.default.cyan("bun run dev"),
4353
4245
  "",
4354
- `${import_picocolors.default.dim("# Terminal 2")}`,
4355
- "cd backend",
4356
- "bun install",
4357
- "bun run dev"
4358
- ].join(`
4359
- `);
4246
+ import_picocolors.default.dim("# Terminal 2"),
4247
+ import_picocolors.default.cyan("cd backend"),
4248
+ import_picocolors.default.cyan("bun install"),
4249
+ import_picocolors.default.cyan("bun run dev")
4250
+ ].map((line) => line.length === 0 ? "" : ` ${line}`).join(`
4251
+ `);
4360
4252
  }
4361
4253
  function formatFrontendOnlyCommands(projectName) {
4362
- return [`cd ${projectName}`, "bun install", "bun run dev"].join(`
4363
- `);
4254
+ return [`cd ${projectName}`, "bun install", "bun run dev"].map((command) => ` ${import_picocolors.default.cyan(command)}`).join(`
4255
+ `);
4364
4256
  }
4365
4257
  function printSuccessMessage(projectConfig) {
4366
4258
  const modeLabel = PROJECT_MODE_DISPLAY[projectConfig.projectMode];
4259
+ const languageLabel = PROJECT_LANGUAGE_DISPLAY[projectConfig.language];
4367
4260
  const cssLabel = CSS_DISPLAY[projectConfig.cssFramework];
4368
4261
  const renderLabel = RENDER_MODE_DISPLAY[projectConfig.renderMode];
4369
4262
  const isFullstack = projectConfig.projectMode === "fullstack";
@@ -4374,6 +4267,7 @@ function printSuccessMessage(projectConfig) {
4374
4267
  ${import_picocolors.default.bold(import_picocolors.default.green("Project created!"))}
4375
4268
 
4376
4269
  ${import_picocolors.default.dim("Mode:")} ${modeLabel}
4270
+ ${import_picocolors.default.dim("Lang:")} ${languageLabel}
4377
4271
  ${import_picocolors.default.dim("CSS:")} ${cssLabel}
4378
4272
  ${import_picocolors.default.dim("Render:")} ${renderLabel}
4379
4273
  ${backendLine}
@@ -4381,7 +4275,7 @@ function printSuccessMessage(projectConfig) {
4381
4275
 
4382
4276
  ${import_picocolors.default.bold("Next steps:")}
4383
4277
 
4384
- ${nextSteps}
4278
+ ${nextSteps}
4385
4279
 
4386
4280
  ${import_picocolors.default.bold("Frontend:")} ${import_picocolors.default.cyan("http://localhost:3000")}
4387
4281
  ${isFullstack ? `${import_picocolors.default.bold("Backend:")} ${import_picocolors.default.cyan("http://localhost:4000")}` : ""}
@@ -4421,4 +4315,4 @@ Error: ${errorMessage}
4421
4315
  process.exit(1);
4422
4316
  });
4423
4317
 
4424
- //# debugId=5A42EB5C435F19ED64756E2164756E21
4318
+ //# debugId=F17FA4C670BBE80264756E2164756E21