├── src ├── vite-env.d.ts ├── components │ ├── Assets │ │ ├── index.ts │ │ ├── AssetsPanel.css │ │ ├── AssetsTab.tsx │ │ ├── AssetsAddButton.tsx │ │ ├── Assets.tsx │ │ ├── AssetsList.tsx │ │ ├── AssetsEmpty.tsx │ │ ├── AssetsPanel.tsx │ │ └── AssetsItem.tsx │ ├── Toolbar │ │ ├── index.ts │ │ ├── ToolButtons │ │ │ ├── ConfigButton.tsx │ │ │ ├── AboutButton.tsx │ │ │ └── ShareButton.tsx │ │ ├── ToolbarDropdownButton.tsx │ │ ├── Toolbar.tsx │ │ ├── ToolbarDropdown.tsx │ │ ├── ToolbarButton.tsx │ │ ├── ToolbarToolsMenu.tsx │ │ └── ExampleList.tsx │ ├── About │ │ ├── index.ts │ │ └── AboutDialog.tsx │ ├── FileTree │ │ ├── index.ts │ │ ├── FileFolder.css │ │ ├── FileEntry.css │ │ ├── FileToolbar.tsx │ │ ├── FileFold.tsx │ │ └── FileTree.tsx │ ├── ProjectBrowser │ │ ├── index.ts │ │ ├── ProjectBrowser.css │ │ ├── ProjectNotFound.tsx │ │ ├── ProjectCreate.tsx │ │ ├── SortBy.tsx │ │ ├── VersionFilter.tsx │ │ ├── GroupBy.tsx │ │ └── ProjectContextMenu.tsx │ ├── Util │ │ └── ConditionalWrap.tsx │ ├── UI │ │ ├── KDropdown │ │ │ └── KDropdownSeparator.tsx │ │ ├── TabsList.tsx │ │ ├── TabTrigger.tsx │ │ ├── View.tsx │ │ └── ConfirmDialog.tsx │ ├── Project │ │ └── BuildModes │ │ │ ├── ImportCodeExample.tsx │ │ │ └── BuildModesInstructions.tsx │ ├── Playground │ │ ├── LoadingPlayground.tsx │ │ ├── GameView.tsx │ │ ├── WorkspaceExample.tsx │ │ └── WorkspaceProject.tsx │ ├── Config │ │ ├── ConfigDialog.tsx │ │ ├── ConfigForm │ │ │ ├── ConfigSelect.tsx │ │ │ └── ConfigCheckbox.tsx │ │ └── ConfigEditor.tsx │ ├── AssetBrew │ │ ├── AssetBrewItem.tsx │ │ └── AssetBrew.tsx │ └── ConsoleView │ │ └── ConsoleView.tsx ├── features │ ├── Projects │ │ ├── models │ │ │ ├── ProjectMode.ts │ │ │ ├── AssetKind.ts │ │ │ ├── ProjectBuildMode.ts │ │ │ ├── FileFolder.ts │ │ │ ├── FileKind.ts │ │ │ ├── File.ts │ │ │ ├── UploadAsset.ts │ │ │ ├── Asset.ts │ │ │ └── Project.ts │ │ ├── application │ │ │ ├── wrapGame.ts │ │ │ ├── preferredVersion.ts │ │ │ ├── validateProjectName.ts │ │ │ ├── buildCodeLegacy.ts │ │ │ ├── loadProject.ts │ │ │ ├── buildProject.ts │ │ │ ├── buildCode.ts │ │ │ ├── path.ts │ │ │ ├── defaultFavicon.ts │ │ │ └── createDefaultFiles.ts │ │ └── stores │ │ │ ├── useProject.ts │ │ │ └── slices │ │ │ └── assets.ts │ └── Editor │ │ ├── application │ │ ├── clearModels.ts │ │ ├── loadFileInModel.ts │ │ └── insertAfterCursor.ts │ │ └── monaco │ │ ├── fun │ │ └── createConfetti.ts │ │ ├── actions │ │ └── format.ts │ │ ├── completion │ │ ├── ImportPathCompletionProvider.ts │ │ └── CompletionAddProvider.ts │ │ ├── monacoConfig.ts │ │ └── themes │ │ └── themes.ts ├── util │ ├── types.ts │ ├── removeExtensions.ts │ ├── cn.ts │ ├── fileSize.ts │ ├── stringToValue.ts │ ├── download.ts │ ├── regex.ts │ ├── npm.ts │ ├── openDialog.ts │ ├── fileToBase64.ts │ ├── compressCode.ts │ ├── allotmentStorage.ts │ ├── logs.ts │ ├── confirm.ts │ ├── prompt.ts │ ├── scrollbarSize.ts │ ├── confirmNavigate.tsx │ ├── compiler.ts │ └── assetsParsing.ts ├── config │ └── common.ts ├── main.tsx ├── App.tsx ├── hooks │ ├── useBeforeUnload.ts │ ├── useAssets.ts │ └── useConfig.ts ├── data │ └── demos.ts └── styles │ ├── toast.css │ └── index.css ├── public ├── pg.png ├── dino-claw@2x.png └── dino-head@2x.png ├── kaplayground.png ├── .gitmodules ├── .vscode ├── extensions.json ├── settings.json ├── launch.json └── snippets.code-snippets ├── postcss.config.js ├── sandbox ├── wrangler.jsonc ├── vite.config.ts └── index.html ├── .github ├── ISSUE_TEMPLATE │ └── feature_request.md └── workflows │ ├── sync-submodules.yml │ └── deploy.yml ├── tsconfig.json ├── .tmp └── auth_info.json ├── index.html ├── dprint.json ├── tsconfig.node.json ├── .gitignore ├── tsconfig.app.json ├── CONTRIBUTING.md ├── vite.config.ts ├── LICENSE ├── scripts ├── versions.ts ├── types.ts └── examples.ts ├── README.md ├── neutralino.config.json ├── tailwind.config.js └── package.json /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/components/Assets/index.ts: -------------------------------------------------------------------------------- 1 | export { Assets } from "./Assets"; 2 | -------------------------------------------------------------------------------- /src/components/Toolbar/index.ts: -------------------------------------------------------------------------------- 1 | export { Toolbar } from "./Toolbar"; 2 | -------------------------------------------------------------------------------- /src/components/About/index.ts: -------------------------------------------------------------------------------- 1 | export { AboutDialog } from "./AboutDialog"; 2 | -------------------------------------------------------------------------------- /src/components/FileTree/index.ts: -------------------------------------------------------------------------------- 1 | export { FileTree } from "./FileTree"; 2 | -------------------------------------------------------------------------------- /src/components/ProjectBrowser/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ProjectBrowser"; 2 | -------------------------------------------------------------------------------- /public/pg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplayground/HEAD/public/pg.png -------------------------------------------------------------------------------- /src/features/Projects/models/ProjectMode.ts: -------------------------------------------------------------------------------- 1 | export type ProjectMode = "ex" | "pj"; 2 | -------------------------------------------------------------------------------- /kaplayground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplayground/HEAD/kaplayground.png -------------------------------------------------------------------------------- /src/features/Projects/models/AssetKind.ts: -------------------------------------------------------------------------------- 1 | export type AssetKind = "sprite" | "sound" | "font"; 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "kaplay"] 2 | path = kaplay 3 | url = https://github.com/marklovers/kaplay 4 | -------------------------------------------------------------------------------- /public/dino-claw@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplayground/HEAD/public/dino-claw@2x.png -------------------------------------------------------------------------------- /public/dino-head@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplayground/HEAD/public/dino-head@2x.png -------------------------------------------------------------------------------- /src/features/Projects/models/ProjectBuildMode.ts: -------------------------------------------------------------------------------- 1 | export type ProjectBuildMode = "esbuild" | "legacy"; 2 | -------------------------------------------------------------------------------- /src/util/types.ts: -------------------------------------------------------------------------------- 1 | export type Prettify = 2 | & { 3 | [K in keyof T]: T[K]; 4 | } 5 | & {}; 6 | -------------------------------------------------------------------------------- /src/features/Projects/models/FileFolder.ts: -------------------------------------------------------------------------------- 1 | export type FileFolder = "root" | "scenes" | "assets" | "utils" | "objects"; 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /src/features/Projects/models/FileKind.ts: -------------------------------------------------------------------------------- 1 | export type FileKind = "kaplay" | "main" | "scene" | "assets" | "util" | "obj"; 2 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /sandbox/wrangler.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iframe-kaplay", 3 | "compatibility_date": "2025-04-27", 4 | "pages_build_output_dir": "dist", 5 | } 6 | -------------------------------------------------------------------------------- /src/util/removeExtensions.ts: -------------------------------------------------------------------------------- 1 | export const removeExtension = (filename: string) => { 2 | return filename.split(".").slice(0, -1).join("."); 3 | }; 4 | -------------------------------------------------------------------------------- /src/components/ProjectBrowser/ProjectBrowser.css: -------------------------------------------------------------------------------- 1 | .examples-list { 2 | display: grid; 3 | grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); 4 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.preferences.importModuleSpecifier": "relative", 3 | "typescript.preferences.importModuleSpecifierEnding": "minimal" 4 | } 5 | -------------------------------------------------------------------------------- /src/components/FileTree/FileFolder.css: -------------------------------------------------------------------------------- 1 | .folded-icon[data-folded="true"] { 2 | transform: rotate(0deg); 3 | } 4 | 5 | .folded-icon[data-folded="false"] { 6 | transform: rotate(90deg); 7 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature 6 | assignees: '' 7 | type: 'Feature' 8 | --- 9 | -------------------------------------------------------------------------------- /src/util/cn.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...args: ClassValue[]) { 5 | return twMerge(clsx(args)); 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.app.json" 6 | }, 7 | { 8 | "path": "./tsconfig.node.json" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/features/Projects/models/File.ts: -------------------------------------------------------------------------------- 1 | import type { FileKind } from "./FileKind"; 2 | 3 | export interface File { 4 | path: string; 5 | language: string; 6 | value: string; 7 | kind: FileKind; 8 | } 9 | -------------------------------------------------------------------------------- /src/features/Projects/models/UploadAsset.ts: -------------------------------------------------------------------------------- 1 | import type { AssetKind } from "./AssetKind"; 2 | 3 | export interface UploadAsset { 4 | name: string; 5 | kind: AssetKind; 6 | path: string; 7 | file: File; 8 | } 9 | -------------------------------------------------------------------------------- /.tmp/auth_info.json: -------------------------------------------------------------------------------- 1 | { 2 | "nlConnectToken": "GLsSU6muu9sj3ZDWOCutx1_O_nRnGPt5gNLVJRBbAGrC-FkWH", 3 | "nlPort": 34263, 4 | "nlToken": "BIpqrZcr8WOXV-pR3laGVCT4JEPzMx_GFCcQvL9pNjcOSxc.GLsSU6muu9sj3ZDWOCutx1_O_nRnGPt5gNLVJRBbAGrC-FkWH" 5 | } 6 | -------------------------------------------------------------------------------- /src/components/Assets/AssetsPanel.css: -------------------------------------------------------------------------------- 1 | .dragging-border { 2 | display: block; 3 | outline: 2px oklch(var(--bc) / 50%) dashed; 4 | outline-offset: -4px; 5 | color: white; 6 | border-radius: 4px; 7 | font-size: 15px; 8 | user-select: none; 9 | } -------------------------------------------------------------------------------- /src/util/fileSize.ts: -------------------------------------------------------------------------------- 1 | export const fileSize = (bytes: number) => { 2 | const i = Math.floor(Math.log(bytes) / Math.log(1024)); 3 | const s = ["B", "KB", "MB", "GB", "TB", "PB"][i]; 4 | return `${parseFloat((bytes / Math.pow(1024, i)).toFixed(2))} ${s}`; 5 | }; 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "./node_modules/.bin/astro dev", 6 | "name": "Development server", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/snippets.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "Print to console": { 3 | "scope": "javascript,typescript", 4 | "prefix": "ie", 5 | "body": [ 6 | "import ${1}Example from \"./../../../../kaplay/examples/${1}.js?raw\";", 7 | ], 8 | "description": "Import an example" 9 | } 10 | } -------------------------------------------------------------------------------- /src/config/common.ts: -------------------------------------------------------------------------------- 1 | import packageJson from "../../package.json"; 2 | 3 | export const VERSION = packageJson.version; 4 | export const CHANGELOG = 5 | "https://github.com/lajbel/kaplayground/blob/master/CHANGELOG.md"; 6 | export const REPO = "https://github.com/kaplayjs/kaplayground"; 7 | -------------------------------------------------------------------------------- /src/features/Projects/models/Asset.ts: -------------------------------------------------------------------------------- 1 | import type { AssetKind } from "./AssetKind"; 2 | 3 | export interface Asset { 4 | name: string; 5 | kind: AssetKind; 6 | path: string; 7 | url: string; 8 | // import function for kaplay 9 | importFunction: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from "react-dom/client"; 2 | import { BrowserRouter } from "react-router-dom"; 3 | import { App } from "./App"; 4 | 5 | createRoot(document.getElementById("root")!).render( 6 | 7 | 8 | , 9 | ); 10 | -------------------------------------------------------------------------------- /src/util/stringToValue.ts: -------------------------------------------------------------------------------- 1 | export const stringToValue = (value: string) => { 2 | if (value === "true") return true; 3 | if (value === "false") return false; 4 | if (!isNaN(Number(value)) && (parseFloat(value) % 0 == 0)) { 5 | return Number(value); 6 | } 7 | return value; 8 | }; 9 | -------------------------------------------------------------------------------- /src/util/download.ts: -------------------------------------------------------------------------------- 1 | export const downloadBlob = async (blob: Blob, filename: string) => { 2 | const url = URL.createObjectURL(blob); 3 | const a = document.createElement("a"); 4 | 5 | a.href = url; 6 | a.download = filename; 7 | a.click(); 8 | 9 | URL.revokeObjectURL(url); 10 | }; 11 | -------------------------------------------------------------------------------- /src/util/regex.ts: -------------------------------------------------------------------------------- 1 | // Regex utilities 2 | 3 | export const DATA_URL_REGEX = /data:[^;]+;base64,[A-Za-z0-9+\/]+={0,2}/g; 4 | export const MATCH_ASSET_URL_REGEX = 5 | /load\w+\(\s*["'`][^"'`]*["'`]\s*,\s*["'`]([^"'`]*)["'`][\s\S]*?/g; 6 | export const INSIDE_ADD_ARRAY_REGEX = /(?:\b(?:k\.)?add\s*\(\s*\[)[^\]]*$/; 7 | -------------------------------------------------------------------------------- /src/util/npm.ts: -------------------------------------------------------------------------------- 1 | import type { Packument } from "query-registry"; 2 | 3 | export const getPackageInfo = async (name: string): Promise => { 4 | const endpoint = `https://registry.npmjs.org/${name}`; 5 | const res = await fetch(endpoint); 6 | const data = await res.json(); 7 | return data; 8 | }; 9 | -------------------------------------------------------------------------------- /src/util/openDialog.ts: -------------------------------------------------------------------------------- 1 | export function openDialog(id: string, params?: unknown) { 2 | if (params) { 3 | window.dispatchEvent( 4 | new CustomEvent("dialog-open", { detail: { id, params } }), 5 | ); 6 | } 7 | 8 | document.querySelector(`#${id}`) 9 | ?.showModal(); 10 | } 11 | -------------------------------------------------------------------------------- /src/util/fileToBase64.ts: -------------------------------------------------------------------------------- 1 | export const fileToBase64 = (file: File): Promise => { 2 | return new Promise((resolve) => { 3 | const reader = new FileReader(); 4 | 5 | reader.onload = (e) => { 6 | resolve(e.target?.result as string); 7 | }; 8 | 9 | reader.readAsDataURL(file); 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /src/features/Projects/application/wrapGame.ts: -------------------------------------------------------------------------------- 1 | import { getVersion, parseAssets } from "../../../util/compiler"; 2 | import { buildCode } from "./buildCode"; 3 | 4 | export async function wrapGame() { 5 | const code = await buildCode(); 6 | 7 | return ` 8 | import kaplay from "${getVersion()}"; 9 | ${parseAssets(code)} 10 | `; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/FileTree/FileEntry.css: -------------------------------------------------------------------------------- 1 | 2 | /* Show file actions on hoveronly if kind is not scene nor assets nor kaplay */ 3 | .file[data-file-kind="scene"]:hover .file-actions { 4 | display: block; 5 | } 6 | 7 | .file[data-file-kind="util"]:hover .file-actions { 8 | display: block; 9 | } 10 | 11 | .file[data-file-kind="obj"]:hover .file-actions { 12 | display: block; 13 | } -------------------------------------------------------------------------------- /src/components/Util/ConditionalWrap.tsx: -------------------------------------------------------------------------------- 1 | export type ConditionalWrapProps = { 2 | condition: boolean | undefined; 3 | wrap: (children: React.ReactNode) => React.ReactElement | null; 4 | children: React.ReactNode; 5 | }; 6 | 7 | export const ConditionalWrap = ( 8 | { condition, wrap, children }: ConditionalWrapProps, 9 | ) => condition ? wrap(children) : <>{children}; 10 | -------------------------------------------------------------------------------- /src/features/Projects/application/preferredVersion.ts: -------------------------------------------------------------------------------- 1 | import { useConfig } from "../../../hooks/useConfig"; 2 | import { useEditor } from "../../../hooks/useEditor"; 3 | 4 | export function preferredVersion() { 5 | const versions = useEditor.getState().getRuntime().kaplayVersions; 6 | const preferredVersion = useConfig.getState().config.preferredVersion; 7 | 8 | return versions.find(v => v.startsWith(`${preferredVersion}.`)) 9 | ?? versions[0]; 10 | } 11 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | KAPLAYGROUND, a playground for making JavaScript games 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/features/Editor/application/clearModels.ts: -------------------------------------------------------------------------------- 1 | import { useEditor } from "../../../hooks/useEditor"; 2 | 3 | export const clearModels = () => { 4 | const { monaco, editor } = useEditor.getState().runtime; 5 | 6 | if (!editor || !monaco) { 7 | throw new Error("Tried to use Monaco editor before it was mounted"); 8 | } 9 | 10 | const models = monaco.editor.getModels(); 11 | 12 | for (const model of models) { 13 | model.dispose(); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/util/compressCode.ts: -------------------------------------------------------------------------------- 1 | import pako from "pako"; 2 | 3 | export function compressCode(str: string) { 4 | return btoa(String.fromCharCode.apply(null, Array.from(pako.deflate(str)))); 5 | } 6 | 7 | export function decompressCode(str: string) { 8 | try { 9 | return pako.inflate( 10 | new Uint8Array(atob(str).split("").map((c) => c.charCodeAt(0))), 11 | { to: "string" }, 12 | ); 13 | } catch { 14 | return str; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /sandbox/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react"; 2 | import { defineConfig } from "vite"; 3 | import { viteStaticCopy } from "vite-plugin-static-copy"; 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | viteStaticCopy({ 8 | targets: [ 9 | { 10 | // all except js files 11 | src: "../kaplay/examples/**/!(*.js)", 12 | dest: "", 13 | }, 14 | ], 15 | }), 16 | ], 17 | }); 18 | -------------------------------------------------------------------------------- /src/features/Projects/stores/useProject.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | import { type AssetsSlice, createAssetsSlice } from "./slices/assets"; 3 | import { createFilesSlice, type FilesSlice } from "./slices/files"; 4 | import { createProjectSlice, type ProjectSlice } from "./slices/project.ts"; 5 | export type ProjectStore = ProjectSlice & FilesSlice & AssetsSlice; 6 | 7 | export const useProject = create((...a) => ({ 8 | ...createProjectSlice(...a), 9 | ...createFilesSlice(...a), 10 | ...createAssetsSlice(...a), 11 | })); 12 | -------------------------------------------------------------------------------- /dprint.json: -------------------------------------------------------------------------------- 1 | { 2 | "indentWidth": 4, 3 | "lineWidth": 80, 4 | "typescript": { 5 | }, 6 | "json": { 7 | "indentWidth": 2 8 | }, 9 | "markdown": { 10 | }, 11 | "excludes": [ 12 | "**/node_modules", 13 | "**/*-lock.json" 14 | ], 15 | "markup": { 16 | }, 17 | "plugins": [ 18 | "https://plugins.dprint.dev/typescript-0.90.4.wasm", 19 | "https://plugins.dprint.dev/json-0.19.2.wasm", 20 | "https://plugins.dprint.dev/markdown-0.17.0.wasm", 21 | "https://plugins.dprint.dev/g-plane/markup_fmt-v0.7.0.wasm" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /src/features/Projects/models/Project.ts: -------------------------------------------------------------------------------- 1 | import type { Asset } from "./Asset"; 2 | import type { File } from "./File"; 3 | import type { ProjectBuildMode } from "./ProjectBuildMode"; 4 | import type { ProjectMode } from "./ProjectMode"; 5 | 6 | export type Project = { 7 | name: string; 8 | version: string; 9 | assets: Map; 10 | files: Map; 11 | kaplayVersion: string; 12 | mode: ProjectMode; 13 | buildMode: ProjectBuildMode; 14 | favicon: string; 15 | createdAt: string; 16 | updatedAt: string; 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/Toolbar/ToolButtons/ConfigButton.tsx: -------------------------------------------------------------------------------- 1 | import { assets } from "@kaplayjs/crew"; 2 | import { ToolbarButton } from "../ToolbarButton"; 3 | 4 | export const ConfigButton = () => { 5 | const handleModalOpenClick = () => { 6 | document.querySelector("#config") 7 | ?.showModal(); 8 | }; 9 | 10 | return ( 11 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/components/Toolbar/ToolButtons/AboutButton.tsx: -------------------------------------------------------------------------------- 1 | import { assets } from "@kaplayjs/crew"; 2 | import { ToolbarButton } from "../ToolbarButton"; 3 | 4 | export const AboutButton = () => { 5 | const handleModalOpenClick = () => { 6 | document.querySelector("#about-dialog") 7 | ?.showModal(); 8 | }; 9 | 10 | return ( 11 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import Playground from "./components/Playground/Playground"; 2 | import "react-toastify/dist/ReactToastify.css"; 3 | import "allotment/dist/style.css"; 4 | import "./styles/index.css"; 5 | import "./styles/toast.css"; 6 | import "@fontsource-variable/outfit"; 7 | import "@fontsource/dm-mono"; 8 | import { Route, Routes } from "react-router-dom"; 9 | 10 | export const App = () => { 11 | return ( 12 | 13 | } /> 14 | {/* } /> */} 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": [ 5 | "ES2023" 6 | ], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | /* Linting */ 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noFallthroughCasesInSwitch": true 20 | }, 21 | "include": [ 22 | "vite.config.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | 4 | # generated types 5 | .astro/ 6 | 7 | # dependencies 8 | node_modules/ 9 | 10 | # logs 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | 23 | # jetbrains setting folder 24 | .idea/ 25 | 26 | .wrangler 27 | public/kaboom.js 28 | public/kaboom.js.map 29 | lib.d.ts 30 | 31 | # Neutralino.JS 32 | bin/ 33 | neutralinojs.log 34 | 35 | # Generated Files 36 | src/data/exampleList.json 37 | src/data/kaplayVersions.json 38 | src/data/publicAssets.json -------------------------------------------------------------------------------- /src/components/UI/KDropdown/KDropdownSeparator.tsx: -------------------------------------------------------------------------------- 1 | import { DropdownMenuSeparator } from "@radix-ui/react-dropdown-menu"; 2 | import { type ComponentProps, type FC, forwardRef } from "react"; 3 | 4 | type DropdownSeparatorProps = ComponentProps; 5 | 6 | export const KDropdownMenuSeparator: FC = forwardRef(( 7 | { children, ...props }, 8 | ref, 9 | ) => { 10 | return ( 11 | 16 | ); 17 | }); 18 | -------------------------------------------------------------------------------- /src/util/allotmentStorage.ts: -------------------------------------------------------------------------------- 1 | export type Allotments = "editor" | "brew" | "console"; 2 | 3 | const getAllotmentKey = (prefix: string, id: Allotments): string => { 4 | return `allotment-${prefix}-${id}`; 5 | }; 6 | 7 | export const allotmentStorage = (prefix: string) => ({ 8 | getAllotmentSize: (id: Allotments, initial: number[] = []): number[] => 9 | JSON.parse( 10 | localStorage.getItem(getAllotmentKey(prefix, id)) 11 | ?? JSON.stringify(initial), 12 | ), 13 | setAllotmentSize: (id: Allotments, size: number[]): void => 14 | localStorage.setItem(getAllotmentKey(prefix, id), JSON.stringify(size)), 15 | }); 16 | -------------------------------------------------------------------------------- /src/components/UI/TabsList.tsx: -------------------------------------------------------------------------------- 1 | import * as Tabs from "@radix-ui/react-tabs"; 2 | import type { FC, PropsWithChildren } from "react"; 3 | import { cn } from "../../util/cn"; 4 | 5 | export type TabsListProps = PropsWithChildren< 6 | { 7 | className?: string; 8 | } 9 | >; 10 | 11 | export const TabsList: FC = (props) => { 12 | const { 13 | children, 14 | className, 15 | } = props; 16 | 17 | return ( 18 | 24 | {children} 25 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": [ 6 | "ES2020", 7 | "DOM", 8 | "DOM.Iterable" 9 | ], 10 | "module": "ESNext", 11 | "skipLibCheck": true, 12 | /* Bundler mode */ 13 | "moduleResolution": "bundler", 14 | "allowImportingTsExtensions": true, 15 | "isolatedModules": true, 16 | "moduleDetection": "force", 17 | "noEmit": true, 18 | "jsx": "react-jsx", 19 | /* Linting */ 20 | "strict": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "noFallthroughCasesInSwitch": true, 24 | "allowJs": true 25 | }, 26 | "include": [ 27 | "src" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to KAPLAYGROUND 2 | 3 | We are currently working on `new-editor` branch, please don't make any changes 4 | to `master` branch. 5 | 6 | ## Setup environment 7 | 8 | ``` 9 | git clone https://github.com/kaplayjs/kaplayground.git 10 | cd kaplayground 11 | pnpm i # will install and setup stuff of submodules 12 | pnpm dev # will start the development server 13 | pnpm fmt # before commit 14 | ``` 15 | 16 | ## Merge dev with master workflow 17 | 18 | ``` 19 | git checkout dev 20 | git merge master 21 | git checkout master 22 | git merge --ff-only dev 23 | ``` 24 | 25 | ## Commit messages 26 | 27 | Follow the KAPLAY repo [conventional commits guidelines.](https://github.com/kaplayjs/kaplay/blob/master/CONTRIBUTING.md#conventional-commits-guide) 28 | -------------------------------------------------------------------------------- /src/features/Editor/monaco/fun/createConfetti.ts: -------------------------------------------------------------------------------- 1 | import confetti from "canvas-confetti"; 2 | 3 | export const createConfetti = () => { 4 | // Create canvas 5 | const canvas = document.createElement("canvas") as HTMLCanvasElement & { 6 | confetti: confetti.CreateTypes; 7 | }; 8 | 9 | canvas.style.position = "absolute"; 10 | canvas.style.pointerEvents = "none"; // Prevent interactions 11 | canvas.style.top = "0"; 12 | canvas.style.left = "0"; 13 | canvas.style.width = "100%"; 14 | canvas.style.height = "100%"; 15 | 16 | document.getElementById("monaco-editor-wrapper")!.appendChild(canvas); 17 | 18 | // Confetti thing setup 19 | canvas.confetti = confetti.create(canvas, { resize: true }); 20 | 21 | return canvas; 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/Project/BuildModes/ImportCodeExample.tsx: -------------------------------------------------------------------------------- 1 | export const ImportCodeExample = () => ( 2 |
3 |
import "./kaplay.js";
4 |
import "./assets.js";
5 |
import "./scenes/game.js";
6 |

 7 |         
go("game");
8 |
9 | ); 10 | -------------------------------------------------------------------------------- /src/components/Playground/LoadingPlayground.tsx: -------------------------------------------------------------------------------- 1 | import { type FC } from "react"; 2 | import { cn } from "../../util/cn"; 3 | 4 | type Props = { 5 | isLoading: boolean; 6 | isPortrait: boolean; 7 | isProject: boolean; 8 | }; 9 | 10 | export const LoadingPlayground: FC = (props) => { 11 | return ( 12 |
20 | 21 | 22 | 23 | Launching Playground... 24 | 25 |
26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /src/features/Editor/application/loadFileInModel.ts: -------------------------------------------------------------------------------- 1 | import { useEditor } from "../../../hooks/useEditor"; 2 | import { debug } from "../../../util/logs"; 3 | import type { File } from "../../Projects/models/File"; 4 | 5 | export const loadFileInModel = async (file: File) => { 6 | const runtime = useEditor.getState().runtime; 7 | const editor = runtime.editor; 8 | const monaco = runtime.monaco; 9 | 10 | if (!editor || !monaco) { 11 | throw new Error("Tried to use Monaco editor before it was mounted"); 12 | } 13 | 14 | if (monaco.editor.getModel(monaco.Uri.parse(file.path))) { 15 | return; 16 | } 17 | 18 | monaco.editor.createModel( 19 | file.value, 20 | "javascript", 21 | monaco.Uri.parse(file.path), 22 | ); 23 | 24 | debug(0, "[editor] loaded model", file.path); 25 | }; 26 | -------------------------------------------------------------------------------- /src/features/Projects/application/validateProjectName.ts: -------------------------------------------------------------------------------- 1 | import { useProject } from "../stores/useProject"; 2 | 3 | let usedNames: string[] | null = null; 4 | let lastCheckActiveProject: string | null = null; 5 | 6 | export const validateProjectName = ( 7 | name: string, 8 | key?: string | null, 9 | ): [boolean, string | null] => { 10 | const projectStore = useProject.getState(); 11 | const { projectKey, getSavedProjects, getProjectMetadata } = projectStore; 12 | 13 | key ||= projectKey; 14 | 15 | if (!usedNames || lastCheckActiveProject != key) { 16 | usedNames = getSavedProjects() 17 | .filter(k => k !== key) 18 | .map(k => getProjectMetadata(k).name); 19 | } 20 | 21 | lastCheckActiveProject = key; 22 | const nameAlreadyUsed = name && usedNames?.includes(name); 23 | 24 | return [ 25 | !nameAlreadyUsed, 26 | nameAlreadyUsed ? "Project with that name already exists!" : null, 27 | ]; 28 | }; 29 | -------------------------------------------------------------------------------- /src/hooks/useBeforeUnload.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | 3 | export const useBeforeUnload = async ( 4 | hasUnsavedChanges: boolean, 5 | focusQuerySelector: string = "#project-save-button", 6 | ) => { 7 | const handleBeforeUnload = (e: BeforeUnloadEvent) => { 8 | if (!hasUnsavedChanges) return; 9 | e.preventDefault(); 10 | 11 | if (focusQuerySelector) { 12 | window.addEventListener("focus", () => { 13 | setTimeout(() => 14 | document.querySelector(focusQuerySelector) 15 | ?.focus() 16 | ); 17 | }, { once: true }); 18 | } 19 | }; 20 | 21 | useEffect(() => { 22 | if (hasUnsavedChanges) { 23 | window.addEventListener("beforeunload", handleBeforeUnload); 24 | } 25 | 26 | return () => { 27 | window.removeEventListener("beforeunload", handleBeforeUnload); 28 | }; 29 | }, [hasUnsavedChanges]); 30 | }; 31 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react"; 2 | import path from "path"; 3 | import { defineConfig } from "vite"; 4 | import { viteStaticCopy } from "vite-plugin-static-copy"; 5 | import { generateExamples } from "./scripts/examples.js"; 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | clearScreen: false, 10 | plugins: [ 11 | react(), 12 | viteStaticCopy({ 13 | targets: [ 14 | { 15 | src: "kaplay/examples/**", 16 | dest: "", 17 | }, 18 | ], 19 | }), 20 | { 21 | name: "kaplay", 22 | buildStart() { 23 | const examplesPath = process.env.EXAMPLES_PATH; 24 | 25 | if (examplesPath) { 26 | generateExamples( 27 | path.join(import.meta.dirname, examplesPath), 28 | ); 29 | } else generateExamples(); 30 | }, 31 | }, 32 | ], 33 | }); 34 | -------------------------------------------------------------------------------- /src/util/logs.ts: -------------------------------------------------------------------------------- 1 | // Debug levels represent how verbose the log is 2 | // 0: Normal log 3 | // 1: Internal and long log 4 | // 2: Ultra internal and long log 5 | // 3: Is traced 6 | 7 | import { useConfig } from "../hooks/useConfig"; 8 | 9 | export const debug = (level: number = 0, ...msg: any[]) => { 10 | let debugLevel = useConfig.getState().config.debugLevel; 11 | 12 | if (import.meta.env.DEV) { 13 | debugLevel = 3; 14 | } 15 | 16 | // TODO: Remove null, debugLevel should be a number 17 | if (debugLevel === null || debugLevel < level) return; 18 | 19 | if (level === 0) { 20 | // For info acceptable to know for the user 21 | console.debug(`%c${msg.join(" ")}`, "color: #6694e3"); 22 | } else if (level === 1) { 23 | // For info about internal loading process 24 | console.debug(`%c${msg.join(" ")}`, "color: #e8db2a"); 25 | } else if (level === 2) { 26 | // For info on execution of internal functions 27 | console.trace(`%c${msg.join(" ")}`, "color: #cc3f7a"); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /src/components/Toolbar/ToolbarDropdownButton.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | DropdownMenuItem, 3 | DropdownMenuItemProps, 4 | } from "@radix-ui/react-dropdown-menu"; 5 | import { forwardRef, type PropsWithChildren } from "react"; 6 | import { cn } from "../../util/cn"; 7 | 8 | type ToolbarDropdownButtonProps = PropsWithChildren< 9 | DropdownMenuItemProps & { 10 | type?: "neutral" | "danger"; 11 | } 12 | >; 13 | 14 | export const ToolbarDropdownButton = forwardRef< 15 | HTMLDivElement, 16 | ToolbarDropdownButtonProps 17 | >(({ children, type, ...props }, ref) => { 18 | return ( 19 | 27 | {children} 28 | 29 | ); 30 | }); 31 | -------------------------------------------------------------------------------- /.github/workflows/sync-submodules.yml: -------------------------------------------------------------------------------- 1 | # This Action is dispatched by kaplayjs/kaplay on master push 2 | 3 | name: "Sync Submodules" 4 | 5 | on: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | sync: 10 | permissions: write-all 11 | name: "Sync Submodules" 12 | runs-on: ubuntu-latest 13 | 14 | defaults: 15 | run: 16 | shell: bash 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | with: 22 | submodules: true 23 | - name: Git Submodule Update 24 | run: | 25 | git pull --recurse-submodules 26 | git submodule update --remote --recursive 27 | - name: Commit and Push Changes 28 | env: 29 | BOT_TOKEN: ${{ secrets.BOT_TOKEN }} 30 | run: | 31 | git config --global user.name "Bag Bot" 32 | git config --global user.email "lajbel@kaplayjs.com" 33 | git add . 34 | git commit -m "chore: bump repo" || echo "No changes to commit" 35 | git push https://x-access-token:$BOT_TOKEN@github.com/${{ github.repository }}.git 36 | -------------------------------------------------------------------------------- /src/components/Assets/AssetsTab.tsx: -------------------------------------------------------------------------------- 1 | import * as Tabs from "@radix-ui/react-tabs"; 2 | import type { FC } from "react"; 3 | 4 | interface AssetsTabProps { 5 | label: string; 6 | icon: string; 7 | } 8 | 9 | export const AssetsTab: FC = ({ label, icon }) => { 10 | return ( 11 | 15 |
16 |
17 |
18 | {label} 19 |
20 | 21 | {label} 26 |
27 |
28 |
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/util/confirm.ts: -------------------------------------------------------------------------------- 1 | import { type ReactNode } from "react"; 2 | import { PromptCallback } from "./prompt"; 3 | 4 | export type ConfirmContent = string | ReactNode; 5 | export type ConfirmOptions = { 6 | confirmText?: string; 7 | dismissText?: string; 8 | type?: "danger" | "warning" | "neutral"; 9 | cancelImmediate?: boolean; 10 | }; 11 | export type ConfirmCallback = ( 12 | title: string, 13 | resolve: (value: boolean) => void, 14 | content?: ConfirmContent, 15 | options?: ConfirmOptions, 16 | ) => void; 17 | 18 | export let confirmCallback: ConfirmCallback | PromptCallback; 19 | 20 | export function confirm( 21 | title: string, 22 | content?: ConfirmContent, 23 | options?: ConfirmOptions, 24 | ): Promise { 25 | return new Promise(resolve => { 26 | (confirmCallback as ConfirmCallback)?.( 27 | title, 28 | resolve, 29 | content, 30 | options ?? {}, 31 | ); 32 | }); 33 | } 34 | 35 | export function registerConfirm(cb: ConfirmCallback | PromptCallback): void { 36 | confirmCallback = cb; 37 | } 38 | -------------------------------------------------------------------------------- /src/components/Playground/GameView.tsx: -------------------------------------------------------------------------------- 1 | import { type FC, useEffect } from "react"; 2 | import { useEditor } from "../../hooks/useEditor"; 3 | 4 | export const GameView: FC = () => { 5 | const setRuntime = useEditor((state) => state.setRuntime); 6 | 7 | useEffect(() => { 8 | const iframe = document.getElementById( 9 | "game-view", 10 | ) as HTMLIFrameElement; 11 | 12 | const iframeWindow = iframe.contentWindow?.window; 13 | (window as any).iframeWindow = iframeWindow; 14 | 15 | setRuntime({ iframe: iframe, console: iframeWindow?.console }); 16 | }, []); 17 | 18 | return ( 19 |