Adding the first glitch gallery
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
node ./scripts/glitch-components.mjs build "$@"
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
node ./scripts/glitch-components.mjs release "$@"
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Please provide name of glitch component to run."
|
||||
echo "Usage: ./dev.sh <component-name>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
COMPONENT="$1"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
COMPONENT_DIR="$SCRIPT_DIR/$COMPONENT"
|
||||
|
||||
if [ ! -d "$COMPONENT_DIR" ]; then
|
||||
echo "Component '$COMPONENT' not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$COMPONENT_DIR/package.json" ]; then
|
||||
echo "Component '$COMPONENT' has no package.json."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Starting dev server for $COMPONENT..."
|
||||
cd "$COMPONENT_DIR"
|
||||
npm run dev
|
||||
Submodule
+1
Submodule onboarding2 added at 705e9e508b
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@glitch-university/glitch-components-workspace",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"glitch:list": "node scripts/glitch-components.mjs list",
|
||||
"glitch:validate": "node scripts/glitch-components.mjs validate",
|
||||
"glitch:build": "node scripts/glitch-components.mjs build",
|
||||
"glitch:release": "node scripts/glitch-components.mjs release",
|
||||
"glitch:new": "node scripts/new-glitch-component.mjs"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
|
||||
const cwd = process.cwd()
|
||||
|
||||
function fail(message, exitCode = 1) {
|
||||
console.error(message)
|
||||
process.exit(exitCode)
|
||||
}
|
||||
|
||||
function discoverComponents() {
|
||||
return fs
|
||||
.readdirSync(cwd, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory() && entry.name.startsWith('glitch_'))
|
||||
.map((entry) => entry.name)
|
||||
.filter((name) => fs.existsSync(path.join(cwd, name, 'package.json')))
|
||||
.sort()
|
||||
}
|
||||
|
||||
function readJson(filePath) {
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const [command = 'list', ...rest] = argv
|
||||
const options = {
|
||||
command,
|
||||
components: [],
|
||||
hostDir: '',
|
||||
stagingDir: path.join(cwd, '.glitch-release')
|
||||
}
|
||||
|
||||
for (let index = 0; index < rest.length; index += 1) {
|
||||
const arg = rest[index]
|
||||
if (arg === '--host') {
|
||||
options.hostDir = path.resolve(cwd, rest[index + 1] ?? '')
|
||||
index += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (arg === '--staging') {
|
||||
options.stagingDir = path.resolve(cwd, rest[index + 1] ?? '')
|
||||
index += 1
|
||||
continue
|
||||
}
|
||||
|
||||
options.components.push(arg)
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
function resolveComponents(requested) {
|
||||
const discovered = discoverComponents()
|
||||
if (requested.length === 0) return discovered
|
||||
|
||||
const missing = requested.filter((name) => !discovered.includes(name))
|
||||
if (missing.length > 0) {
|
||||
fail(`Unknown components: ${missing.join(', ')}`)
|
||||
}
|
||||
|
||||
return requested
|
||||
}
|
||||
|
||||
function loadComponent(componentDirName) {
|
||||
const componentDir = path.join(cwd, componentDirName)
|
||||
const packagePath = path.join(componentDir, 'package.json')
|
||||
const manifestPath = path.join(componentDir, 'glitch.manifest.json')
|
||||
const packageJson = readJson(packagePath)
|
||||
const manifest = fs.existsSync(manifestPath) ? readJson(manifestPath) : null
|
||||
|
||||
return {
|
||||
componentDirName,
|
||||
componentDir,
|
||||
packagePath,
|
||||
manifestPath,
|
||||
packageJson,
|
||||
manifest
|
||||
}
|
||||
}
|
||||
|
||||
function validateComponent(component) {
|
||||
const requiredFiles = ['package.json', 'src/index.tsx', 'src/Component.tsx', 'src/types.ts', 'src/dev.tsx']
|
||||
|
||||
const errors = []
|
||||
const warnings = []
|
||||
const hasManifest = Boolean(component.manifest)
|
||||
const hasStandardSignals = hasManifest
|
||||
|
||||
for (const relativePath of requiredFiles) {
|
||||
if (!fs.existsSync(path.join(component.componentDir, relativePath))) {
|
||||
if (hasStandardSignals) {
|
||||
errors.push(`Missing required file: ${relativePath}`)
|
||||
} else {
|
||||
warnings.push(`Legacy structure is missing standard file: ${relativePath}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const hasViteConfig =
|
||||
fs.existsSync(path.join(component.componentDir, 'vite.config.ts')) ||
|
||||
fs.existsSync(path.join(component.componentDir, 'vite.config.mjs')) ||
|
||||
fs.existsSync(path.join(component.componentDir, 'vite.config.js'))
|
||||
|
||||
if (!hasViteConfig) {
|
||||
if (hasStandardSignals) {
|
||||
errors.push('Missing required file: vite.config.ts')
|
||||
} else {
|
||||
warnings.push('Legacy structure is missing standard file: vite.config.ts')
|
||||
}
|
||||
}
|
||||
|
||||
if (!component.packageJson.scripts?.dev && hasStandardSignals) {
|
||||
errors.push('Missing package script: dev')
|
||||
}
|
||||
|
||||
if (!component.packageJson.scripts?.build && hasStandardSignals) {
|
||||
errors.push('Missing package script: build')
|
||||
}
|
||||
|
||||
const indexPath = path.join(component.componentDir, 'src', 'index.tsx')
|
||||
if (fs.existsSync(indexPath)) {
|
||||
const source = fs.readFileSync(indexPath, 'utf8')
|
||||
if (!source.includes('export default') && hasStandardSignals) {
|
||||
errors.push('src/index.tsx must export default')
|
||||
}
|
||||
if (!source.includes('export const metadata') && hasStandardSignals) {
|
||||
errors.push('src/index.tsx must export metadata')
|
||||
}
|
||||
if (source.includes("import './dev.css'") || source.includes("import './dev-theme.css'")) {
|
||||
warnings.push('Production entry imports a dev-only stylesheet')
|
||||
}
|
||||
}
|
||||
|
||||
const styleFiles = ['src/styles.css', 'src/styles.module.css'].filter((relativePath) =>
|
||||
fs.existsSync(path.join(component.componentDir, relativePath))
|
||||
)
|
||||
|
||||
for (const relativePath of styleFiles) {
|
||||
const source = fs.readFileSync(path.join(component.componentDir, relativePath), 'utf8')
|
||||
if (source.includes('@import url(')) {
|
||||
warnings.push(`${relativePath} imports a remote stylesheet`)
|
||||
}
|
||||
}
|
||||
|
||||
if (!component.manifest) {
|
||||
warnings.push('Missing glitch.manifest.json')
|
||||
} else {
|
||||
for (const key of ['componentId', 'displayName', 'version', 'folderName', 'packageName', 'entry']) {
|
||||
if (!component.manifest[key]) {
|
||||
errors.push(`glitch.manifest.json is missing "${key}"`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!fs.existsSync(path.join(component.componentDir, 'README.md'))) {
|
||||
warnings.push('Missing README.md')
|
||||
}
|
||||
|
||||
return { errors, warnings }
|
||||
}
|
||||
|
||||
function runBuild(component) {
|
||||
const result = spawnSync('npm', ['run', 'build'], {
|
||||
cwd: component.componentDir,
|
||||
stdio: 'inherit'
|
||||
})
|
||||
|
||||
if (result.status !== 0) {
|
||||
fail(`Build failed for ${component.componentDirName}`, result.status ?? 1)
|
||||
}
|
||||
}
|
||||
|
||||
function ensureDirectory(directory) {
|
||||
fs.mkdirSync(directory, { recursive: true })
|
||||
}
|
||||
|
||||
function copyDirectoryContents(sourceDir, destinationDir) {
|
||||
ensureDirectory(destinationDir)
|
||||
for (const entry of fs.readdirSync(sourceDir)) {
|
||||
fs.cpSync(path.join(sourceDir, entry), path.join(destinationDir, entry), {
|
||||
force: true,
|
||||
recursive: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function releaseComponent(component, hostDir, stagingDir) {
|
||||
const distDir = path.join(component.componentDir, 'dist')
|
||||
if (!fs.existsSync(distDir)) {
|
||||
fail(`Missing dist directory for ${component.componentDirName}. Run build first.`)
|
||||
}
|
||||
|
||||
const manifest =
|
||||
component.manifest ??
|
||||
{
|
||||
componentId: component.componentDirName.replace(/^glitch_/, '').replaceAll('_', '-'),
|
||||
displayName: component.packageJson.description ?? component.packageJson.name,
|
||||
version: component.packageJson.version,
|
||||
folderName: component.componentDirName,
|
||||
packageName: component.packageJson.name,
|
||||
entry: `dist/${component.packageJson.main?.replace(/^\.\/dist\//, '') ?? 'index.js'}`,
|
||||
source: 'src/index.tsx',
|
||||
tags: ['legacy']
|
||||
}
|
||||
|
||||
const releaseDir = path.join(hostDir, manifest.folderName)
|
||||
copyDirectoryContents(distDir, releaseDir)
|
||||
fs.writeFileSync(path.join(releaseDir, 'glitch.manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`)
|
||||
|
||||
const stagingComponentDir = path.join(stagingDir, manifest.folderName)
|
||||
copyDirectoryContents(distDir, stagingComponentDir)
|
||||
fs.writeFileSync(
|
||||
path.join(stagingComponentDir, 'glitch.manifest.json'),
|
||||
`${JSON.stringify(manifest, null, 2)}\n`
|
||||
)
|
||||
}
|
||||
|
||||
const options = parseArgs(process.argv.slice(2))
|
||||
const componentNames = resolveComponents(options.components)
|
||||
const components = componentNames.map(loadComponent)
|
||||
|
||||
switch (options.command) {
|
||||
case 'list': {
|
||||
for (const componentName of componentNames) {
|
||||
console.log(componentName)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'validate': {
|
||||
let errorCount = 0
|
||||
let warningCount = 0
|
||||
|
||||
for (const component of components) {
|
||||
const { errors, warnings } = validateComponent(component)
|
||||
if (errors.length === 0 && warnings.length === 0) {
|
||||
console.log(`OK ${component.componentDirName}`)
|
||||
continue
|
||||
}
|
||||
|
||||
console.log(component.componentDirName)
|
||||
for (const error of errors) {
|
||||
errorCount += 1
|
||||
console.log(` ERROR ${error}`)
|
||||
}
|
||||
for (const warning of warnings) {
|
||||
warningCount += 1
|
||||
console.log(` WARN ${warning}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (errorCount > 0) {
|
||||
fail(`Validation failed with ${errorCount} error(s) and ${warningCount} warning(s).`)
|
||||
}
|
||||
|
||||
console.log(`Validation passed with ${warningCount} warning(s).`)
|
||||
break
|
||||
}
|
||||
|
||||
case 'build': {
|
||||
for (const component of components) {
|
||||
console.log(`=== Building ${component.componentDirName} ===`)
|
||||
runBuild(component)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'release': {
|
||||
const hostDir =
|
||||
options.hostDir ||
|
||||
path.resolve(cwd, '..', 'gnommoweb', 'src', 'components', 'glitch')
|
||||
|
||||
ensureDirectory(hostDir)
|
||||
ensureDirectory(options.stagingDir)
|
||||
|
||||
for (const component of components) {
|
||||
console.log(`=== Releasing ${component.componentDirName} ===`)
|
||||
runBuild(component)
|
||||
releaseComponent(component, hostDir, options.stagingDir)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
fail(`Unknown command: ${options.command}`)
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
const cwd = process.cwd()
|
||||
const templateDir = path.join(cwd, 'templates', 'react-vite-glitch-component')
|
||||
|
||||
function fail(message) {
|
||||
console.error(message)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
function toKebabCase(value) {
|
||||
return value
|
||||
.trim()
|
||||
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
||||
.replace(/[^a-zA-Z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
.toLowerCase()
|
||||
}
|
||||
|
||||
function toSnakeCase(value) {
|
||||
return value.replace(/-/g, '_')
|
||||
}
|
||||
|
||||
function toPascalCase(value) {
|
||||
return value
|
||||
.split('-')
|
||||
.filter(Boolean)
|
||||
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
|
||||
.join('')
|
||||
}
|
||||
|
||||
function toTitleCase(value) {
|
||||
return value
|
||||
.split('-')
|
||||
.filter(Boolean)
|
||||
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = [...argv]
|
||||
const result = {
|
||||
componentId: '',
|
||||
title: '',
|
||||
dir: ''
|
||||
}
|
||||
|
||||
while (args.length > 0) {
|
||||
const arg = args.shift()
|
||||
if (!arg) continue
|
||||
|
||||
if (!result.componentId && !arg.startsWith('--')) {
|
||||
result.componentId = arg
|
||||
continue
|
||||
}
|
||||
|
||||
if (arg === '--title') {
|
||||
result.title = args.shift() ?? ''
|
||||
continue
|
||||
}
|
||||
|
||||
if (arg === '--dir') {
|
||||
result.dir = args.shift() ?? ''
|
||||
continue
|
||||
}
|
||||
|
||||
fail(`Unknown argument: ${arg}`)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function copyTemplate(sourceDir, destinationDir, replacements) {
|
||||
fs.mkdirSync(destinationDir, { recursive: true })
|
||||
|
||||
for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
|
||||
const sourcePath = path.join(sourceDir, entry.name)
|
||||
const destinationPath = path.join(destinationDir, entry.name)
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
copyTemplate(sourcePath, destinationPath, replacements)
|
||||
continue
|
||||
}
|
||||
|
||||
const source = fs.readFileSync(sourcePath, 'utf8')
|
||||
const rendered = Object.entries(replacements).reduce(
|
||||
(content, [key, value]) => content.replaceAll(key, value),
|
||||
source
|
||||
)
|
||||
fs.writeFileSync(destinationPath, rendered)
|
||||
}
|
||||
}
|
||||
|
||||
const options = parseArgs(process.argv.slice(2))
|
||||
if (!options.componentId) {
|
||||
fail('Usage: npm run glitch:new -- <component-id> --title "Display Name" [--dir /custom/path]')
|
||||
}
|
||||
|
||||
if (!fs.existsSync(templateDir)) {
|
||||
fail(`Template directory not found: ${templateDir}`)
|
||||
}
|
||||
|
||||
const componentId = toKebabCase(options.componentId)
|
||||
if (!componentId) {
|
||||
fail('Component id resolved to an empty value.')
|
||||
}
|
||||
|
||||
const folderName = `glitch_${toSnakeCase(componentId)}`
|
||||
const displayName = options.title.trim() || toTitleCase(componentId)
|
||||
const bundleName = componentId
|
||||
const destinationDir = options.dir
|
||||
? path.resolve(cwd, options.dir)
|
||||
: path.join(cwd, folderName)
|
||||
|
||||
if (fs.existsSync(destinationDir)) {
|
||||
fail(`Destination already exists: ${destinationDir}`)
|
||||
}
|
||||
|
||||
copyTemplate(templateDir, destinationDir, {
|
||||
'__COMPONENT_ID__': componentId,
|
||||
'__COMPONENT_DISPLAY_NAME__': displayName,
|
||||
'__LIB_GLOBAL_NAME__': toPascalCase(componentId),
|
||||
'__FOLDER_NAME__': folderName,
|
||||
'__BUNDLE_NAME__': bundleName
|
||||
})
|
||||
|
||||
console.log(`Created ${folderName}`)
|
||||
console.log(`Path: ${destinationDir}`)
|
||||
console.log('Next steps:')
|
||||
console.log(` cd ${folderName}`)
|
||||
console.log(' npm install')
|
||||
console.log(' npm run dev')
|
||||
Executable
+78
@@ -0,0 +1,78 @@
|
||||
#!/bin/bash
|
||||
# Sets up git remotes for all glitch-component folders and pushes to ramanujan.
|
||||
#
|
||||
# Usage:
|
||||
# ./setup-remotes.sh # set remotes only
|
||||
# ./setup-remotes.sh --push # set remotes and push all
|
||||
#
|
||||
# Reads GIT_USER / GIT_PASSWORD from ../.env if present, otherwise prompts.
|
||||
|
||||
set -e
|
||||
|
||||
REMOTE_HOST="ramanujan.glitch.university"
|
||||
COMPONENTS_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PUSH=false
|
||||
[[ "$1" == "--push" ]] && PUSH=true
|
||||
|
||||
# ── Load credentials ──────────────────────────────────────────────────────────
|
||||
|
||||
ENV_FILE="$COMPONENTS_DIR/../gnommoweb/.env.prod"
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
GIT_USER=$(grep -E '^GIT_USER=' "$ENV_FILE" | cut -d= -f2 | tr -d '"'"'" | head -1)
|
||||
GIT_PASSWORD=$(grep -E '^GIT_PASSWORD=' "$ENV_FILE" | cut -d= -f2 | tr -d '"'"'" | head -1)
|
||||
fi
|
||||
|
||||
if [ -z "$GIT_USER" ] || [ -z "$GIT_PASSWORD" ]; then
|
||||
read -rp "Git username: " GIT_USER
|
||||
read -rsp "Git password: " GIT_PASSWORD
|
||||
echo
|
||||
fi
|
||||
|
||||
REMOTE_BASE="https://${GIT_USER}:${GIT_PASSWORD}@${REMOTE_HOST}"
|
||||
|
||||
# ── Process each component folder ─────────────────────────────────────────────
|
||||
|
||||
for dir in "$COMPONENTS_DIR"/*/; do
|
||||
name=$(basename "$dir")
|
||||
|
||||
# Skip the skills folder and any non-component entries
|
||||
[[ "$name" == "skills" ]] && continue
|
||||
|
||||
echo "── $name"
|
||||
cd "$dir"
|
||||
|
||||
# Init if not already a git repo
|
||||
if [ ! -d ".git" ]; then
|
||||
git init -q
|
||||
git add -A
|
||||
git commit -q -m "Initial commit" 2>/dev/null || true
|
||||
echo " initialised"
|
||||
fi
|
||||
|
||||
# Add or update remote
|
||||
if git remote get-url origin &>/dev/null; then
|
||||
git remote set-url origin "${REMOTE_BASE}/${name}.git"
|
||||
echo " remote updated"
|
||||
else
|
||||
git remote add origin "${REMOTE_BASE}/${name}.git"
|
||||
echo " remote added"
|
||||
fi
|
||||
|
||||
# Push
|
||||
if $PUSH; then
|
||||
if git push -u origin main -q 2>/dev/null || git push -u origin master:main -q 2>/dev/null; then
|
||||
echo " pushed"
|
||||
else
|
||||
echo " push failed (repo may not exist on server yet)"
|
||||
fi
|
||||
fi
|
||||
|
||||
cd "$COMPONENTS_DIR"
|
||||
done
|
||||
|
||||
echo ""
|
||||
if $PUSH; then
|
||||
echo "Done."
|
||||
else
|
||||
echo "Remotes configured. Run with --push to push all, or push individually."
|
||||
fi
|
||||
@@ -0,0 +1,405 @@
|
||||
# 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`
|
||||
@@ -0,0 +1,59 @@
|
||||
export interface GlitchComponentConfig {
|
||||
id: string
|
||||
name: string
|
||||
version: string
|
||||
params: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface GlitchHostBridge {
|
||||
playSound?: (id: string, payload?: Record<string, unknown>) => void
|
||||
stopSound?: (id: string, payload?: Record<string, unknown>) => void
|
||||
emit?: (type: string, payload?: unknown) => void
|
||||
}
|
||||
|
||||
export interface GlitchTheme {
|
||||
primary: string
|
||||
accent: string
|
||||
bg: string
|
||||
bgSecondary: string
|
||||
text: string
|
||||
textMuted: string
|
||||
border: string
|
||||
}
|
||||
|
||||
export interface GlitchComponentResult {
|
||||
success: boolean
|
||||
score?: number
|
||||
data?: unknown
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface GlitchComponentProps {
|
||||
config: GlitchComponentConfig
|
||||
onComplete: (result: GlitchComponentResult) => void
|
||||
onProgress?: (percent: number) => void
|
||||
theme?: GlitchTheme
|
||||
className?: string
|
||||
host?: GlitchHostBridge
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
export interface GlitchComponentMetadata {
|
||||
name: string
|
||||
displayName: string
|
||||
version: string
|
||||
paramSchema: ParamSchema
|
||||
defaultParams: Record<string, unknown>
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
:root {
|
||||
--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;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background:
|
||||
radial-gradient(circle at top, rgb(99 102 241 / 18%), transparent 36%),
|
||||
linear-gradient(180deg, #080810, #0d1018);
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-main);
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font: inherit;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
# __COMPONENT_DISPLAY_NAME__
|
||||
|
||||
Standalone Glitch University component scaffolded from the workspace standard.
|
||||
|
||||
## Commands
|
||||
|
||||
- `npm install`
|
||||
- `npm run dev`
|
||||
- `npm run build`
|
||||
- `npm run preview`
|
||||
|
||||
## Notes
|
||||
|
||||
- The packaged entrypoint is `src/index.tsx`
|
||||
- Dev-only styling lives in `src/dev-theme.css`
|
||||
- Host integration metadata lives in `glitch.manifest.json`
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"componentId": "__COMPONENT_ID__",
|
||||
"displayName": "__COMPONENT_DISPLAY_NAME__",
|
||||
"version": "1.0.0",
|
||||
"folderName": "__FOLDER_NAME__",
|
||||
"packageName": "@glitch-components/__COMPONENT_ID__",
|
||||
"entry": "dist/__BUNDLE_NAME__.js",
|
||||
"source": "src/index.tsx",
|
||||
"tags": [
|
||||
"glitch-component"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>__COMPONENT_DISPLAY_NAME__</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/dev.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "@glitch-components/__COMPONENT_ID__",
|
||||
"version": "1.0.0",
|
||||
"description": "__COMPONENT_DISPLAY_NAME__ glitch component for Glitch University",
|
||||
"type": "module",
|
||||
"main": "./dist/__BUNDLE_NAME__.js",
|
||||
"module": "./dist/__BUNDLE_NAME__.js",
|
||||
"types": "./src/types.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/__BUNDLE_NAME__.js",
|
||||
"types": "./src/types.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"src/types.ts",
|
||||
"glitch.manifest.json"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.2.0",
|
||||
"@types/react-dom": "^19.2.0",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"leva": "^0.10.1",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.2.4",
|
||||
"vite-plugin-css-injected-by-js": "^3.5.2"
|
||||
},
|
||||
"keywords": [
|
||||
"glitch",
|
||||
"mini-game",
|
||||
"react",
|
||||
"component"
|
||||
],
|
||||
"author": "Glitch.university",
|
||||
"license": "MIT"
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { CSSProperties } from 'react'
|
||||
import styles from './styles.module.css'
|
||||
import type { GlitchComponentProps } from './types'
|
||||
|
||||
interface ComponentParams {
|
||||
speed?: number
|
||||
primaryColor?: string
|
||||
}
|
||||
|
||||
export default function GlitchComponent({
|
||||
config,
|
||||
onComplete,
|
||||
onProgress,
|
||||
theme,
|
||||
className
|
||||
}: GlitchComponentProps) {
|
||||
const params = (config.params ?? {}) as ComponentParams
|
||||
const speed = params.speed ?? 1
|
||||
|
||||
const componentTheme = {
|
||||
'--component-bg': theme?.bg,
|
||||
'--component-bg-secondary': theme?.bgSecondary,
|
||||
'--component-text': theme?.text,
|
||||
'--component-text-muted': theme?.textMuted,
|
||||
'--component-primary': params.primaryColor ?? theme?.primary,
|
||||
'--component-accent': theme?.accent,
|
||||
'--component-border': theme?.border
|
||||
} as CSSProperties
|
||||
|
||||
function handleComplete() {
|
||||
onProgress?.(100)
|
||||
onComplete({
|
||||
success: true,
|
||||
score: Math.round(100 * speed),
|
||||
data: { completedAt: new Date().toISOString(), speed }
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={[styles.root, className].filter(Boolean).join(' ')} style={componentTheme}>
|
||||
<h2 className={styles.title}>__COMPONENT_DISPLAY_NAME__</h2>
|
||||
<p className={styles.copy}>
|
||||
Replace this scaffold UI with the actual glitch mechanic. Keep the contract and host token usage.
|
||||
</p>
|
||||
<div className={styles.row}>
|
||||
<span className={styles.badge}>Speed: {speed.toFixed(1)}</span>
|
||||
<button className={styles.button} onClick={() => onProgress?.(50)} type="button">
|
||||
Progress 50%
|
||||
</button>
|
||||
<button className={styles.buttonPrimary} onClick={handleComplete} type="button">
|
||||
Complete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Iceland&family=JetBrains+Mono:wght@400;600&family=Russo+One&display=swap");
|
||||
|
||||
:root {
|
||||
--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;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background:
|
||||
radial-gradient(circle at top, rgb(99 102 241 / 18%), transparent 36%),
|
||||
linear-gradient(180deg, #080810, #0d1018);
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-main);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import { Leva, useControls } from 'leva'
|
||||
import Component from './Component'
|
||||
import './dev-theme.css'
|
||||
import { metadata } from './index'
|
||||
|
||||
function buildLevaSchema() {
|
||||
return Object.entries(metadata.paramSchema).reduce((acc, [key, schema]) => {
|
||||
if (schema.type === 'range' || schema.type === 'number') {
|
||||
acc[key] = {
|
||||
value: schema.default as number,
|
||||
min: schema.min,
|
||||
max: schema.max,
|
||||
step: schema.step
|
||||
}
|
||||
return acc
|
||||
}
|
||||
|
||||
if (schema.type === 'select') {
|
||||
acc[key] = {
|
||||
value: schema.default,
|
||||
options:
|
||||
schema.options?.reduce<Record<string, string | number>>((map, option) => {
|
||||
map[option.label] = option.value
|
||||
return map
|
||||
}, {}) ?? {}
|
||||
}
|
||||
return acc
|
||||
}
|
||||
|
||||
acc[key] = schema.default
|
||||
return acc
|
||||
}, {} as Record<string, unknown>)
|
||||
}
|
||||
|
||||
function DevHarness() {
|
||||
const params = useControls(buildLevaSchema())
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', 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'
|
||||
}}
|
||||
onProgress={(percent) => {
|
||||
console.log('Progress:', percent)
|
||||
}}
|
||||
onComplete={(result) => {
|
||||
console.log('Complete:', result)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<DevHarness />
|
||||
</React.StrictMode>
|
||||
)
|
||||
@@ -0,0 +1,37 @@
|
||||
import Component from './Component'
|
||||
import type { GlitchComponentMetadata } from './types'
|
||||
|
||||
export default Component
|
||||
|
||||
export const metadata: GlitchComponentMetadata = {
|
||||
name: '__COMPONENT_ID__',
|
||||
displayName: '__COMPONENT_DISPLAY_NAME__',
|
||||
version: '1.0.0',
|
||||
paramSchema: {
|
||||
speed: {
|
||||
type: 'range',
|
||||
label: 'Speed',
|
||||
default: 1,
|
||||
min: 0.1,
|
||||
max: 5,
|
||||
step: 0.1
|
||||
},
|
||||
primaryColor: {
|
||||
type: 'color',
|
||||
label: 'Primary Color',
|
||||
default: '#6366f1'
|
||||
}
|
||||
},
|
||||
defaultParams: {
|
||||
speed: 1,
|
||||
primaryColor: '#6366f1'
|
||||
}
|
||||
}
|
||||
|
||||
export type {
|
||||
GlitchComponentProps,
|
||||
GlitchComponentConfig,
|
||||
GlitchComponentResult,
|
||||
GlitchTheme,
|
||||
GlitchHostBridge
|
||||
} from './types'
|
||||
@@ -0,0 +1,85 @@
|
||||
.root {
|
||||
--component-bg: var(--color-bg, #0a0a0f);
|
||||
--component-bg-secondary: var(--color-bg-secondary, #12121a);
|
||||
--component-text: var(--color-text, #e8e8ec);
|
||||
--component-text-muted: var(--color-text-muted, #9999a8);
|
||||
--component-primary: var(--color-primary, #6366f1);
|
||||
--component-accent: var(--color-accent, #22d3ee);
|
||||
--component-border: var(--color-border, #2a2a3a);
|
||||
box-sizing: border-box;
|
||||
width: min(100%, 900px);
|
||||
margin: 0 auto;
|
||||
padding: var(--spacing-md, 2rem);
|
||||
border-radius: var(--border-radius, 12px);
|
||||
border: 1px solid var(--component-border);
|
||||
background:
|
||||
radial-gradient(circle at 20% 10%, color-mix(in srgb, var(--component-primary) 16%, transparent), transparent 45%),
|
||||
linear-gradient(180deg, var(--component-bg-secondary), var(--component-bg));
|
||||
color: var(--component-text);
|
||||
font-family: var(--font-main, system-ui, sans-serif);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.root,
|
||||
.root *,
|
||||
.root *::before,
|
||||
.root *::after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0 0 var(--spacing-xs, 0.5rem);
|
||||
color: var(--component-primary);
|
||||
font-family: var(--font-display, var(--font-main, system-ui, sans-serif));
|
||||
font-size: clamp(1.2rem, 2vw, 1.75rem);
|
||||
}
|
||||
|
||||
.copy {
|
||||
margin: 0 0 var(--spacing-sm, 1rem);
|
||||
color: var(--component-text-muted);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-xs, 0.5rem);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.badge {
|
||||
border: 1px solid var(--component-border);
|
||||
border-radius: var(--border-radius-sm, 8px);
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-family: var(--font-mono, monospace);
|
||||
background: color-mix(in srgb, var(--component-primary) 8%, transparent);
|
||||
}
|
||||
|
||||
.button,
|
||||
.buttonPrimary {
|
||||
border-radius: var(--border-radius-sm, 8px);
|
||||
border: 1px solid var(--component-border);
|
||||
padding: 0.65rem 0.9rem;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
transition:
|
||||
transform var(--transition, 0.2s ease),
|
||||
opacity var(--transition, 0.2s ease),
|
||||
border-color var(--transition, 0.2s ease);
|
||||
}
|
||||
|
||||
.button {
|
||||
background: var(--component-bg-secondary);
|
||||
color: var(--component-text);
|
||||
}
|
||||
|
||||
.buttonPrimary {
|
||||
background: var(--component-primary);
|
||||
border-color: color-mix(in srgb, var(--component-primary) 65%, white);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.button:hover,
|
||||
.buttonPrimary:hover {
|
||||
opacity: 0.95;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
export interface GlitchComponentConfig {
|
||||
id: string
|
||||
name: string
|
||||
version: string
|
||||
params: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface GlitchHostBridge {
|
||||
playSound?: (id: string, payload?: Record<string, unknown>) => void
|
||||
stopSound?: (id: string, payload?: Record<string, unknown>) => void
|
||||
emit?: (type: string, payload?: unknown) => void
|
||||
}
|
||||
|
||||
export interface GlitchTheme {
|
||||
primary: string
|
||||
accent: string
|
||||
bg: string
|
||||
bgSecondary: string
|
||||
text: string
|
||||
textMuted: string
|
||||
border: string
|
||||
}
|
||||
|
||||
export interface GlitchComponentResult {
|
||||
success: boolean
|
||||
score?: number
|
||||
data?: unknown
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface GlitchComponentProps {
|
||||
config: GlitchComponentConfig
|
||||
onComplete: (result: GlitchComponentResult) => void
|
||||
onProgress?: (percent: number) => void
|
||||
theme?: GlitchTheme
|
||||
className?: string
|
||||
host?: GlitchHostBridge
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
export interface GlitchComponentMetadata {
|
||||
name: string
|
||||
displayName: string
|
||||
version: string
|
||||
paramSchema: ParamSchema
|
||||
defaultParams: Record<string, unknown>
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"module": "ESNext",
|
||||
"types": [
|
||||
"vite/client"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { resolve } from 'path'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { defineConfig } from 'vite'
|
||||
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
|
||||
|
||||
export default defineConfig(({ mode }) => ({
|
||||
plugins: [react(), cssInjectedByJsPlugin()],
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/index.tsx'),
|
||||
name: '__LIB_GLOBAL_NAME__',
|
||||
fileName: '__BUNDLE_NAME__',
|
||||
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
|
||||
}
|
||||
}))
|
||||
Reference in New Issue
Block a user