New storywriter started
This commit is contained in:
parent
f5406130df
commit
6966352d4b
|
|
@ -0,0 +1,366 @@
|
||||||
|
# AI Agent Instructions for TS-Games
|
||||||
|
|
||||||
|
This document provides guidelines for AI agents working with the TS-Games project.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
TS-Games is a custom framework/build system for creating simple single-file TypeScript games. All games compile into one standalone HTML file with no external dependencies—all assets are inlined as data URLs.
|
||||||
|
|
||||||
|
## Key Concepts
|
||||||
|
|
||||||
|
### Two Types of Applications
|
||||||
|
|
||||||
|
1. **Games** - Interactive applications with game loops
|
||||||
|
- Located in `src/games/<name>/`
|
||||||
|
- Entry point: `index.ts` or `index.tsx`
|
||||||
|
- Default export: `runGame()` function
|
||||||
|
- Use `gameLoop()` from `@common/game` for structured game development
|
||||||
|
|
||||||
|
2. **Apps** - More complex applications (like AI-Story)
|
||||||
|
- Also located in `src/games/<name>/`
|
||||||
|
- Entry point: `index.tsx`
|
||||||
|
- Default export: `main()` function
|
||||||
|
- May use Preact components and complex state management
|
||||||
|
|
||||||
|
### Single HTML File Architecture
|
||||||
|
|
||||||
|
- All games build into a single `.html` file in `dist/`
|
||||||
|
- No external dependencies at runtime
|
||||||
|
- All assets (images, audio, fonts, WASM) are inlined as base64 data URLs
|
||||||
|
- Build process uses Bun's plugin system to transform assets
|
||||||
|
|
||||||
|
## Creating a New Game
|
||||||
|
|
||||||
|
### Step 1: Create Directory Structure
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p src/games/<yourgame>/assets
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Create Entry Point
|
||||||
|
|
||||||
|
Create `src/games/<yourgame>/index.ts` or `index.tsx`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Simple game example (index.ts)
|
||||||
|
import { gameLoop } from "@common/game";
|
||||||
|
import Input from "@common/input";
|
||||||
|
|
||||||
|
const setup = () => {
|
||||||
|
// Initialize game state
|
||||||
|
return { x: 0, y: 0 };
|
||||||
|
};
|
||||||
|
|
||||||
|
const frame = (dt: number, state: ReturnType<typeof setup>) => {
|
||||||
|
// Update game state
|
||||||
|
state.x += Input.getHorizontal() * dt;
|
||||||
|
state.y += Input.getVertical() * dt;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default gameLoop(setup, frame);
|
||||||
|
```
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Preact component example (index.tsx)
|
||||||
|
import { render } from "preact";
|
||||||
|
import App from "./components/app";
|
||||||
|
|
||||||
|
export default function main() {
|
||||||
|
render(<App />, document.body);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Add Assets (Optional)
|
||||||
|
|
||||||
|
Place assets in `src/games/<yourgame>/assets/`:
|
||||||
|
|
||||||
|
- **Images**: `.png`, `.jpg`, `.jpeg` → Import as `HTMLImageElement`
|
||||||
|
- **Audio**: `.wav`, `.mp3`, `.ogg` → Import as `HTMLAudioElement`
|
||||||
|
- **CSS**: `.css`, `.module.css` → Import as styles
|
||||||
|
- **Fonts**: `.font.css` → Import font faces
|
||||||
|
- **WASM**: `.c`, `.cpp`, `.wasm` → Import as WASM module
|
||||||
|
- **Icons**: `favicon.ico` → Auto-used as page icon
|
||||||
|
- **PWA**: `pwa_icon.png` → Used for PWA manifest
|
||||||
|
|
||||||
|
### Step 4: Import Assets
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Import image
|
||||||
|
import spritesheet from './assets/spritesheet.png';
|
||||||
|
console.log(spritesheet); // <img src="data:image/png;base64,..." />
|
||||||
|
|
||||||
|
// Import audio
|
||||||
|
import heal from './assets/heal.ogg';
|
||||||
|
heal.play();
|
||||||
|
|
||||||
|
// Import CSS
|
||||||
|
import './assets/styles.css';
|
||||||
|
|
||||||
|
// Import CSS modules
|
||||||
|
import styles from './assets/styles.module.css';
|
||||||
|
<div className={styles.root} />
|
||||||
|
|
||||||
|
// Import WASM (C/C++)
|
||||||
|
import wasmModule from './assets/game.c';
|
||||||
|
// Access exports: wasmModule.memory, wasmModule.functionName()
|
||||||
|
|
||||||
|
// Import GLSL shader
|
||||||
|
import shader from './assets/shader.glsl';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
### Running Development Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun start
|
||||||
|
```
|
||||||
|
|
||||||
|
Navigate to `http://localhost:3000` to see the game list. Games rebuild on each page reload.
|
||||||
|
|
||||||
|
To run a specific game:
|
||||||
|
```bash
|
||||||
|
bun start --game=<yourgame>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Building for Production
|
||||||
|
|
||||||
|
Build a specific game:
|
||||||
|
```bash
|
||||||
|
bun run build <yourgame>
|
||||||
|
```
|
||||||
|
|
||||||
|
Or select from interactive list:
|
||||||
|
```bash
|
||||||
|
bun run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Output: `dist/<yourgame>.html`
|
||||||
|
|
||||||
|
### Building with Options
|
||||||
|
|
||||||
|
- `--local`: Build for local testing (no PWA, different index)
|
||||||
|
- `--production`: Minify JS/CSS/HTML (default for build)
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
tsgames/
|
||||||
|
├── src/
|
||||||
|
│ ├── games/ # All games/apps live here
|
||||||
|
│ │ ├── <game1>/ # Individual game directory
|
||||||
|
│ │ │ ├── index.ts # Entry point (required)
|
||||||
|
│ │ │ └── assets/ # Game assets
|
||||||
|
│ │ └── index/ # Special: game list page
|
||||||
|
│ ├── common/ # Shared utilities
|
||||||
|
│ │ ├── game.ts # Game loop helper
|
||||||
|
│ │ ├── input.ts # Input handling
|
||||||
|
│ │ ├── dom.ts # DOM utilities
|
||||||
|
│ │ ├── utils.ts # General utilities
|
||||||
|
│ │ ├── errors.ts # Error formatting
|
||||||
|
│ │ ├── components/ # Shared Preact components
|
||||||
|
│ │ ├── hooks/ # React-like hooks
|
||||||
|
│ │ ├── display/ # Canvas/graphics utilities
|
||||||
|
│ │ └── physics/ # Physics utilities
|
||||||
|
│ └── types.d.ts # Global type definitions
|
||||||
|
├── build/
|
||||||
|
│ ├── build.ts # Build script
|
||||||
|
│ ├── server.ts # Dev server
|
||||||
|
│ ├── html.ts # HTML generation
|
||||||
|
│ ├── isGame.ts # Game detection
|
||||||
|
│ ├── imagePlugin.ts # Image bundler plugin
|
||||||
|
│ ├── audioPlugin.ts # Audio bundler plugin
|
||||||
|
│ ├── fontPlugin.ts # Font bundler plugin
|
||||||
|
│ ├── wasmPlugin.ts # WASM bundler plugin
|
||||||
|
│ └── filePlugin.ts # Generic file plugin
|
||||||
|
├── dist/ # Built HTML files
|
||||||
|
└── test/ # Tests
|
||||||
|
```
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
### Bun Configuration
|
||||||
|
|
||||||
|
- Use `bun` for all package management and script running
|
||||||
|
- `.env` files are automatically loaded (no dotenv needed)
|
||||||
|
- Use `Bun.serve()` for servers (not express)
|
||||||
|
- Use `Bun.file()` for file operations (not fs)
|
||||||
|
- Use `Bun.$` for shell commands (not execa)
|
||||||
|
|
||||||
|
### TypeScript Configuration
|
||||||
|
|
||||||
|
- JSX: `react-jsx` with Preact
|
||||||
|
- Module resolution: `bundler`
|
||||||
|
- Path aliases: `@common/*` → `./src/common/*`
|
||||||
|
- Strict mode enabled
|
||||||
|
|
||||||
|
### Asset Processing
|
||||||
|
|
||||||
|
All asset processing happens at build time via Bun plugins:
|
||||||
|
|
||||||
|
1. **Images** (`.png`, `.jpg`, `.jpeg`):
|
||||||
|
- Converted to base64 data URLs
|
||||||
|
- Return `HTMLImageElement` that's ready to use
|
||||||
|
|
||||||
|
2. **Audio** (`.wav`, `.mp3`, `.ogg`):
|
||||||
|
- Converted to base64 data URLs
|
||||||
|
- Return `HTMLAudioElement` with loaded source
|
||||||
|
|
||||||
|
3. **CSS**:
|
||||||
|
- Bundled and inlined in `<style>` tag
|
||||||
|
- CSS modules supported (`.module.css`)
|
||||||
|
- Modern CSS features transpiled (nesting, vendor prefixes)
|
||||||
|
|
||||||
|
4. **Fonts**:
|
||||||
|
- Import via `.font.css` files
|
||||||
|
- Base64 inlined
|
||||||
|
|
||||||
|
5. **WASM**:
|
||||||
|
- `.c`/`.cpp`: Compiled to WASM at build time (requires clang)
|
||||||
|
- `.wasm`: Direct inclusion
|
||||||
|
- All inlined as base64 data URLs
|
||||||
|
|
||||||
|
### WASM Development
|
||||||
|
|
||||||
|
For C/C++ files:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// Example: src/games/life/life.c
|
||||||
|
EXPORT(init) void init();
|
||||||
|
EXPORT(step) void step();
|
||||||
|
EXPORT(getCell) int getCell(int x, int y);
|
||||||
|
|
||||||
|
// Must export memory
|
||||||
|
```
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- Install: `sudo apt install clang lld wabt`
|
||||||
|
- Only function exports and `memory` supported
|
||||||
|
|
||||||
|
### PWA Support
|
||||||
|
|
||||||
|
Production builds include PWA manifest with:
|
||||||
|
- App name from game directory name (capitalized)
|
||||||
|
- Icon from `favicon.ico` or `pwa_icon.png`
|
||||||
|
- Fullscreen display mode
|
||||||
|
|
||||||
|
### Publishing
|
||||||
|
|
||||||
|
Configure `.env`:
|
||||||
|
```bash
|
||||||
|
PUBLISH_LOCATION=ssh.example.com:/var/www/games/
|
||||||
|
PUBLISH_URL=https://example.com/
|
||||||
|
```
|
||||||
|
|
||||||
|
Build and publish:
|
||||||
|
```bash
|
||||||
|
bun run build <yourgame>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Game Loop Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { gameLoop } from "@common/game";
|
||||||
|
import Input from "@common/input";
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
playerX: number;
|
||||||
|
playerY: number;
|
||||||
|
score: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const setup = (): State => {
|
||||||
|
return {
|
||||||
|
playerX: 100,
|
||||||
|
playerY: 100,
|
||||||
|
score: 0,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const frame = (dt: number, state: State): State => {
|
||||||
|
// Update based on input
|
||||||
|
state.playerX += Input.getHorizontal() * 200 * dt;
|
||||||
|
state.playerY += Input.getVertical() * 200 * dt;
|
||||||
|
|
||||||
|
// Update score
|
||||||
|
state.score += dt;
|
||||||
|
|
||||||
|
// Return new state (optional)
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default gameLoop(setup, frame);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Preact Component Pattern
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { useState, useEffect } from "preact/hooks";
|
||||||
|
import styles from './app.module.css';
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
const [count, setCount] = useState(0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<h1>Count: {count}</h1>
|
||||||
|
<button onClick={() => setCount(c => c + 1)}>
|
||||||
|
Increment
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Run tests:
|
||||||
|
```bash
|
||||||
|
bun test
|
||||||
|
```
|
||||||
|
|
||||||
|
Tests use Bun's built-in test framework:
|
||||||
|
```typescript
|
||||||
|
import { test, expect } from "bun:test";
|
||||||
|
|
||||||
|
test("example", () => {
|
||||||
|
expect(1 + 1).toBe(2);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
1. **No external runtime dependencies** - Everything must be bundled
|
||||||
|
2. **Single entry point** - Each game has one default export
|
||||||
|
3. **Game name from directory** - Directory name becomes game name and URL path
|
||||||
|
4. **Hot reload** - Dev server rebuilds on each page reload
|
||||||
|
5. **Mobile support** - Use `?mobile=true` to enable Eruda debugger
|
||||||
|
6. **Production debugging** - Use `?production=true` to test production builds in dev
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Study existing games for patterns:
|
||||||
|
- **Simple**: `playground/` - Basic game loop example
|
||||||
|
- **Canvas**: `binario/` - Canvas-based puzzle game
|
||||||
|
- **Preact**: `ai-story/` - Complex app with components and state
|
||||||
|
- **WASM**: `life/` - C code compiled to WASM
|
||||||
|
- **Text-based**: `text-dungeon/` - Text adventure game
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Build fails with "No entry point found"
|
||||||
|
- Ensure your game has `index.ts` or `index.tsx` with default export
|
||||||
|
|
||||||
|
### Assets not loading
|
||||||
|
- Check file extensions are supported
|
||||||
|
- Verify assets are in `src/games/<name>/assets/`
|
||||||
|
|
||||||
|
### WASM compilation fails
|
||||||
|
- Install clang and WASM toolchain
|
||||||
|
- Check C/C++ code compiles with `-Werror`
|
||||||
|
|
||||||
|
### TypeScript errors
|
||||||
|
- Run `bunx tsc --noEmit --skipLibCheck` to check types
|
||||||
|
- Use `@common/*` path alias for common imports
|
||||||
15
README.md
15
README.md
|
|
@ -92,21 +92,6 @@ bun run build
|
||||||
import "./assets/lcd.font.css";
|
import "./assets/lcd.font.css";
|
||||||
```
|
```
|
||||||
|
|
||||||
- [AssemblyScript](https://www.assemblyscript.org/) support (TypeScript-like language compiled into WebAssembly)
|
|
||||||
- Example: `src/games/playground/awoo.wasm.ts`
|
|
||||||
- Triggered by file name `*.wasm.ts`
|
|
||||||
|
|
||||||
- Import `*.c`/`*.cpp` files (compile to wasm on the fly)
|
|
||||||
- Example: `src/games/life/life.c`
|
|
||||||
- To use, `clang` and wasm toochain should be present in the system
|
|
||||||
```bash
|
|
||||||
sudo apt install clang lld wabt
|
|
||||||
```
|
|
||||||
- Supports only function exports & `memory`
|
|
||||||
- `EXPORT(jsName) void c_function();`
|
|
||||||
- No stdlib
|
|
||||||
|
|
||||||
|
|
||||||
## Publishing
|
## Publishing
|
||||||
|
|
||||||
- Make sure you have `scp` installed (it most certainly is)
|
- Make sure you have `scp` installed (it most certainly is)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
.root {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 100dvw;
|
||||||
|
height: 100dvh;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
.editor {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
resize: none;
|
||||||
|
padding: 48px 72px;
|
||||||
|
font-family: 'Georgia', serif;
|
||||||
|
font-size: 17px;
|
||||||
|
line-height: 1.9;
|
||||||
|
color: var(--text-dim);
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::selection {
|
||||||
|
background: var(--bg-active);
|
||||||
|
color: var(--yellow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
.sidebar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
transition: width 0.2s ease, min-width 0.2s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar:last-child {
|
||||||
|
border-right: none;
|
||||||
|
border-left: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.open {
|
||||||
|
width: 260px;
|
||||||
|
min-width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closed {
|
||||||
|
width: 32px;
|
||||||
|
min-width: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-self: flex-end;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
:root {
|
||||||
|
/* Monokai-inspired palette */
|
||||||
|
--bg: #272822;
|
||||||
|
--bg-panel: #1e1f1a;
|
||||||
|
--bg-hover: #3e3d32;
|
||||||
|
--bg-active: #49483e;
|
||||||
|
--border: #3e3d32;
|
||||||
|
--accent: #f92672;
|
||||||
|
--accent-alt: #a6e22e;
|
||||||
|
--text: #f8f8f2;
|
||||||
|
--text-muted: #75715e;
|
||||||
|
--text-dim: #cfcfc2;
|
||||||
|
--yellow: #e6db74;
|
||||||
|
--orange: #fd971f;
|
||||||
|
--blue: #66d9ef;
|
||||||
|
--purple: #ae81ff;
|
||||||
|
|
||||||
|
--radius: 4px;
|
||||||
|
--transition: 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--bg-active) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: 'Georgia', serif;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.6;
|
||||||
|
width: 100dvw;
|
||||||
|
height: 100dvh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: inherit;
|
||||||
|
transition: color var(--transition), background var(--transition);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--text);
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { Sidebar } from "./sidebar/sidebar";
|
||||||
|
import { Editor } from "./editor/editor";
|
||||||
|
import styles from '../assets/app.module.css';
|
||||||
|
|
||||||
|
export const App = () => {
|
||||||
|
return (
|
||||||
|
<div class={styles.root}>
|
||||||
|
<Sidebar side="left" />
|
||||||
|
<Editor />
|
||||||
|
<Sidebar side="right" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import styles from '../../assets/editor.module.css';
|
||||||
|
|
||||||
|
export const Editor = () => {
|
||||||
|
return (
|
||||||
|
<div class={styles.editor}>
|
||||||
|
<textarea class={styles.textarea} placeholder="Start writing..." />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { useBool } from "@common/hooks/useBool";
|
||||||
|
import styles from '../../assets/sidebar.module.css';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
side: 'left' | 'right';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Sidebar = ({ side }: Props) => {
|
||||||
|
const open = useBool(true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={clsx(styles.sidebar, open.value ? styles.open : styles.closed)}>
|
||||||
|
<button class={styles.toggle} onClick={open.toggle}>
|
||||||
|
{side === 'left' ? (open.value ? '◀' : '▶') : (open.value ? '▶' : '◀')}
|
||||||
|
</button>
|
||||||
|
{open.value && (
|
||||||
|
<div class={styles.content}>
|
||||||
|
{/* TODO */}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { createContext } from "preact";
|
||||||
|
import { useContext, useMemo, useReducer } from "preact/hooks";
|
||||||
|
|
||||||
|
// ─── State ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
// TODO: define state shape
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Actions ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type Action =
|
||||||
|
| { type: 'TODO' };
|
||||||
|
|
||||||
|
// ─── Initial State ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const DEFAULT_STATE: IState = {
|
||||||
|
// TODO
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Reducer ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function reducer(state: IState, _action: Action): IState {
|
||||||
|
// TODO: implement action handlers
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Context ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface IStateContext {
|
||||||
|
state: IState;
|
||||||
|
dispatch: (action: Action) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StateContext = createContext<IStateContext>({} as IStateContext);
|
||||||
|
|
||||||
|
export const useAppState = () => useContext(StateContext);
|
||||||
|
|
||||||
|
// ─── Provider ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const StateContextProvider = ({ children }: { children?: any }) => {
|
||||||
|
const [state, dispatch] = useReducer(reducer, DEFAULT_STATE);
|
||||||
|
|
||||||
|
const value = useMemo<IStateContext>(() => ({ state, dispatch }), [state]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StateContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</StateContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { render } from "preact";
|
||||||
|
import { StateContextProvider } from "./contexts/state";
|
||||||
|
import { App } from "./components/app";
|
||||||
|
|
||||||
|
import './assets/style.css';
|
||||||
|
|
||||||
|
export default function main() {
|
||||||
|
render(
|
||||||
|
<StateContextProvider>
|
||||||
|
<App />
|
||||||
|
</StateContextProvider>,
|
||||||
|
document.body
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue