Initial commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Demographic Wave
|
||||||
|
|
||||||
|
Standalone Glitch University component that simulates how a large generation moves through age bands and changes which cohort's policy preferences dominate.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
- `npm install`
|
||||||
|
- `npm run dev`
|
||||||
|
- `npm run build`
|
||||||
|
|
||||||
|
The component follows the Glitch Component contract and registers itself as `window.GlitchComponents["demographic-wave"]` when loaded from the built IIFE bundle.
|
||||||
Vendored
+8
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"componentId": "demographic-wave",
|
||||||
|
"displayName": "Demographic Wave",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"folderName": "glitch_demographic_wave",
|
||||||
|
"packageName": "@glitch-components/demographic-wave",
|
||||||
|
"entry": "dist/demographic-wave.js",
|
||||||
|
"source": "src/index.tsx",
|
||||||
|
"tags": [
|
||||||
|
"glitch-component",
|
||||||
|
"demography",
|
||||||
|
"policy",
|
||||||
|
"simulation"
|
||||||
|
]
|
||||||
|
}
|
||||||
+12
@@ -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>Demographic Wave</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/dev.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Generated
+2919
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"name": "@glitch-components/demographic-wave",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Demographic Wave glitch component for Glitch University",
|
||||||
|
"type": "module",
|
||||||
|
"main": "./dist/demographic-wave.js",
|
||||||
|
"module": "./dist/demographic-wave.js",
|
||||||
|
"types": "./src/types.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/demographic-wave.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",
|
||||||
|
"demography",
|
||||||
|
"simulation",
|
||||||
|
"component"
|
||||||
|
],
|
||||||
|
"author": "Glitch.university",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
@@ -0,0 +1,460 @@
|
|||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import type { CSSProperties } from 'react'
|
||||||
|
import styles from './styles.module.css'
|
||||||
|
import type { GlitchComponentProps } from './types'
|
||||||
|
|
||||||
|
type GenerationKey = 'silent' | 'boomer' | 'post'
|
||||||
|
|
||||||
|
interface Stage {
|
||||||
|
min: number
|
||||||
|
title: string
|
||||||
|
copy: string
|
||||||
|
policy: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ComponentParams {
|
||||||
|
startYear?: number
|
||||||
|
endYear?: number
|
||||||
|
animationSpeed?: number
|
||||||
|
waveScale?: number
|
||||||
|
startEqualized?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Palette {
|
||||||
|
silent: string
|
||||||
|
boomer: string
|
||||||
|
post: string
|
||||||
|
accent: string
|
||||||
|
grid: string
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const COHORT_YEARS = Array.from({ length: 82 }, (_, index) => 1928 + index)
|
||||||
|
const BOOMER_CENTER_YEAR = 1957
|
||||||
|
const MIN_YEAR = 1946
|
||||||
|
const MAX_YEAR = 2038
|
||||||
|
|
||||||
|
const STAGES: Stage[] = [
|
||||||
|
{
|
||||||
|
min: 0,
|
||||||
|
title: 'SCHOOLS',
|
||||||
|
copy: 'A large child cohort pulls classrooms, teachers, and local budgets toward youth infrastructure.',
|
||||||
|
policy: 'Education and schools'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
min: 18,
|
||||||
|
title: 'CAMPUS',
|
||||||
|
copy: 'The wave enters college and first jobs. Credentials, housing, and entry labor markets start to flex.',
|
||||||
|
policy: 'Universities and entry jobs'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
min: 30,
|
||||||
|
title: 'FAMILY',
|
||||||
|
copy: 'The cohort becomes parents and buyers. Homes, childcare, transport, and suburbs absorb the demand.',
|
||||||
|
policy: 'Housing and family subsidies'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
min: 50,
|
||||||
|
title: 'VOTES',
|
||||||
|
copy: 'High turnout meets cohort size. Tax, asset, and stability preferences become easier to pass.',
|
||||||
|
policy: 'Tax and asset rules'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
min: 65,
|
||||||
|
title: 'RETIREMENT',
|
||||||
|
copy: 'The wave reaches old age. Health care, pensions, and age-linked spending gain political gravity.',
|
||||||
|
policy: 'Pensions and health care'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const GENERATIONS: { key: GenerationKey; label: string }[] = [
|
||||||
|
{ key: 'silent', label: 'Silent generation' },
|
||||||
|
{ key: 'boomer', label: 'Boomers' },
|
||||||
|
{ key: 'post', label: 'Later cohorts' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value))
|
||||||
|
|
||||||
|
const numberParam = (value: unknown, fallback: number) => (typeof value === 'number' ? value : fallback)
|
||||||
|
|
||||||
|
const booleanParam = (value: unknown, fallback: boolean) => (typeof value === 'boolean' ? value : fallback)
|
||||||
|
|
||||||
|
const cohortFamily = (birthYear: number): GenerationKey => {
|
||||||
|
if (birthYear >= 1946 && birthYear <= 1964) return 'boomer'
|
||||||
|
if (birthYear < 1946) return 'silent'
|
||||||
|
return 'post'
|
||||||
|
}
|
||||||
|
|
||||||
|
const turnout = (age: number) => {
|
||||||
|
if (age < 18 || age > 88) return 0
|
||||||
|
if (age < 30) return 0.42
|
||||||
|
if (age < 45) return 0.62
|
||||||
|
if (age < 65) return 0.82
|
||||||
|
return 0.9 - Math.max(0, age - 72) * 0.018
|
||||||
|
}
|
||||||
|
|
||||||
|
const stageForAge = (age: number) =>
|
||||||
|
STAGES.reduce((chosen, stage) => (age >= stage.min ? stage : chosen), STAGES[0])
|
||||||
|
|
||||||
|
const createCohortSize = (equalMode: boolean, waveScale: number) => (birthYear: number) => {
|
||||||
|
if (equalMode) return 1
|
||||||
|
|
||||||
|
const boomerPeak = Math.exp(-Math.pow((birthYear - BOOMER_CENTER_YEAR) / 7.5, 2)) * waveScale
|
||||||
|
const postDip = Math.exp(-Math.pow((birthYear - 1974) / 8, 2)) * -0.22
|
||||||
|
const echo = Math.exp(-Math.pow((birthYear - 1991) / 9, 2)) * 0.18
|
||||||
|
return Math.max(0.5, 0.78 + boomerPeak + postDip + echo)
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawWave(
|
||||||
|
canvas: HTMLCanvasElement,
|
||||||
|
year: number,
|
||||||
|
equalMode: boolean,
|
||||||
|
waveScale: number,
|
||||||
|
palette: Palette
|
||||||
|
) {
|
||||||
|
const rect = canvas.getBoundingClientRect()
|
||||||
|
const scale = window.devicePixelRatio || 1
|
||||||
|
const width = rect.width
|
||||||
|
const height = rect.height
|
||||||
|
|
||||||
|
canvas.width = Math.floor(width * scale)
|
||||||
|
canvas.height = Math.floor(height * scale)
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
if (!ctx) return
|
||||||
|
|
||||||
|
ctx.setTransform(scale, 0, 0, scale, 0, 0)
|
||||||
|
ctx.clearRect(0, 0, width, height)
|
||||||
|
|
||||||
|
const padX = width * 0.07
|
||||||
|
const base = height * 0.72
|
||||||
|
const maxH = height * 0.68
|
||||||
|
const sizeFor = createCohortSize(equalMode, waveScale)
|
||||||
|
|
||||||
|
ctx.lineWidth = 1
|
||||||
|
ctx.strokeStyle = palette.grid
|
||||||
|
for (let index = 0; index <= 4; index += 1) {
|
||||||
|
const x = padX + (index / 4) * (width - padX * 2)
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(x, height * 0.14)
|
||||||
|
ctx.lineTo(x, height * 0.88)
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const birthYear of COHORT_YEARS) {
|
||||||
|
const age = year - birthYear
|
||||||
|
if (age < 0 || age > 88) continue
|
||||||
|
|
||||||
|
const size = sizeFor(birthYear)
|
||||||
|
const x = padX + (age / 88) * (width - padX * 2)
|
||||||
|
const radius = Math.max(4, width * 0.006 + size * width * 0.009)
|
||||||
|
const y = base - size * maxH * 0.52 - Math.sin((birthYear + year) * 0.18) * height * 0.025
|
||||||
|
const family = cohortFamily(birthYear)
|
||||||
|
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(x, y, radius, 0, Math.PI * 2)
|
||||||
|
ctx.fillStyle = palette[family]
|
||||||
|
ctx.globalAlpha = family === 'boomer' ? 0.94 : 0.68
|
||||||
|
ctx.fill()
|
||||||
|
ctx.globalAlpha = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
const boomerAge = year - BOOMER_CENTER_YEAR
|
||||||
|
if (boomerAge < 0 || boomerAge > 88) return
|
||||||
|
|
||||||
|
const x = padX + (boomerAge / 88) * (width - padX * 2)
|
||||||
|
const y = base - sizeFor(BOOMER_CENTER_YEAR) * maxH * 0.52
|
||||||
|
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(x, height * 0.16)
|
||||||
|
ctx.lineTo(x, height * 0.9)
|
||||||
|
ctx.strokeStyle = palette.boomer
|
||||||
|
ctx.globalAlpha = 0.42
|
||||||
|
ctx.lineWidth = 2
|
||||||
|
ctx.stroke()
|
||||||
|
ctx.globalAlpha = 1
|
||||||
|
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(x, y, Math.max(16, width * 0.035), 0, Math.PI * 2)
|
||||||
|
ctx.strokeStyle = palette.accent
|
||||||
|
ctx.lineWidth = 3
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Component({
|
||||||
|
config,
|
||||||
|
onComplete,
|
||||||
|
onProgress,
|
||||||
|
theme,
|
||||||
|
className,
|
||||||
|
host
|
||||||
|
}: GlitchComponentProps) {
|
||||||
|
const params = config.params as ComponentParams
|
||||||
|
const startYear = clamp(Math.round(numberParam(params.startYear, MIN_YEAR)), MIN_YEAR, MAX_YEAR)
|
||||||
|
const endYear = clamp(Math.round(numberParam(params.endYear, MAX_YEAR)), startYear + 1, MAX_YEAR)
|
||||||
|
const animationSpeed = clamp(numberParam(params.animationSpeed, 1), 0.25, 3)
|
||||||
|
const waveScale = clamp(numberParam(params.waveScale, 1.15), 0.2, 1.8)
|
||||||
|
const [year, setYear] = useState(startYear)
|
||||||
|
const [running, setRunning] = useState(true)
|
||||||
|
const [equalMode, setEqualMode] = useState(() => booleanParam(params.startEqualized, false))
|
||||||
|
const [completed, setCompleted] = useState(false)
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement | null>(null)
|
||||||
|
const lastTickRef = useRef(0)
|
||||||
|
|
||||||
|
const palette = useMemo<Palette>(() => ({
|
||||||
|
silent: '#71a6d2',
|
||||||
|
boomer: '#e7b84f',
|
||||||
|
post: '#7ee0a0',
|
||||||
|
accent: theme?.accent ?? '#22d3ee',
|
||||||
|
grid: 'rgba(232, 232, 236, 0.13)',
|
||||||
|
text: theme?.text ?? '#e8e8ec'
|
||||||
|
}), [theme?.accent, theme?.text])
|
||||||
|
|
||||||
|
const themeStyle = useMemo(() => ({
|
||||||
|
'--gc-primary': theme?.primary,
|
||||||
|
'--gc-accent': theme?.accent,
|
||||||
|
'--gc-bg': theme?.bg,
|
||||||
|
'--gc-bg-secondary': theme?.bgSecondary,
|
||||||
|
'--gc-text': theme?.text,
|
||||||
|
'--gc-text-muted': theme?.textMuted,
|
||||||
|
'--gc-border': theme?.border
|
||||||
|
}) as CSSProperties, [theme])
|
||||||
|
|
||||||
|
const sizeFor = useMemo(() => createCohortSize(equalMode, waveScale), [equalMode, waveScale])
|
||||||
|
|
||||||
|
const boomerAge = year - BOOMER_CENTER_YEAR
|
||||||
|
const activeStage = stageForAge(clamp(boomerAge, 0, 88))
|
||||||
|
|
||||||
|
const currentPressure = useMemo(() => {
|
||||||
|
const totals = Object.fromEntries(STAGES.map((stage) => [stage.policy, 0]))
|
||||||
|
for (const birthYear of COHORT_YEARS) {
|
||||||
|
const age = year - birthYear
|
||||||
|
if (age < 0 || age > 88) continue
|
||||||
|
|
||||||
|
const voteWeight = sizeFor(birthYear) * turnout(age)
|
||||||
|
totals[stageForAge(age).policy] += voteWeight
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = Object.values(totals).reduce((sum, value) => sum + value, 0)
|
||||||
|
return total > 0 ? Math.round((totals[activeStage.policy] / total) * 100) : 0
|
||||||
|
}, [activeStage.policy, sizeFor, year])
|
||||||
|
|
||||||
|
const generationWins = useMemo(() => {
|
||||||
|
const wins = Object.fromEntries(GENERATIONS.map((generation) => [generation.key, 0])) as Record<GenerationKey, number>
|
||||||
|
|
||||||
|
for (let targetYear = startYear; targetYear <= year; targetYear += 1) {
|
||||||
|
const totals = Object.fromEntries(GENERATIONS.map((generation) => [generation.key, 0])) as Record<GenerationKey, number>
|
||||||
|
|
||||||
|
for (const birthYear of COHORT_YEARS) {
|
||||||
|
const age = targetYear - birthYear
|
||||||
|
if (age < 18 || age > 88) continue
|
||||||
|
totals[cohortFamily(birthYear)] += sizeFor(birthYear) * turnout(age)
|
||||||
|
}
|
||||||
|
|
||||||
|
const winner = GENERATIONS.reduce((chosen, generation) =>
|
||||||
|
totals[generation.key] > totals[chosen.key] ? generation : chosen
|
||||||
|
, GENERATIONS[0])
|
||||||
|
|
||||||
|
if (totals[winner.key] > 0) wins[winner.key] += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return wins
|
||||||
|
}, [sizeFor, startYear, year])
|
||||||
|
|
||||||
|
const maxWins = Math.max(...Object.values(generationWins), 1)
|
||||||
|
const progress = Math.round(((year - startYear) / Math.max(1, endYear - startYear)) * 100)
|
||||||
|
|
||||||
|
const redraw = useCallback(() => {
|
||||||
|
if (!canvasRef.current) return
|
||||||
|
drawWave(canvasRef.current, year, equalMode, waveScale, palette)
|
||||||
|
}, [equalMode, palette, waveScale, year])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setYear((current) => clamp(current, startYear, endYear))
|
||||||
|
}, [endYear, startYear])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setEqualMode(booleanParam(params.startEqualized, false))
|
||||||
|
}, [params.startEqualized])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onProgress?.(progress)
|
||||||
|
}, [onProgress, progress])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
redraw()
|
||||||
|
const canvas = canvasRef.current
|
||||||
|
if (!canvas) return undefined
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(redraw)
|
||||||
|
resizeObserver.observe(canvas)
|
||||||
|
return () => resizeObserver.disconnect()
|
||||||
|
}, [redraw])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let animationFrame = 0
|
||||||
|
|
||||||
|
const step = (timestamp: number) => {
|
||||||
|
const interval = 260 / animationSpeed
|
||||||
|
if (running && timestamp - lastTickRef.current > interval) {
|
||||||
|
setYear((current) => (current >= endYear ? startYear : current + 1))
|
||||||
|
lastTickRef.current = timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
animationFrame = window.requestAnimationFrame(step)
|
||||||
|
}
|
||||||
|
|
||||||
|
animationFrame = window.requestAnimationFrame(step)
|
||||||
|
return () => window.cancelAnimationFrame(animationFrame)
|
||||||
|
}, [animationSpeed, endYear, running, startYear])
|
||||||
|
|
||||||
|
const playClick = useCallback((target: string) => {
|
||||||
|
host?.playSound?.('ui.button_click', { target, component: config.name })
|
||||||
|
}, [config.name, host])
|
||||||
|
|
||||||
|
const setMode = useCallback((nextEqualMode: boolean) => {
|
||||||
|
playClick(nextEqualMode ? 'equal-generations' : 'boomer-wave')
|
||||||
|
setEqualMode(nextEqualMode)
|
||||||
|
}, [playClick])
|
||||||
|
|
||||||
|
const handleSlider = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
playClick('timeline')
|
||||||
|
setRunning(false)
|
||||||
|
setYear(Number(event.currentTarget.value))
|
||||||
|
}, [playClick])
|
||||||
|
|
||||||
|
const finishProof = useCallback(() => {
|
||||||
|
if (completed) return
|
||||||
|
|
||||||
|
setCompleted(true)
|
||||||
|
playClick('complete')
|
||||||
|
onComplete({
|
||||||
|
success: true,
|
||||||
|
score: 100,
|
||||||
|
data: {
|
||||||
|
year,
|
||||||
|
mode: equalMode ? 'equal-generations' : 'boomer-wave',
|
||||||
|
generationWins,
|
||||||
|
currentPressure,
|
||||||
|
configId: config.id,
|
||||||
|
completedAt: new Date().toISOString()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [completed, config.id, currentPressure, equalMode, generationWins, onComplete, playClick, year])
|
||||||
|
|
||||||
|
const rootClassName = className ? `${styles.wrapper} ${className}` : styles.wrapper
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={rootClassName} style={themeStyle} aria-label="Demographic wave simulator">
|
||||||
|
<div className={styles.frame}>
|
||||||
|
<header className={styles.header}>
|
||||||
|
<div className={styles.controls} aria-label="Simulation controls">
|
||||||
|
<button className={styles.iconButton} type="button" onClick={() => setRunning((value) => !value)}>
|
||||||
|
{running ? 'II' : '>'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`${styles.modeButton} ${!equalMode ? styles.modeActive : ''}`}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setMode(false)}
|
||||||
|
>
|
||||||
|
W
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`${styles.modeButton} ${equalMode ? styles.modeActive : ''}`}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setMode(true)}
|
||||||
|
>
|
||||||
|
=
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className={styles.stageGrid}>
|
||||||
|
<div className={styles.wavePanel}>
|
||||||
|
<div className={styles.readoutRow}>
|
||||||
|
<div>
|
||||||
|
<strong className={styles.year}>{year}</strong>
|
||||||
|
</div>
|
||||||
|
<div className={styles.legend}>
|
||||||
|
{GENERATIONS.map((generation) => (
|
||||||
|
<span key={generation.key}>
|
||||||
|
<i className={styles[generation.key]} />
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.track}>
|
||||||
|
<div className={styles.ageMarks} aria-hidden="true">
|
||||||
|
<span>0</span>
|
||||||
|
<span>20</span>
|
||||||
|
<span>40</span>
|
||||||
|
<span>60</span>
|
||||||
|
<span>80</span>
|
||||||
|
</div>
|
||||||
|
<canvas ref={canvasRef} className={styles.canvas} />
|
||||||
|
<div className={styles.stageMarks} aria-hidden="true">
|
||||||
|
<span>Kids</span>
|
||||||
|
<span>Campus</span>
|
||||||
|
<span>Work</span>
|
||||||
|
<span>Assets</span>
|
||||||
|
<span>Care</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<aside className={styles.eventPanel}>
|
||||||
|
<h3>{activeStage.title}</h3>
|
||||||
|
<div className={styles.meter}>
|
||||||
|
<strong>{currentPressure}%</strong>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.timeline}>
|
||||||
|
<input
|
||||||
|
aria-label="Simulation year"
|
||||||
|
type="range"
|
||||||
|
min={startYear}
|
||||||
|
max={endYear}
|
||||||
|
value={year}
|
||||||
|
onChange={handleSlider}
|
||||||
|
/>
|
||||||
|
<strong>{progress}%</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section className={styles.proofPanel} aria-label="Accepted policy preference winners">
|
||||||
|
<div className={styles.proofHeader}>
|
||||||
|
<div>
|
||||||
|
<h3>Policy wins</h3>
|
||||||
|
</div>
|
||||||
|
<div className={styles.proofActions}>
|
||||||
|
<span className={styles.modePill}>{equalMode ? '=' : 'W'}</span>
|
||||||
|
<button className={styles.completeButton} type="button" onClick={finishProof} disabled={completed}>
|
||||||
|
{completed ? 'OK' : '✓'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.bars}>
|
||||||
|
{GENERATIONS.map((generation) => {
|
||||||
|
const percent = Math.round((generationWins[generation.key] / maxWins) * 100)
|
||||||
|
return (
|
||||||
|
<div className={styles.barRow} key={generation.key}>
|
||||||
|
<span>{generation.key === 'silent' ? 'S' : generation.key === 'boomer' ? 'B' : 'L'}</span>
|
||||||
|
<div className={styles.barTrack}>
|
||||||
|
<div
|
||||||
|
className={`${styles.barFill} ${styles[generation.key]}`}
|
||||||
|
style={{ width: `${percent}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<strong>{percent}</strong>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
@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);
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import { Leva, useControls } from 'leva'
|
||||||
|
import Component from './Component'
|
||||||
|
import { metadata } from './index'
|
||||||
|
import './dev-theme.css'
|
||||||
|
|
||||||
|
function DevHarness() {
|
||||||
|
const params = useControls({
|
||||||
|
startYear: { value: 1946, min: 1946, max: 2028, step: 1 },
|
||||||
|
endYear: { value: 2038, min: 1964, max: 2038, step: 1 },
|
||||||
|
animationSpeed: { value: 1, min: 0.25, max: 3, step: 0.25 },
|
||||||
|
waveScale: { value: 1.15, min: 0.2, max: 1.8, step: 0.05 },
|
||||||
|
startEqualized: false
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ minHeight: '100vh' }}>
|
||||||
|
<Leva hidden />
|
||||||
|
<Component
|
||||||
|
config={{
|
||||||
|
id: 'dev',
|
||||||
|
name: metadata.name,
|
||||||
|
version: metadata.version,
|
||||||
|
params: params as Record<string, unknown>
|
||||||
|
}}
|
||||||
|
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,70 @@
|
|||||||
|
import Component from './Component'
|
||||||
|
import type { GlitchComponentMetadata } from './types'
|
||||||
|
|
||||||
|
export default Component
|
||||||
|
|
||||||
|
export const metadata: GlitchComponentMetadata = {
|
||||||
|
name: 'demographic-wave',
|
||||||
|
displayName: 'Demographic Wave',
|
||||||
|
version: '1.0.0',
|
||||||
|
paramSchema: {
|
||||||
|
startYear: {
|
||||||
|
type: 'range',
|
||||||
|
label: 'Start Year',
|
||||||
|
default: 1946,
|
||||||
|
min: 1946,
|
||||||
|
max: 2028,
|
||||||
|
step: 1
|
||||||
|
},
|
||||||
|
endYear: {
|
||||||
|
type: 'range',
|
||||||
|
label: 'End Year',
|
||||||
|
default: 2038,
|
||||||
|
min: 1964,
|
||||||
|
max: 2038,
|
||||||
|
step: 1
|
||||||
|
},
|
||||||
|
animationSpeed: {
|
||||||
|
type: 'range',
|
||||||
|
label: 'Animation Speed',
|
||||||
|
default: 1,
|
||||||
|
min: 0.25,
|
||||||
|
max: 3,
|
||||||
|
step: 0.25
|
||||||
|
},
|
||||||
|
waveScale: {
|
||||||
|
type: 'range',
|
||||||
|
label: 'Boomer Wave Scale',
|
||||||
|
default: 1.15,
|
||||||
|
min: 0.2,
|
||||||
|
max: 1.8,
|
||||||
|
step: 0.05
|
||||||
|
},
|
||||||
|
startEqualized: {
|
||||||
|
type: 'boolean',
|
||||||
|
label: 'Start Equalized',
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
defaultParams: {
|
||||||
|
startYear: 1946,
|
||||||
|
endYear: 2038,
|
||||||
|
animationSpeed: 1,
|
||||||
|
waveScale: 1.15,
|
||||||
|
startEqualized: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type {
|
||||||
|
GlitchComponentProps,
|
||||||
|
GlitchComponentConfig,
|
||||||
|
GlitchComponentResult,
|
||||||
|
GlitchTheme,
|
||||||
|
GlitchHostBridge
|
||||||
|
} from './types'
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
type GCRegistry = Record<string, { default: typeof Component; metadata: GlitchComponentMetadata }>
|
||||||
|
;(window as unknown as { GlitchComponents: GCRegistry }).GlitchComponents ??= {}
|
||||||
|
;(window as unknown as { GlitchComponents: GCRegistry }).GlitchComponents[metadata.name] = { default: Component, metadata }
|
||||||
|
}
|
||||||
@@ -0,0 +1,494 @@
|
|||||||
|
.wrapper {
|
||||||
|
--gc-bg: var(--color-bg, #0a0a0f);
|
||||||
|
--gc-bg-secondary: var(--color-bg-secondary, #12121a);
|
||||||
|
--gc-text: var(--color-text, #e8e8ec);
|
||||||
|
--gc-text-muted: var(--color-text-muted, #9999a8);
|
||||||
|
--gc-primary: var(--color-primary, #6366f1);
|
||||||
|
--gc-accent: var(--color-accent, #22d3ee);
|
||||||
|
--gc-border: var(--color-border, #2a2a3a);
|
||||||
|
--gc-font-main: var(--font-main, system-ui, sans-serif);
|
||||||
|
--gc-font-display: var(--font-display, var(--gc-font-main));
|
||||||
|
--gc-font-mono: var(--font-mono, monospace);
|
||||||
|
--gc-shell-padding: clamp(8px, 2vw, 18px);
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100dvh;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: var(--gc-shell-padding);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper *,
|
||||||
|
.wrapper *::before,
|
||||||
|
.wrapper *::after {
|
||||||
|
box-sizing: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame {
|
||||||
|
position: relative;
|
||||||
|
width: min(100%, calc((100dvh - (var(--gc-shell-padding) * 2)) * 16 / 9));
|
||||||
|
max-height: calc(100dvh - (var(--gc-shell-padding) * 2));
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto minmax(0, 1fr) auto auto;
|
||||||
|
gap: 0.5em;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 1em;
|
||||||
|
color: var(--gc-text);
|
||||||
|
font-family: var(--gc-font-main);
|
||||||
|
font-size: clamp(10px, 1.12cqw, 18px);
|
||||||
|
line-height: 1.25;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 12% 10%, color-mix(in srgb, var(--gc-primary) 26%, transparent), transparent 34%),
|
||||||
|
radial-gradient(circle at 86% 16%, color-mix(in srgb, var(--gc-accent) 18%, transparent), transparent 30%),
|
||||||
|
linear-gradient(145deg, var(--gc-bg), var(--gc-bg-secondary));
|
||||||
|
border: 1px solid color-mix(in srgb, var(--gc-border) 74%, transparent);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 1px rgb(255 255 255 / 4%),
|
||||||
|
0 18px 48px rgb(0 0 0 / 34%);
|
||||||
|
container-type: inline-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header,
|
||||||
|
.stageGrid,
|
||||||
|
.readoutRow,
|
||||||
|
.proofHeader,
|
||||||
|
.timeline,
|
||||||
|
.controls,
|
||||||
|
.legend,
|
||||||
|
.meter {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header,
|
||||||
|
.readoutRow,
|
||||||
|
.proofHeader,
|
||||||
|
.meter {
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: block;
|
||||||
|
margin: 0 0 0.25em;
|
||||||
|
color: color-mix(in srgb, var(--gc-accent) 80%, white);
|
||||||
|
font-family: var(--gc-font-mono);
|
||||||
|
font-size: 0.62em;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
gap: 0.45em;
|
||||||
|
justify-content: stretch;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconButton,
|
||||||
|
.modeButton,
|
||||||
|
.completeButton {
|
||||||
|
border: 1px solid color-mix(in srgb, var(--gc-border) 78%, transparent);
|
||||||
|
color: var(--gc-text);
|
||||||
|
font: inherit;
|
||||||
|
font-family: var(--gc-font-mono);
|
||||||
|
font-size: 0.68em;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 120ms ease, border-color 120ms ease, background-color 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconButton:hover,
|
||||||
|
.modeButton:hover,
|
||||||
|
.completeButton:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
border-color: color-mix(in srgb, var(--gc-accent) 62%, var(--gc-border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconButton {
|
||||||
|
min-width: 3.7em;
|
||||||
|
min-height: 2.75em;
|
||||||
|
background: rgb(10 14 24 / 72%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modeButton {
|
||||||
|
min-height: 2.75em;
|
||||||
|
padding: 0 0.85em;
|
||||||
|
background: rgb(10 14 24 / 58%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modeActive {
|
||||||
|
color: #090b10;
|
||||||
|
background: linear-gradient(90deg, #e7b84f, color-mix(in srgb, var(--gc-accent) 58%, #e7b84f));
|
||||||
|
border-color: rgb(255 255 255 / 56%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stageGrid {
|
||||||
|
min-height: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1.36fr) minmax(12em, 0.64fr);
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 0.65em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wavePanel,
|
||||||
|
.eventPanel,
|
||||||
|
.proofPanel,
|
||||||
|
.timeline {
|
||||||
|
min-width: 0;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--gc-border) 76%, transparent);
|
||||||
|
background: rgb(5 9 18 / 54%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wavePanel {
|
||||||
|
min-height: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto minmax(0, 1fr);
|
||||||
|
gap: 0.45em;
|
||||||
|
padding: 0.7em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.year {
|
||||||
|
display: block;
|
||||||
|
font-family: var(--gc-font-display);
|
||||||
|
font-size: 1.85em;
|
||||||
|
line-height: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend {
|
||||||
|
gap: 0.4em;
|
||||||
|
justify-content: end;
|
||||||
|
color: var(--gc-text-muted);
|
||||||
|
font-family: var(--gc-font-mono);
|
||||||
|
font-size: 0.62em;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend i {
|
||||||
|
display: inline-block;
|
||||||
|
width: 0.8em;
|
||||||
|
height: 0.8em;
|
||||||
|
margin-right: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track {
|
||||||
|
position: relative;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--gc-border) 70%, transparent);
|
||||||
|
background:
|
||||||
|
linear-gradient(90deg, rgb(113 166 210 / 6%), transparent 32%, rgb(231 184 79 / 8%) 62%, rgb(126 224 160 / 7%)),
|
||||||
|
rgb(7 11 17 / 78%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ageMarks,
|
||||||
|
.stageMarks {
|
||||||
|
position: absolute;
|
||||||
|
left: 7%;
|
||||||
|
right: 7%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||||
|
color: rgb(232 232 236 / 60%);
|
||||||
|
font-family: var(--gc-font-mono);
|
||||||
|
font-size: 0.58em;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
pointer-events: none;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ageMarks {
|
||||||
|
top: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stageMarks {
|
||||||
|
bottom: 0.75em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eventPanel {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 1em;
|
||||||
|
padding: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eventPanel h3,
|
||||||
|
.proofHeader h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--gc-text);
|
||||||
|
font-family: var(--gc-font-display);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eventPanel h3 {
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eventPanel p {
|
||||||
|
display: none;
|
||||||
|
margin: 0.65em 0 0;
|
||||||
|
color: color-mix(in srgb, var(--gc-text-muted) 86%, white);
|
||||||
|
font-size: 0.88em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meter {
|
||||||
|
margin-top: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
border-top: 0;
|
||||||
|
color: var(--gc-text-muted);
|
||||||
|
font-family: var(--gc-font-mono);
|
||||||
|
font-size: 0.7em;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meter strong {
|
||||||
|
color: #e7b84f;
|
||||||
|
font-family: var(--gc-font-display);
|
||||||
|
font-size: 2.2em;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 3.5em;
|
||||||
|
gap: 0.8em;
|
||||||
|
padding: 0.62em 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline .label {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline input {
|
||||||
|
width: 100%;
|
||||||
|
accent-color: #e7b84f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline strong {
|
||||||
|
text-align: right;
|
||||||
|
font-family: var(--gc-font-mono);
|
||||||
|
font-size: 0.72em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proofPanel {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
gap: 0.8em;
|
||||||
|
padding: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proofHeader {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proofHeader h3 {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modePill {
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--gc-border) 72%, transparent);
|
||||||
|
min-width: 3em;
|
||||||
|
padding: 0.45em 0.7em;
|
||||||
|
color: var(--gc-text-muted);
|
||||||
|
font-family: var(--gc-font-mono);
|
||||||
|
font-size: 0.62em;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proofActions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.45em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bars {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.45em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.barRow {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.3em minmax(0, 1fr) 2.2em;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.65em;
|
||||||
|
color: var(--gc-text-muted);
|
||||||
|
font-family: var(--gc-font-mono);
|
||||||
|
font-size: 0.68em;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.barTrack {
|
||||||
|
height: 1.35em;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--gc-border) 76%, transparent);
|
||||||
|
background: rgb(3 6 12 / 72%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.barFill {
|
||||||
|
height: 100%;
|
||||||
|
transition: width 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.barRow strong {
|
||||||
|
color: var(--gc-text);
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.silent {
|
||||||
|
background: #71a6d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boomer {
|
||||||
|
background: #e7b84f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post {
|
||||||
|
background: #7ee0a0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.completeButton {
|
||||||
|
min-height: 2.35em;
|
||||||
|
padding: 0 0.8em;
|
||||||
|
background: color-mix(in srgb, var(--gc-primary) 28%, rgb(5 9 18));
|
||||||
|
}
|
||||||
|
|
||||||
|
.completeButton:disabled {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.68;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (orientation: portrait) {
|
||||||
|
.frame {
|
||||||
|
width: min(100%, calc((100dvh - (var(--gc-shell-padding) * 2)) / 2));
|
||||||
|
aspect-ratio: 1 / 2;
|
||||||
|
grid-template-rows: auto minmax(0, 1.24fr) auto minmax(0, 0.56fr);
|
||||||
|
font-size: clamp(10px, 3.2cqw, 16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.55em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconButton,
|
||||||
|
.modeButton {
|
||||||
|
min-height: 2.35em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stageGrid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: minmax(0, 1.25fr) auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wavePanel {
|
||||||
|
padding: 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.readoutRow {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.45em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend {
|
||||||
|
justify-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eventPanel {
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0.72em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eventPanel h3 {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eventPanel p {
|
||||||
|
margin-top: 0.42em;
|
||||||
|
font-size: 0.78em;
|
||||||
|
line-height: 1.22;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meter {
|
||||||
|
padding-top: 0.48em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meter strong {
|
||||||
|
font-size: 1.7em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stageMarks {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proofPanel {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0.5em;
|
||||||
|
padding: 0.65em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proofHeader {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.45em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proofActions {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proofHeader h3 {
|
||||||
|
font-size: 0.82em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.barRow {
|
||||||
|
grid-template-columns: 1fr 2.2em;
|
||||||
|
gap: 0.32em;
|
||||||
|
font-size: 0.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.barRow span {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.barTrack {
|
||||||
|
height: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.completeButton {
|
||||||
|
min-height: 2.35em;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
}
|
||||||
Vendored
+1
@@ -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,32 @@
|
|||||||
|
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: 'GlitchComponent',
|
||||||
|
fileName: 'demographic-wave',
|
||||||
|
formats: ['iife']
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: ['react', 'react-dom'],
|
||||||
|
output: {
|
||||||
|
globals: {
|
||||||
|
react: 'React',
|
||||||
|
'react-dom': 'ReactDOM'
|
||||||
|
},
|
||||||
|
assetFileNames: 'assets/[name][extname]'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sourcemap: true,
|
||||||
|
minify: mode === 'production'
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 3001,
|
||||||
|
open: true
|
||||||
|
}
|
||||||
|
}))
|
||||||
Reference in New Issue
Block a user