Adding the first glitch gallery

This commit is contained in:
2026-03-24 11:30:14 +01:00
commit 137d1a8191
24 changed files with 1572 additions and 0 deletions
@@ -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
}
}))