406 lines
11 KiB
Markdown
406 lines
11 KiB
Markdown
|
|
# Create GlitchComponent
|
||
|
|
|
||
|
|
Create a new GlitchComponent project that conforms to the standardized plugin architecture for Glitch University.
|
||
|
|
A glitchComponent is a way to create mini-games and functionality packages that can be easily included within Glitch University
|
||
|
|
Goal: Given this skill file, any capable developer should be able to create a GlitchComponent. The js file can be copied from dist into the public folder.
|
||
|
|
Then the component is registered, and from there - integrated within Glitch University.
|
||
|
|
|
||
|
|
Goal is to test Component mini-games outside the scope of the full platform. A glitchComponent is a way to create mini-games and functionality packages that can be easily included within Glitch University.
|
||
|
|
|
||
|
|
The tradeoffs are of course CSS style leakage (good and bad), sound system compatibility etc.
|
||
|
|
|
||
|
|
## Arguments
|
||
|
|
|
||
|
|
- `$ARGUMENTS` - Component name (e.g., "matrix-rain", "particle-system")
|
||
|
|
|
||
|
|
## Instructions
|
||
|
|
|
||
|
|
When creating a new GlitchComponent:
|
||
|
|
|
||
|
|
1. **If no directory specified**, create in a sibling directory: `../glitch-components/$ARGUMENTS/`
|
||
|
|
|
||
|
|
2. **Create the full project structure**:
|
||
|
|
```
|
||
|
|
$ARGUMENTS/
|
||
|
|
├── src/
|
||
|
|
│ ├── index.tsx # Main export with metadata
|
||
|
|
│ ├── Component.tsx # Component implementation
|
||
|
|
│ ├── styles.module.css # Scoped CSS styles
|
||
|
|
│ ├── types.ts # TypeScript interfaces
|
||
|
|
│ └── dev.tsx # Leva dev harness
|
||
|
|
├── vite.config.ts
|
||
|
|
├── package.json
|
||
|
|
├── tsconfig.json
|
||
|
|
└── index.html
|
||
|
|
```
|
||
|
|
|
||
|
|
3. After creation, explain how to:
|
||
|
|
- Run `cd backend & npm install && npm run dev` for local development
|
||
|
|
- Build with `npm run build`
|
||
|
|
- Deploy output to host app
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Architecture Principles
|
||
|
|
|
||
|
|
1. **Build as Library**: Use Vite Library Mode to output a self-contained bundle
|
||
|
|
2. **CSS Isolation**: Use CSS Modules to prevent style bleeding
|
||
|
|
3. **Inversion of Control**: Accept props for all configurable parameters
|
||
|
|
4. **Local Dev Support**: Use Leva controls during development, props in production
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Required TypeScript Interfaces
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// src/types.ts
|
||
|
|
|
||
|
|
export interface GlitchComponentConfig {
|
||
|
|
id: string;
|
||
|
|
name: string;
|
||
|
|
version: string;
|
||
|
|
params: Record<string, unknown>;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface GlitchComponentProps {
|
||
|
|
config: GlitchComponentConfig;
|
||
|
|
onComplete: (result: GlitchComponentResult) => void;
|
||
|
|
onProgress?: (percent: number) => void;
|
||
|
|
theme?: GlitchTheme;
|
||
|
|
className?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface GlitchComponentResult {
|
||
|
|
success: boolean;
|
||
|
|
score?: number;
|
||
|
|
data?: unknown;
|
||
|
|
error?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface GlitchTheme {
|
||
|
|
primary: string; // #6366f1
|
||
|
|
accent: string; // #22d3ee
|
||
|
|
bg: string; // #0a0a0f
|
||
|
|
bgSecondary: string; // #12121a
|
||
|
|
text: string; // #e8e8ec
|
||
|
|
textMuted: string; // #9999a8
|
||
|
|
border: string; // #2a2a3a
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface GlitchComponentMetadata {
|
||
|
|
name: string;
|
||
|
|
displayName: string;
|
||
|
|
version: string;
|
||
|
|
paramSchema: ParamSchema;
|
||
|
|
defaultParams: Record<string, unknown>;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface ParamSchema {
|
||
|
|
[key: string]: {
|
||
|
|
type: 'number' | 'string' | 'boolean' | 'color' | 'select' | 'range';
|
||
|
|
label?: string;
|
||
|
|
description?: string;
|
||
|
|
default: unknown;
|
||
|
|
options?: { value: string | number; label: string }[];
|
||
|
|
min?: number;
|
||
|
|
max?: number;
|
||
|
|
step?: number;
|
||
|
|
};
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Main Export Structure
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// src/index.tsx
|
||
|
|
import Component from './Component';
|
||
|
|
import type { GlitchComponentMetadata } from './types';
|
||
|
|
|
||
|
|
export default Component;
|
||
|
|
|
||
|
|
export const metadata: GlitchComponentMetadata = {
|
||
|
|
name: 'my-component',
|
||
|
|
displayName: 'My Component',
|
||
|
|
version: '1.0.0',
|
||
|
|
paramSchema: {
|
||
|
|
speed: { type: 'range', label: 'Speed', default: 1.0, min: 0.1, max: 5.0, step: 0.1 },
|
||
|
|
primaryColor: { type: 'color', label: 'Color', default: '#6366f1' }
|
||
|
|
},
|
||
|
|
defaultParams: { speed: 1.0, primaryColor: '#6366f1' }
|
||
|
|
};
|
||
|
|
|
||
|
|
export type { GlitchComponentProps, GlitchComponentConfig, GlitchComponentResult } from './types';
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Component Template
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// src/Component.tsx
|
||
|
|
import { useCallback, useEffect, useState } from 'react';
|
||
|
|
import styles from './styles.module.css';
|
||
|
|
import type { GlitchComponentProps } from './types';
|
||
|
|
|
||
|
|
interface ComponentParams {
|
||
|
|
speed: number;
|
||
|
|
primaryColor: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function MyComponent({ config, onComplete, onProgress, theme, className }: GlitchComponentProps) {
|
||
|
|
const params = config.params as ComponentParams;
|
||
|
|
const { speed = 1.0, primaryColor } = params;
|
||
|
|
|
||
|
|
const handleComplete = useCallback((success: boolean, score?: number) => {
|
||
|
|
onComplete({ success, score, data: { completedAt: new Date().toISOString() } });
|
||
|
|
}, [onComplete]);
|
||
|
|
|
||
|
|
const cssVars = {
|
||
|
|
'--gc-primary': primaryColor || theme?.primary || '#6366f1',
|
||
|
|
'--gc-accent': theme?.accent || '#22d3ee',
|
||
|
|
'--gc-bg': theme?.bg || '#0a0a0f',
|
||
|
|
'--gc-text': theme?.text || '#e8e8ec',
|
||
|
|
} as React.CSSProperties;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className={`${styles.container} ${className || ''}`} style={cssVars}>
|
||
|
|
<h2 className={styles.title}>My GlitchComponent</h2>
|
||
|
|
<button className={styles.button} onClick={() => handleComplete(true, 100)}>
|
||
|
|
Complete
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## CSS Module Template
|
||
|
|
|
||
|
|
```css
|
||
|
|
/* src/styles.module.css */
|
||
|
|
.container {
|
||
|
|
background: var(--gc-bg, #0a0a0f);
|
||
|
|
color: var(--gc-text, #e8e8ec);
|
||
|
|
padding: 2rem;
|
||
|
|
border-radius: 12px;
|
||
|
|
box-sizing: border-box;
|
||
|
|
font-family: system-ui, -apple-system, sans-serif;
|
||
|
|
line-height: 1.5;
|
||
|
|
}
|
||
|
|
|
||
|
|
.container *, .container *::before, .container *::after {
|
||
|
|
box-sizing: inherit;
|
||
|
|
}
|
||
|
|
|
||
|
|
.title {
|
||
|
|
color: var(--gc-primary, #6366f1);
|
||
|
|
margin: 0 0 1rem;
|
||
|
|
font-size: 1.5rem;
|
||
|
|
font-weight: 600;
|
||
|
|
}
|
||
|
|
|
||
|
|
.button {
|
||
|
|
background: var(--gc-primary, #6366f1);
|
||
|
|
color: white;
|
||
|
|
border: none;
|
||
|
|
padding: 0.75rem 1.5rem;
|
||
|
|
border-radius: 8px;
|
||
|
|
font-size: 1rem;
|
||
|
|
cursor: pointer;
|
||
|
|
transition: opacity 0.2s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.button:hover { opacity: 0.9; }
|
||
|
|
.button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## CSS variables
|
||
|
|
The following css variables are defined in the master project. Using these in the packaged css
|
||
|
|
should pick up the variables value in the deployed context.
|
||
|
|
For the testing context, please use the the following variables and definitions in a css file
|
||
|
|
|
||
|
|
````css
|
||
|
|
|
||
|
|
--color-bg: #0a0a0f;
|
||
|
|
--color-bg-secondary: #12121a;
|
||
|
|
--color-text: #e8e8ec;
|
||
|
|
--color-text-muted: #9999a8;
|
||
|
|
--color-primary: #6366f1;
|
||
|
|
--color-primary-hover: #818cf8;
|
||
|
|
--color-accent: #22d3ee;
|
||
|
|
--color-accent-secondary: #a855f7;
|
||
|
|
--color-border: #2a2a3a;
|
||
|
|
--font-main: 'Iceland', -apple-system, BlinkMacSystemFont, sans-serif;
|
||
|
|
--font-display: 'Russo One', -apple-system, BlinkMacSystemFont, sans-serif;
|
||
|
|
--font-mono: 'JetBrains Mono', 'Iceland', monospace;
|
||
|
|
--small-font: 1.875rem;
|
||
|
|
--spacing-xs: 0.5rem;
|
||
|
|
--spacing-sm: 1rem;
|
||
|
|
--spacing-md: 2rem;
|
||
|
|
--spacing-lg: 4rem;
|
||
|
|
--spacing-xl: 6rem;
|
||
|
|
--max-width: 1200px;
|
||
|
|
--border-radius: 12px;
|
||
|
|
--border-radius-sm: 8px;
|
||
|
|
|
||
|
|
--transition: 0.2s ease;
|
||
|
|
```
|
||
|
|
|
||
|
|
|
||
|
|
## Sounds
|
||
|
|
If you want to use sounds in the component, note that the app uses Howler,
|
||
|
|
and sound files are defined in the constant like this in the parent project,
|
||
|
|
|
||
|
|
```
|
||
|
|
|
||
|
|
const SOUND_DEFINITIONS = {
|
||
|
|
// ==========================================
|
||
|
|
// UI - General interactions
|
||
|
|
// ==========================================
|
||
|
|
'ui.button_click': { file: 'pop-402323.mp3', folder: 'ui', volume: 0.4 },
|
||
|
|
'ui.button_hover': { file: 'pop-402323.mp3', folder: 'ui', volume: 0.15 },
|
||
|
|
}
|
||
|
|
|
||
|
|
```
|
||
|
|
|
||
|
|
These sounds are called using code like this
|
||
|
|
```
|
||
|
|
playSound('ui.button_click');
|
||
|
|
|
||
|
|
```
|
||
|
|
This for info so that the sound system created for the componet can work with the containing framework.
|
||
|
|
|
||
|
|
`---
|
||
|
|
## Vite Configuration
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// vite.config.ts
|
||
|
|
import { defineConfig } from 'vite';
|
||
|
|
import react from '@vitejs/plugin-react';
|
||
|
|
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js';
|
||
|
|
import { resolve } from 'path';
|
||
|
|
|
||
|
|
export default defineConfig(({ mode }) => ({
|
||
|
|
plugins: [react(), cssInjectedByJsPlugin()],
|
||
|
|
build: {
|
||
|
|
lib: {
|
||
|
|
entry: resolve(__dirname, 'src/index.tsx'),
|
||
|
|
name: 'MyGlitchComponent',
|
||
|
|
fileName: 'my-glitch-component',
|
||
|
|
formats: ['es']
|
||
|
|
},
|
||
|
|
rollupOptions: {
|
||
|
|
external: ['react', 'react-dom', 'react/jsx-runtime'],
|
||
|
|
output: {
|
||
|
|
globals: { react: 'React', 'react-dom': 'ReactDOM', 'react/jsx-runtime': 'jsxRuntime' },
|
||
|
|
assetFileNames: 'assets/[name][extname]'
|
||
|
|
}
|
||
|
|
},
|
||
|
|
sourcemap: true,
|
||
|
|
minify: mode === 'production'
|
||
|
|
},
|
||
|
|
server: { port: 3001, open: true }
|
||
|
|
}));
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Package.json
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"name": "my-glitch-component",
|
||
|
|
"version": "1.0.0",
|
||
|
|
"type": "module",
|
||
|
|
"main": "./dist/my-glitch-component.js",
|
||
|
|
"module": "./dist/my-glitch-component.js",
|
||
|
|
"scripts": {
|
||
|
|
"dev": "vite",
|
||
|
|
"build": "tsc && vite build",
|
||
|
|
"preview": "vite preview"
|
||
|
|
},
|
||
|
|
"peerDependencies": {
|
||
|
|
"react": "^18.0.0",
|
||
|
|
"react-dom": "^18.0.0"
|
||
|
|
},
|
||
|
|
"devDependencies": {
|
||
|
|
"@types/react": "^18.2.0",
|
||
|
|
"@types/react-dom": "^18.2.0",
|
||
|
|
"@vitejs/plugin-react": "^4.2.0",
|
||
|
|
"leva": "^0.9.35",
|
||
|
|
"typescript": "^5.3.0",
|
||
|
|
"vite": "^5.0.0",
|
||
|
|
"vite-plugin-css-injected-by-js": "^3.3.0"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Dev Harness with Leva
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// src/dev.tsx
|
||
|
|
import React from 'react';
|
||
|
|
import ReactDOM from 'react-dom/client';
|
||
|
|
import { useControls, Leva } from 'leva';
|
||
|
|
import Component from './Component';
|
||
|
|
import { metadata } from './index';
|
||
|
|
|
||
|
|
function DevHarness() {
|
||
|
|
const levaSchema = Object.entries(metadata.paramSchema).reduce((acc, [key, schema]) => {
|
||
|
|
if (schema.type === 'range' || schema.type === 'number') {
|
||
|
|
acc[key] = { value: schema.default, min: schema.min, max: schema.max, step: schema.step };
|
||
|
|
} else if (schema.type === 'select') {
|
||
|
|
acc[key] = { value: schema.default, options: schema.options?.reduce((o, opt) => ({ ...o, [opt.label]: opt.value }), {}) };
|
||
|
|
} else {
|
||
|
|
acc[key] = schema.default;
|
||
|
|
}
|
||
|
|
return acc;
|
||
|
|
}, {} as Record<string, unknown>);
|
||
|
|
|
||
|
|
const params = useControls(levaSchema);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div style={{ minHeight: '100vh', background: '#0a0a0f', padding: '2rem' }}>
|
||
|
|
<Leva collapsed={false} />
|
||
|
|
<Component
|
||
|
|
config={{ id: 'dev', name: metadata.name, version: metadata.version, params }}
|
||
|
|
theme={{ primary: '#6366f1', accent: '#22d3ee', bg: '#0a0a0f', bgSecondary: '#12121a', text: '#e8e8ec', textMuted: '#9999a8', border: '#2a2a3a' }}
|
||
|
|
onComplete={(r) => { console.log('Complete:', r); alert(`Done! Score: ${r.score}`); }}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
ReactDOM.createRoot(document.getElementById('root')!).render(<React.StrictMode><DevHarness /></React.StrictMode>);
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Three.js / R3F Components
|
||
|
|
|
||
|
|
For 3D components, add to externals in vite.config.ts:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
external: ['react', 'react-dom', 'react/jsx-runtime', 'three', '@react-three/fiber', '@react-three/drei']
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Deployment
|
||
|
|
0. `nvm use 20` # to select node version 20
|
||
|
|
1. `npm run build`
|
||
|
|
2. Copy `dist/*.js` to host app's `src/components/glitch/`
|
||
|
|
3. Add database entry:
|
||
|
|
```sql
|
||
|
|
INSERT INTO glitch_components (name, display_name, version, file_path, param_schema, default_params)
|
||
|
|
VALUES ('my-component', 'My Component', '1.0.0', 'my-component.js', '{"speed":{"type":"range","default":1.0}}', '{"speed":1.0}');
|
||
|
|
```
|
||
|
|
4. Link to tech via `glitch_component_id`
|