create-rakta-app 0.1.8 → 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 +840 -1093
- package/dist/index.js.map +8 -7
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1666,6 +1666,625 @@ 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 ?? window.webkitAudioContext;
|
|
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 {
|
|
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
|
+
}
|
|
1914
|
+
|
|
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);
|
|
1943
|
+
|
|
1944
|
+
useEffect(() => {
|
|
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();
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
statusRef.current = "SWIMMING";
|
|
1977
|
+
setStatus("SWIMMING");
|
|
1978
|
+
}, [resetObstacle]);
|
|
1979
|
+
|
|
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;
|
|
2008
|
+
|
|
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);
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
setPlayerY(playerYRef.current);
|
|
2056
|
+
setObstacleX(obstacleXRef.current);
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
animationRef.current = requestAnimationFrame(frame);
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
animationRef.current = requestAnimationFrame(frame);
|
|
2063
|
+
return () => {
|
|
2064
|
+
if (animationRef.current !== null) cancelAnimationFrame(animationRef.current);
|
|
2065
|
+
};
|
|
2066
|
+
}, [bestScore, lowLatencyMode, obstacleHeight, obstacleWidth, resetObstacle, simSpeed]);
|
|
2067
|
+
|
|
2068
|
+
const obstacleSizeClass =
|
|
2069
|
+
obstacleHeight < 82 ? "KECIL" : obstacleHeight < 120 ? "SEDANG" : "BESAR";
|
|
2070
|
+
const obstaclePos = "BOTTOM" as const;
|
|
2071
|
+
|
|
2072
|
+
return (
|
|
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
|
|
2082
|
+
</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>
|
|
2089
|
+
|
|
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]">
|
|
2091
|
+
<div>
|
|
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>
|
|
2117
|
+
</div>
|
|
2118
|
+
</div>
|
|
2119
|
+
|
|
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
|
+
))}
|
|
2134
|
+
</div>
|
|
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>
|
|
2151
|
+
|
|
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>
|
|
2161
|
+
|
|
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>
|
|
2179
|
+
|
|
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>
|
|
2186
|
+
|
|
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>
|
|
2197
|
+
|
|
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
|
+
)}
|
|
2211
|
+
</button>
|
|
2212
|
+
|
|
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>
|
|
2221
|
+
|
|
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>
|
|
2264
|
+
|
|
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>
|
|
2284
|
+
);
|
|
2285
|
+
}
|
|
2286
|
+
`;
|
|
2287
|
+
|
|
1669
2288
|
// src/types.ts
|
|
1670
2289
|
var CSS_DISPLAY = {
|
|
1671
2290
|
tailwind: "Tailwind CSS",
|
|
@@ -1703,14 +2322,18 @@ var PROJECT_MODE_DISPLAY = {
|
|
|
1703
2322
|
fullstack: "Fullstack app (frontend + backend + database)",
|
|
1704
2323
|
"frontend-only": "Frontend only (no backend, no database)"
|
|
1705
2324
|
};
|
|
2325
|
+
var PROJECT_LANGUAGE_DISPLAY = {
|
|
2326
|
+
typescript: "TypeScript / TSX",
|
|
2327
|
+
javascript: "JavaScript / JSX"
|
|
2328
|
+
};
|
|
1706
2329
|
|
|
1707
2330
|
// src/generator.ts
|
|
1708
2331
|
var DEFAULT_METADATA_TITLE = "Rakta.js | Small in size. Fierce in speed. Alive in every route";
|
|
1709
2332
|
var FAVICON_BYTES = readFileSync(new URL("../assets/favicon.ico", import.meta.url));
|
|
1710
2333
|
function getRootFiles(projectConfig) {
|
|
1711
|
-
const { projectName, projectMode } = projectConfig;
|
|
2334
|
+
const { projectName, projectMode, useTypeScript } = projectConfig;
|
|
1712
2335
|
const workspaces = projectMode === "fullstack" ? ["frontend", "backend", "shared"] : [];
|
|
1713
|
-
|
|
2336
|
+
const files = [
|
|
1714
2337
|
{
|
|
1715
2338
|
path: "package.json",
|
|
1716
2339
|
content: JSON.stringify({
|
|
@@ -1725,36 +2348,16 @@ function getRootFiles(projectConfig) {
|
|
|
1725
2348
|
"build:backend": "cd backend && bun run build",
|
|
1726
2349
|
build: "bun run build:frontend && bun run build:backend",
|
|
1727
2350
|
start: "cd backend && bun run start",
|
|
1728
|
-
|
|
2351
|
+
...useTypeScript ? {
|
|
2352
|
+
typecheck: "cd frontend && bun run typecheck && cd ../backend && bun run typecheck"
|
|
2353
|
+
} : {}
|
|
1729
2354
|
} : {
|
|
1730
2355
|
dev: "rakta dev",
|
|
1731
2356
|
build: "rakta build",
|
|
1732
2357
|
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
|
|
2358
|
+
...useTypeScript ? { typecheck: "tsc --noEmit" } : {}
|
|
1756
2359
|
},
|
|
1757
|
-
|
|
2360
|
+
description: `${projectName} \xE2\u20AC\u201D built with Rakta.js`
|
|
1758
2361
|
}, null, 2)
|
|
1759
2362
|
},
|
|
1760
2363
|
{
|
|
@@ -1799,11 +2402,38 @@ dist/
|
|
|
1799
2402
|
content: generateProjectReadme(projectConfig)
|
|
1800
2403
|
}
|
|
1801
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;
|
|
1802
2430
|
}
|
|
1803
2431
|
function getFrontendOnlyFiles(projectConfig) {
|
|
1804
|
-
const { projectName, cssFramework } = projectConfig;
|
|
2432
|
+
const { projectName, cssFramework, useTypeScript } = projectConfig;
|
|
1805
2433
|
const styleFileName = cssFramework === "sass" ? "globals.scss" : "globals.css";
|
|
1806
|
-
|
|
2434
|
+
const pageExtension = useTypeScript ? "tsx" : "jsx";
|
|
2435
|
+
const scriptExtension = useTypeScript ? "ts" : "js";
|
|
2436
|
+
const files = [
|
|
1807
2437
|
{
|
|
1808
2438
|
path: "package.json",
|
|
1809
2439
|
content: JSON.stringify({
|
|
@@ -1816,47 +2446,27 @@ function getFrontendOnlyFiles(projectConfig) {
|
|
|
1816
2446
|
build: "rakta build",
|
|
1817
2447
|
start: "rakta start",
|
|
1818
2448
|
routes: "rakta routes",
|
|
1819
|
-
typecheck: "tsc --noEmit"
|
|
2449
|
+
...useTypeScript ? { typecheck: "tsc --noEmit" } : {}
|
|
1820
2450
|
},
|
|
1821
2451
|
dependencies: {
|
|
1822
|
-
raktajs: "^0.1.
|
|
2452
|
+
raktajs: "^0.1.9",
|
|
1823
2453
|
react: "^19.2.7",
|
|
1824
2454
|
"react-dom": "^19.2.7",
|
|
2455
|
+
"react-icons": "^5.7.0",
|
|
1825
2456
|
...getCssDependencies(cssFramework)
|
|
1826
2457
|
},
|
|
1827
|
-
devDependencies: {
|
|
2458
|
+
devDependencies: useTypeScript ? {
|
|
1828
2459
|
"@types/react": "^19.2.17",
|
|
1829
2460
|
"@types/react-dom": "^19.2.3",
|
|
1830
2461
|
typescript: "^6.0.3",
|
|
1831
2462
|
...getCssDevDependencies(cssFramework)
|
|
2463
|
+
} : {
|
|
2464
|
+
...getCssDevDependencies(cssFramework)
|
|
1832
2465
|
}
|
|
1833
2466
|
}, null, 2)
|
|
1834
2467
|
},
|
|
1835
2468
|
{
|
|
1836
|
-
path:
|
|
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",
|
|
2469
|
+
path: `rakta.config.${scriptExtension}`,
|
|
1860
2470
|
content: `import { defineRaktaConfig } from "raktajs";
|
|
1861
2471
|
|
|
1862
2472
|
export default defineRaktaConfig({
|
|
@@ -1873,32 +2483,40 @@ export default defineRaktaConfig({
|
|
|
1873
2483
|
`
|
|
1874
2484
|
},
|
|
1875
2485
|
{
|
|
1876
|
-
path:
|
|
2486
|
+
path: `app/layout.${pageExtension}`,
|
|
1877
2487
|
content: generateFrontendOnlyLayout()
|
|
1878
2488
|
},
|
|
1879
2489
|
{
|
|
1880
|
-
path:
|
|
2490
|
+
path: `app/page.${pageExtension}`,
|
|
1881
2491
|
content: generateFrontendOnlyPage(projectName)
|
|
1882
2492
|
},
|
|
1883
2493
|
{
|
|
1884
|
-
path:
|
|
2494
|
+
path: `app/loading.${pageExtension}`,
|
|
1885
2495
|
content: generateFrontendOnlyLoading()
|
|
1886
2496
|
},
|
|
1887
2497
|
{
|
|
1888
|
-
path:
|
|
2498
|
+
path: `app/error.${pageExtension}`,
|
|
1889
2499
|
content: generateFrontendOnlyError()
|
|
1890
2500
|
},
|
|
1891
2501
|
{
|
|
1892
|
-
path:
|
|
2502
|
+
path: `app/notFound.${pageExtension}`,
|
|
1893
2503
|
content: generateFrontendOnlyNotFound()
|
|
1894
2504
|
},
|
|
1895
2505
|
{
|
|
1896
|
-
path:
|
|
1897
|
-
content:
|
|
2506
|
+
path: `app/components/CoralObstacle.${pageExtension}`,
|
|
2507
|
+
content: STARTER_CORAL_OBSTACLE_CODE
|
|
2508
|
+
},
|
|
2509
|
+
{
|
|
2510
|
+
path: `app/components/ShrimpCharacter.${pageExtension}`,
|
|
2511
|
+
content: STARTER_SHRIMP_CHARACTER_CODE
|
|
2512
|
+
},
|
|
2513
|
+
{
|
|
2514
|
+
path: `app/utils/audio.${scriptExtension}`,
|
|
2515
|
+
content: STARTER_AUDIO_CODE
|
|
1898
2516
|
},
|
|
1899
2517
|
{
|
|
1900
|
-
path:
|
|
1901
|
-
content:
|
|
2518
|
+
path: `app/types.${scriptExtension}`,
|
|
2519
|
+
content: STARTER_TYPES_CODE
|
|
1902
2520
|
},
|
|
1903
2521
|
{
|
|
1904
2522
|
path: `styles/${styleFileName}`,
|
|
@@ -1913,6 +2531,31 @@ export default defineRaktaConfig({
|
|
|
1913
2531
|
content: FAVICON_BYTES
|
|
1914
2532
|
}
|
|
1915
2533
|
];
|
|
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);
|
|
1916
2559
|
}
|
|
1917
2560
|
function getFullstackFrontendFiles(projectConfig) {
|
|
1918
2561
|
const { projectName, cssFramework } = projectConfig;
|
|
@@ -1934,7 +2577,7 @@ function getFullstackFrontendFiles(projectConfig) {
|
|
|
1934
2577
|
typecheck: "tsc --noEmit"
|
|
1935
2578
|
},
|
|
1936
2579
|
dependencies: {
|
|
1937
|
-
raktajs: "^0.1.
|
|
2580
|
+
raktajs: "^0.1.9",
|
|
1938
2581
|
react: "^19.2.7",
|
|
1939
2582
|
"react-dom": "^19.2.7",
|
|
1940
2583
|
...getCssDependencies(cssFramework)
|
|
@@ -1974,7 +2617,7 @@ export default defineRaktaConfig({
|
|
|
1974
2617
|
appName: "${projectName}",
|
|
1975
2618
|
seo: {
|
|
1976
2619
|
defaultTitle: "${DEFAULT_METADATA_TITLE}",
|
|
1977
|
-
defaultDescription: "Built with Rakta.js \
|
|
2620
|
+
defaultDescription: "Built with Rakta.js \xE2\u20AC\u201D Small in size. Fierce in speed. Alive in every route.",
|
|
1978
2621
|
},
|
|
1979
2622
|
render: {
|
|
1980
2623
|
defaultMode: "csr",
|
|
@@ -2230,7 +2873,7 @@ export type UserSchema = typeof userSchema;
|
|
|
2230
2873
|
},
|
|
2231
2874
|
{
|
|
2232
2875
|
path: `frontend/styles/${styleFileName}`,
|
|
2233
|
-
content:
|
|
2876
|
+
content: getFrontendOnlyCssGlobals(cssFramework)
|
|
2234
2877
|
},
|
|
2235
2878
|
{
|
|
2236
2879
|
path: "frontend/public/.gitkeep",
|
|
@@ -2250,6 +2893,28 @@ export type UserSchema = typeof userSchema;
|
|
|
2250
2893
|
}
|
|
2251
2894
|
];
|
|
2252
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 =");
|
|
2898
|
+
}
|
|
2899
|
+
function processFilesForLanguage(files, useTypeScript) {
|
|
2900
|
+
if (useTypeScript) {
|
|
2901
|
+
return files;
|
|
2902
|
+
}
|
|
2903
|
+
return files.filter((file) => !file.path.endsWith(".d.ts")).map((file) => {
|
|
2904
|
+
if (typeof file.content !== "string") {
|
|
2905
|
+
return file;
|
|
2906
|
+
}
|
|
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
|
+
};
|
|
2916
|
+
});
|
|
2917
|
+
}
|
|
2253
2918
|
function getBackendFiles(projectConfig) {
|
|
2254
2919
|
return [
|
|
2255
2920
|
...getBackendCommonFiles(projectConfig),
|
|
@@ -2672,431 +3337,99 @@ function getCssDevDependencies(cssFramework) {
|
|
|
2672
3337
|
}
|
|
2673
3338
|
}
|
|
2674
3339
|
function getFrontendOnlyCssGlobals(cssFramework) {
|
|
2675
|
-
const cssImport = cssFramework === "tailwind" ? `@import "
|
|
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";
|
|
2676
3342
|
|
|
2677
3343
|
` : cssFramework === "bootstrap" ? `@import url("https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css");
|
|
2678
3344
|
|
|
2679
|
-
` : cssFramework === "sass" ? `$color-primary: #
|
|
3345
|
+
` : cssFramework === "sass" ? `$color-primary: #e11d48;
|
|
2680
3346
|
$color-background: #050505;
|
|
2681
|
-
$color-foreground: #
|
|
3347
|
+
$color-foreground: #fafafa;
|
|
2682
3348
|
|
|
2683
3349
|
` : "";
|
|
2684
|
-
return `${cssImport}
|
|
2685
|
-
--
|
|
2686
|
-
--
|
|
2687
|
-
--color-
|
|
2688
|
-
--color-
|
|
2689
|
-
--color-
|
|
2690
|
-
--color-
|
|
2691
|
-
color-
|
|
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;
|
|
2692
3358
|
}
|
|
2693
3359
|
|
|
2694
|
-
|
|
2695
|
-
|
|
3360
|
+
:root {
|
|
3361
|
+
color-scheme: dark;
|
|
3362
|
+
background: #050505;
|
|
3363
|
+
color: #fafafa;
|
|
3364
|
+
font-family: var(--font-sans);
|
|
2696
3365
|
}
|
|
2697
3366
|
|
|
3367
|
+
* { box-sizing: border-box; }
|
|
3368
|
+
html { scroll-behavior: smooth; }
|
|
2698
3369
|
body {
|
|
2699
3370
|
margin: 0;
|
|
2700
3371
|
min-width: 320px;
|
|
2701
3372
|
min-height: 100vh;
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
|
2706
|
-
Roboto, sans-serif;
|
|
3373
|
+
background: #050505;
|
|
3374
|
+
color: #fafafa;
|
|
3375
|
+
overflow-x: hidden;
|
|
2707
3376
|
-webkit-font-smoothing: antialiased;
|
|
2708
3377
|
}
|
|
3378
|
+
button, a { font: inherit; }
|
|
3379
|
+
a { color: inherit; text-decoration: none; }
|
|
3380
|
+
#rakta-root { min-height: 100vh; }
|
|
2709
3381
|
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
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 {
|
|
2727
|
-
min-height: 100vh;
|
|
2728
|
-
}
|
|
2729
|
-
|
|
2730
|
-
main {
|
|
2731
|
-
width: min(1120px, calc(100% - 32px));
|
|
2732
|
-
min-height: 100vh;
|
|
2733
|
-
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);
|
|
2746
|
-
}
|
|
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;
|
|
2759
|
-
}
|
|
2760
|
-
|
|
2761
|
-
h1 {
|
|
2762
|
-
max-width: 760px;
|
|
2763
|
-
font-size: clamp(42px, 8vw, 84px);
|
|
2764
|
-
letter-spacing: 0;
|
|
2765
|
-
}
|
|
2766
|
-
|
|
2767
|
-
h2 {
|
|
2768
|
-
font-size: clamp(28px, 4vw, 44px);
|
|
2769
|
-
}
|
|
2770
|
-
|
|
2771
|
-
click {
|
|
2772
|
-
display: inline-flex;
|
|
2773
|
-
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;
|
|
2788
|
-
}
|
|
2789
|
-
|
|
2790
|
-
click:hover {
|
|
2791
|
-
border-color: #f87171;
|
|
2792
|
-
background: rgba(220, 38, 38, 0.12);
|
|
2793
|
-
transform: translateY(-1px);
|
|
2794
|
-
}
|
|
2795
|
-
|
|
2796
|
-
button {
|
|
2797
|
-
font: inherit;
|
|
2798
|
-
}
|
|
2799
|
-
|
|
2800
|
-
.relative {
|
|
2801
|
-
position: relative;
|
|
2802
|
-
}
|
|
2803
|
-
|
|
2804
|
-
.absolute {
|
|
2805
|
-
position: absolute;
|
|
2806
|
-
}
|
|
2807
|
-
|
|
2808
|
-
.inset-0 {
|
|
2809
|
-
inset: 0;
|
|
2810
|
-
}
|
|
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; }
|
|
2811
3386
|
|
|
2812
|
-
|
|
2813
|
-
|
|
3387
|
+
@keyframes scanline {
|
|
3388
|
+
0% { transform: translateY(-100%); }
|
|
3389
|
+
100% { transform: translateY(100vh); }
|
|
2814
3390
|
}
|
|
2815
|
-
|
|
2816
|
-
|
|
3391
|
+
.scanline {
|
|
3392
|
+
position: fixed;
|
|
3393
|
+
top: 0;
|
|
2817
3394
|
left: 0;
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
.top-2 {
|
|
2821
|
-
top: 8px;
|
|
2822
|
-
}
|
|
2823
|
-
|
|
2824
|
-
.right-3 {
|
|
2825
|
-
right: 12px;
|
|
2826
|
-
}
|
|
2827
|
-
|
|
2828
|
-
.block {
|
|
2829
|
-
display: block;
|
|
2830
|
-
}
|
|
2831
|
-
|
|
2832
|
-
.flex {
|
|
2833
|
-
display: flex;
|
|
2834
|
-
}
|
|
2835
|
-
|
|
2836
|
-
.flex-col {
|
|
2837
|
-
flex-direction: column;
|
|
2838
|
-
}
|
|
2839
|
-
|
|
2840
|
-
.flex-wrap {
|
|
2841
|
-
flex-wrap: wrap;
|
|
2842
|
-
}
|
|
2843
|
-
|
|
2844
|
-
.items-center {
|
|
2845
|
-
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 {
|
|
2861
|
-
gap: 16px;
|
|
2862
|
-
}
|
|
2863
|
-
|
|
2864
|
-
.gap-8 {
|
|
2865
|
-
gap: 32px;
|
|
2866
|
-
}
|
|
2867
|
-
|
|
2868
|
-
.w-full {
|
|
3395
|
+
z-index: 999;
|
|
2869
3396
|
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 {
|
|
3397
|
+
height: 120px;
|
|
2937
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;
|
|
2938
3402
|
}
|
|
2939
3403
|
|
|
2940
|
-
.
|
|
2941
|
-
|
|
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);
|
|
2942
3409
|
}
|
|
2943
3410
|
|
|
2944
|
-
|
|
2945
|
-
|
|
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); }
|
|
2946
3415
|
}
|
|
2947
|
-
|
|
2948
|
-
.
|
|
2949
|
-
|
|
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); }
|
|
2950
3420
|
}
|
|
2951
|
-
|
|
2952
|
-
.
|
|
2953
|
-
|
|
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;
|
|
2954
3425
|
}
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
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;
|
|
2976
|
-
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
|
-
}
|
|
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: "";
|
|
3003
|
-
}
|
|
3004
|
-
|
|
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;
|
|
3019
|
-
}
|
|
3020
|
-
|
|
3021
|
-
section {
|
|
3022
|
-
padding: 20px;
|
|
3023
|
-
}
|
|
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;
|
|
3024
3430
|
}
|
|
3025
3431
|
`;
|
|
3026
3432
|
}
|
|
3027
|
-
function getCssGlobals(cssFramework) {
|
|
3028
|
-
const sharedBase = `
|
|
3029
|
-
:root {
|
|
3030
|
-
--color-primary: #dc2626;
|
|
3031
|
-
--color-background: #050505;
|
|
3032
|
-
--color-foreground: #f8fafc;
|
|
3033
|
-
}
|
|
3034
|
-
|
|
3035
|
-
* {
|
|
3036
|
-
box-sizing: border-box;
|
|
3037
|
-
}
|
|
3038
|
-
|
|
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;
|
|
3044
|
-
}
|
|
3045
|
-
|
|
3046
|
-
.page-shell {
|
|
3047
|
-
width: min(100% - 2rem, 960px);
|
|
3048
|
-
margin: 0 auto;
|
|
3049
|
-
padding: 4rem 0;
|
|
3050
|
-
}
|
|
3051
|
-
|
|
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;
|
|
3059
|
-
}
|
|
3060
|
-
|
|
3061
|
-
.eyebrow {
|
|
3062
|
-
color: #dc2626;
|
|
3063
|
-
font-weight: 700;
|
|
3064
|
-
letter-spacing: 0.12em;
|
|
3065
|
-
font-size: 0.75rem;
|
|
3066
|
-
text-transform: uppercase;
|
|
3067
|
-
}
|
|
3068
|
-
|
|
3069
|
-
.button-row {
|
|
3070
|
-
display: flex;
|
|
3071
|
-
gap: 0.75rem;
|
|
3072
|
-
flex-wrap: wrap;
|
|
3073
|
-
margin-top: 1rem;
|
|
3074
|
-
}
|
|
3075
|
-
|
|
3076
|
-
a {
|
|
3077
|
-
color: #dc2626;
|
|
3078
|
-
}
|
|
3079
|
-
|
|
3080
|
-
button {
|
|
3081
|
-
cursor: pointer;
|
|
3082
|
-
}
|
|
3083
|
-
`;
|
|
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
|
-
}
|
|
3100
3433
|
function generateFrontendOnlyRaktaEnv() {
|
|
3101
3434
|
return `declare module "*.css";
|
|
3102
3435
|
declare module "*.scss";
|
|
@@ -3115,14 +3448,6 @@ declare global {
|
|
|
3115
3448
|
click: RaktaClickAttributes;
|
|
3116
3449
|
}
|
|
3117
3450
|
}
|
|
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
3451
|
}
|
|
3127
3452
|
|
|
3128
3453
|
declare module "react/jsx-runtime" {
|
|
@@ -3165,54 +3490,8 @@ export default function RootLayout({ children }: RootLayoutProps) {
|
|
|
3165
3490
|
}
|
|
3166
3491
|
`;
|
|
3167
3492
|
}
|
|
3168
|
-
function generateFrontendOnlyPage(
|
|
3169
|
-
return
|
|
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
|
-
`;
|
|
3493
|
+
function generateFrontendOnlyPage(_projectName) {
|
|
3494
|
+
return STARTER_PAGE_CODE;
|
|
3216
3495
|
}
|
|
3217
3496
|
function generateFrontendOnlyLoading() {
|
|
3218
3497
|
return `export default function Loading() {
|
|
@@ -3282,564 +3561,6 @@ function generateFrontendOnlyNotFound() {
|
|
|
3282
3561
|
}
|
|
3283
3562
|
`;
|
|
3284
3563
|
}
|
|
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
3564
|
function generateFullstackHomePage(projectName) {
|
|
3844
3565
|
return `import React from "react";
|
|
3845
3566
|
|
|
@@ -3850,7 +3571,7 @@ export default function HomePage() {
|
|
|
3850
3571
|
<p className="eyebrow">THE RED ROUTER FRAMEWORK</p>
|
|
3851
3572
|
<h1>Welcome to ${projectName}</h1>
|
|
3852
3573
|
<p>
|
|
3853
|
-
Built with Rakta.js \
|
|
3574
|
+
Built with Rakta.js \xE2\u20AC\u201D Small in size. Fierce in speed. Alive in every route.
|
|
3854
3575
|
</p>
|
|
3855
3576
|
<div className="button-row">
|
|
3856
3577
|
<a href="/about">About</a>
|
|
@@ -4061,7 +3782,7 @@ function generateProjectReadme(projectConfig) {
|
|
|
4061
3782
|
if (projectMode === "frontend-only") {
|
|
4062
3783
|
return `# ${projectName}
|
|
4063
3784
|
|
|
4064
|
-
Built with Rakta.js \
|
|
3785
|
+
Built with Rakta.js \xE2\u20AC\u201D Small in size. Fierce in speed. Alive in every route.
|
|
4065
3786
|
|
|
4066
3787
|
## Stack
|
|
4067
3788
|
|
|
@@ -4080,12 +3801,12 @@ bun run dev
|
|
|
4080
3801
|
|
|
4081
3802
|
## ShrimpRun
|
|
4082
3803
|
|
|
4083
|
-
Your starter includes ShrimpRun \
|
|
3804
|
+
Your starter includes ShrimpRun \xE2\u20AC\u201D an interactive game where a shrimp dodges obstacles. Press Space or click to jump!
|
|
4084
3805
|
`;
|
|
4085
3806
|
}
|
|
4086
3807
|
return `# ${projectName}
|
|
4087
3808
|
|
|
4088
|
-
Built with Rakta.js \
|
|
3809
|
+
Built with Rakta.js \xE2\u20AC\u201D Small in size. Fierce in speed. Alive in every route.
|
|
4089
3810
|
|
|
4090
3811
|
## Stack
|
|
4091
3812
|
|
|
@@ -4200,6 +3921,24 @@ async function promptCssFramework() {
|
|
|
4200
3921
|
});
|
|
4201
3922
|
return getPromptValue(promptResult);
|
|
4202
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
|
+
}
|
|
4203
3942
|
async function promptRenderMode() {
|
|
4204
3943
|
const promptResult = await select({
|
|
4205
3944
|
message: "Choose a render mode:",
|
|
@@ -4312,12 +4051,16 @@ async function promptDatabase() {
|
|
|
4312
4051
|
}
|
|
4313
4052
|
async function runPrompts(projectName) {
|
|
4314
4053
|
const projectMode = await promptProjectMode();
|
|
4054
|
+
const language = await promptProjectLanguage();
|
|
4315
4055
|
const cssFramework = await promptCssFramework();
|
|
4316
4056
|
const renderMode = await promptRenderMode();
|
|
4057
|
+
const useTypeScript = language === "typescript";
|
|
4317
4058
|
if (projectMode === "frontend-only") {
|
|
4318
4059
|
return {
|
|
4319
4060
|
projectName,
|
|
4320
4061
|
projectMode,
|
|
4062
|
+
language,
|
|
4063
|
+
useTypeScript,
|
|
4321
4064
|
cssFramework,
|
|
4322
4065
|
renderMode,
|
|
4323
4066
|
backendFramework: "gaman",
|
|
@@ -4329,6 +4072,8 @@ async function runPrompts(projectName) {
|
|
|
4329
4072
|
return {
|
|
4330
4073
|
projectName,
|
|
4331
4074
|
projectMode,
|
|
4075
|
+
language,
|
|
4076
|
+
useTypeScript,
|
|
4332
4077
|
cssFramework,
|
|
4333
4078
|
renderMode,
|
|
4334
4079
|
backendFramework,
|
|
@@ -4346,24 +4091,25 @@ function getProjectNameFromArgs(cliArgs) {
|
|
|
4346
4091
|
}
|
|
4347
4092
|
function formatFullstackCommands() {
|
|
4348
4093
|
return [
|
|
4349
|
-
|
|
4350
|
-
"cd frontend",
|
|
4351
|
-
"bun install",
|
|
4352
|
-
"bun run dev",
|
|
4094
|
+
import_picocolors.default.dim("# Terminal 1"),
|
|
4095
|
+
import_picocolors.default.cyan("cd frontend"),
|
|
4096
|
+
import_picocolors.default.cyan("bun install"),
|
|
4097
|
+
import_picocolors.default.cyan("bun run dev"),
|
|
4353
4098
|
"",
|
|
4354
|
-
|
|
4355
|
-
"cd backend",
|
|
4356
|
-
"bun install",
|
|
4357
|
-
"bun run dev"
|
|
4358
|
-
].join(`
|
|
4359
|
-
|
|
4099
|
+
import_picocolors.default.dim("# Terminal 2"),
|
|
4100
|
+
import_picocolors.default.cyan("cd backend"),
|
|
4101
|
+
import_picocolors.default.cyan("bun install"),
|
|
4102
|
+
import_picocolors.default.cyan("bun run dev")
|
|
4103
|
+
].map((line) => line.length === 0 ? "" : ` ${line}`).join(`
|
|
4104
|
+
`);
|
|
4360
4105
|
}
|
|
4361
4106
|
function formatFrontendOnlyCommands(projectName) {
|
|
4362
|
-
return [`cd ${projectName}`, "bun install", "bun run dev"].join(`
|
|
4363
|
-
|
|
4107
|
+
return [`cd ${projectName}`, "bun install", "bun run dev"].map((command) => ` ${import_picocolors.default.cyan(command)}`).join(`
|
|
4108
|
+
`);
|
|
4364
4109
|
}
|
|
4365
4110
|
function printSuccessMessage(projectConfig) {
|
|
4366
4111
|
const modeLabel = PROJECT_MODE_DISPLAY[projectConfig.projectMode];
|
|
4112
|
+
const languageLabel = PROJECT_LANGUAGE_DISPLAY[projectConfig.language];
|
|
4367
4113
|
const cssLabel = CSS_DISPLAY[projectConfig.cssFramework];
|
|
4368
4114
|
const renderLabel = RENDER_MODE_DISPLAY[projectConfig.renderMode];
|
|
4369
4115
|
const isFullstack = projectConfig.projectMode === "fullstack";
|
|
@@ -4374,6 +4120,7 @@ function printSuccessMessage(projectConfig) {
|
|
|
4374
4120
|
${import_picocolors.default.bold(import_picocolors.default.green("Project created!"))}
|
|
4375
4121
|
|
|
4376
4122
|
${import_picocolors.default.dim("Mode:")} ${modeLabel}
|
|
4123
|
+
${import_picocolors.default.dim("Lang:")} ${languageLabel}
|
|
4377
4124
|
${import_picocolors.default.dim("CSS:")} ${cssLabel}
|
|
4378
4125
|
${import_picocolors.default.dim("Render:")} ${renderLabel}
|
|
4379
4126
|
${backendLine}
|
|
@@ -4381,7 +4128,7 @@ function printSuccessMessage(projectConfig) {
|
|
|
4381
4128
|
|
|
4382
4129
|
${import_picocolors.default.bold("Next steps:")}
|
|
4383
4130
|
|
|
4384
|
-
|
|
4131
|
+
${nextSteps}
|
|
4385
4132
|
|
|
4386
4133
|
${import_picocolors.default.bold("Frontend:")} ${import_picocolors.default.cyan("http://localhost:3000")}
|
|
4387
4134
|
${isFullstack ? `${import_picocolors.default.bold("Backend:")} ${import_picocolors.default.cyan("http://localhost:4000")}` : ""}
|
|
@@ -4421,4 +4168,4 @@ Error: ${errorMessage}
|
|
|
4421
4168
|
process.exit(1);
|
|
4422
4169
|
});
|
|
4423
4170
|
|
|
4424
|
-
//# debugId=
|
|
4171
|
+
//# debugId=AC42DEC47A292C4F64756E2164756E21
|