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