app.jsx

3.1 kB · jsx · 96 lines

1import { useEffect, useState } from 'react';2import { createRoot } from 'react-dom/client';3import { Shell } from '../kit/ui/chrome.jsx';4import { demos, load, sidebar } from './tree.js';5import site from './site.js';67export function mount(node) {8  createRoot(document.getElementById('root')).render(node);9}1011export function useShelves() {12  const [shelves, set] = useState(demos);13  useEffect(() => {14    let live = true;15    if (!shelves.length) load().then((list) => live && set(list));16    return () => { live = false; };17  }, [shelves.length]);18  return shelves;19}2021const READS = `${site.prefix}reads`;2223function reads() {24  const el = typeof document === 'undefined' ? null : document.getElementById(READS);25  return el ? JSON.parse(el.textContent) : [];26}2728export function Page({ crumb, title, sub, foot, bare, controls, contents, children }) {29  const shelves = useShelves();30  const nodes = sidebar({ demos: shelves });31  const pages = reads();32  return (33    <Shell route={`/demos/${crumb}/`} title={bare ? undefined : title} lead={sub} tree={nodes} controls={controls} contents={contents} wide>34      {children}35      {foot && <p className="foot" hidden={bare}>{foot}</p>}36      {pages.length > 0 && <p className="reads" hidden={bare}>Read: {pages.map((page, i) => <span key={page.href}>{i > 0 && ', '}<a href={page.href}>{page.name}</a></span>)}.</p>}37    </Shell>38  );39}4041export function Row({ hidden, children }) {42  return <div className="row" hidden={hidden}>{children}</div>;43}4445export function Group({ name, hidden, children }) {46  return (47    <section aria-label={name} hidden={hidden}>48      <h3>{name}</h3>49      <Row>{children}</Row>50    </section>51  );52}5354const pair = (option) => (Array.isArray(option) ? option : [option, option]);5556export function Pick({ label, value, onChange, options }) {57  return (58    <label>{label} <select value={value} onChange={(e) => onChange(e.target.value)}>59      {options.map(pair).map(([v, text]) => <option key={v} value={v}>{text}</option>)}60    </select></label>61  );62}6364export function Slider({ label, value, onChange, min, max, step = 1, show }) {65  return (66    <label>{label} <input type="range" min={min} max={max} step={step} value={value} onChange={(e) => onChange(+e.target.value)} /><span className="num">{show ?? value}</span></label>67  );68}6970export function Text({ label, value, onChange, wide }) {71  return (72    <label>{label} <input type="text" className={wide ? 'wide' : undefined} value={value} onChange={(e) => onChange(e.target.value)} /></label>73  );74}7576export function Check({ label, checked, onChange }) {77  return (78    <label><input type="checkbox" checked={checked} onChange={(e) => onChange(e.target.checked)} /> {label}</label>79  );80}8182export function Btn({ primary, on, onClick, children }) {83  return <button className={primary ? 'primary' : on ? 'on' : undefined} onClick={onClick}>{children}</button>;84}8586export function Stats({ children }) {87  return <div className="stats">{children}</div>;88}8990export function Stat({ label, children }) {91  return <span>{label} <b>{children}</b></span>;92}9394export function Note({ error, children }) {95  return <div className="note">{error ? String(error.message ?? error) : children}</div>;96}