chrome.jsx
9.1 kB · jsx · 302 lines
1import { useEffect } from 'react';2import { letters } from './font.js';3import { wire } from './chrome.js';4import { conf, HUES } from './config.js';56/* GLYPHS */78function Cells({ rows, cols, grid, className, label, fill, children }) {9 const cells = [];10 grid.forEach((row, y) =>11 row.forEach((on, x) => {12 if (!on && !fill) return;13 const i = y * cols + x;14 cells.push(<rect key={i} className={on ? undefined : 'ink'} style={on ? undefined : { '--i': i }} x={x} y={y} width={1} height={1} />);15 }),16 );17 return (18 <svg className={className ? `glyphs ${className}` : 'glyphs'} viewBox={`0 0 ${cols} ${rows}`} role={label ? 'img' : undefined} aria-label={label} aria-hidden={label ? undefined : true}>19 {cells}20 {children}21 </svg>22 );23}2425export function Glyph({ text, className, label }) {26 return <Cells {...letters(text)} className={className} label={label} />;27}2829const pixels = (rows) => rows.map((row) => [...row].map((c) => (c === '1' ? 1 : 0)));3031const PANEL = {32 left: ['11111', '11001', '11001', '11001', '11111'],33 right: ['11111', '10011', '10011', '10011', '11111'],34};3536function Panel({ side }) {37 return <Cells rows={5} cols={5} grid={pixels(PANEL[side])} fill />;38}3940function Ring() {41 const dots = [];42 for (let y = 1; y <= 3; y++) for (let x = 1; x <= 3; x++) dots.push(<rect key={`${x}.${y}`} className="dot" x={x} y={y} width={1} height={1} />);43 return <Cells {...letters('O')}>{dots}</Cells>;44}4546function Wordmark({ className }) {47 return <Glyph text={conf().title.toUpperCase()} className={className} />;48}4950/* HEADER */5152function Header() {53 const site = conf();54 return (55 <header className="top">56 <a className="glyph" href={site.menu} aria-label="Menu">57 <Glyph text="+" />58 </a>59 <a className="mark" href="/" aria-label={`${site.title} home`}>60 <Wordmark />61 </a>62 <a className="glyph" href={site.cart} data-cart aria-label="Cart">63 <Ring />64 </a>65 </header>66 );67}6869function Dock({ route = '/' }) {70 const word = decodeURIComponent(route).split('/').filter(Boolean).pop() ?? 'home';71 return (72 <div className="dock">73 <button type="button" className="glyph" data-pane="left" aria-controls="left" aria-expanded="false" aria-label="Site tree">74 <Panel side="left" />75 </button>76 <span className="route">77 <Glyph text={word.toUpperCase()} label={word} />78 </span>79 <button type="button" className="glyph" data-pane="right" aria-controls="right" aria-expanded="false" aria-label="Page tools">80 <Panel side="right" />81 </button>82 </div>83 );84}8586/* TREE */8788const holds = (node, current) => node.href === current || (node.nodes ?? []).some((sub) => holds(sub, current));8990function Leaf({ node, here }) {91 return (92 <a href={node.href} aria-current={here}>93 {node.icon && <span className={node.icon} aria-hidden="true"></span>}94 {node.icon ? <span className="name">{node.name}</span> : node.name}95 </a>96 );97}9899function Node({ node, current }) {100 const here = node.href === current ? 'page' : undefined;101 const lazy = node.lazy !== undefined;102 if (!node.nodes && !lazy) return <li><Leaf node={node} here={here} /></li>;103 return (104 <li>105 <details open={holds(node, current) || undefined} data-lazy={lazy ? node.lazy : undefined}>106 <summary>{node.href ? <Leaf node={node} here={here} /> : node.name}</summary>107 <ul>{(node.nodes ?? []).map((sub) => <Node key={sub.name} node={sub} current={current} />)}</ul>108 </details>109 </li>110 );111}112113const EXPLORER = '/git/tree.json';114115const hasLazy = (nodes) => nodes.some((node) => node.lazy !== undefined || hasLazy(node.nodes ?? []));116117function Tree({ nodes = [], current = '' }) {118 const source = hasLazy(nodes) ? EXPLORER : undefined;119 return <ul className="tree" data-source={source}>{nodes.map((node) => <Node key={node.name} node={node} current={current} />)}</ul>;120}121122/* MENU */123124function Dates({ dates }) {125 if (!dates?.length) return null;126 return <p className="dates">{dates.map((date) => <span key={date}>{date}</span>)}</p>;127}128129function Card({ node }) {130 if (!node.figure) return <a className="tile plain" href={node.href}><h2>{node.name}</h2>{node.text && <p>{node.text}</p>}<Dates dates={node.dates} /></a>;131 return (132 <a className="tile" href={node.href}>133 <img className="dark" src={node.figure.dark} alt="" width="1024" height="1024" loading="lazy" decoding="async" />134 <img className="light" src={node.figure.light} alt="" width="1024" height="1024" loading="lazy" decoding="async" />135 <h2>{node.name}</h2>136 {node.text && <p>{node.text}</p>}137 <Dates dates={node.dates} />138 </a>139 );140}141142const leaves = (nodes) => nodes.filter((node) => node.href && !(node.nodes && node.nodes.length));143144const groups = (nodes) => nodes.filter((node) => node.nodes && node.nodes.length);145146export function Grid({ nodes = [] }) {147 const list = leaves(nodes);148 if (!list.length) return null;149 return <div className="gallery grid">{list.map((node) => <Card key={node.href} node={node} />)}</div>;150}151152export function Menu({ tree = [] }) {153 const pages = leaves(tree);154 return (155 <div className="menu">156 {groups(tree).map((group) => (157 <section key={group.name} aria-label={group.name}>158 <h2>{group.href ? <a href={group.href}>{group.name}</a> : group.name}</h2>159 <Grid nodes={group.nodes} />160 {groups(group.nodes).map((shelf) => (161 <div key={shelf.name} className="shelf">162 <h3>{shelf.href ? <a href={shelf.href}>{shelf.name}</a> : shelf.name}</h3>163 <Grid nodes={shelf.nodes} />164 </div>165 ))}166 </section>167 ))}168 {pages.length > 0 && (169 <section aria-label="Pages">170 <h2>Pages</h2>171 <Grid nodes={pages} />172 </section>173 )}174 </div>175 );176}177178/* CONTENTS */179180function Contents({ items = [] }) {181 return (182 <nav className="contents" aria-label="Contents">183 <h2>Contents</h2>184 <ol>185 {items.map((item) => (186 <li key={item.id} className={`h${item.level ?? 2}`}>187 <a href={`#${item.id}`}>{item.text}</a>188 </li>189 ))}190 </ol>191 </nav>192 );193}194195function Controls({ children }) {196 return <section className="controls" aria-label="Controls">{children}</section>;197}198199const FACES = [200 ['', 'System'],201 ['sans', 'Noto Sans'],202 ['serif', 'Noto Serif'],203 ['mono', 'Noto Sans Mono'],204 ['mrly', 'MrlyFont'],205];206207const TINTS = [['', 'Auto'], ...HUES.map((hue) => [hue, hue[0].toUpperCase() + hue.slice(1)])];208209const SCREENS = [210 ['', 'Wordmark'],211 ['matrix', 'Matrix'],212 ['sleep', 'Sleep'],213 ['mandelbrot', 'Mandelbrot'],214 ['julia', 'Julia'],215];216217function Pick({ label, name, options }) {218 return (219 <label className="pick">220 {label}221 <select {...{ [name]: true }} defaultValue="">222 {options.map(([value, text]) => <option key={value} value={value}>{text}</option>)}223 </select>224 </label>225 );226}227228function Settings() {229 return (230 <section className="settings" aria-label="Settings">231 <h2>Settings</h2>232 <div className="row">233 <button type="button" className="theme" data-theme-toggle>Theme <b>auto</b></button>234 <Pick label="Font" name="data-font-pick" options={FACES} />235 <Pick label="Tint" name="data-tint-pick" options={TINTS} />236 <Pick label="Saver" name="data-saver-pick" options={SCREENS} />237 </div>238 </section>239 );240}241242/* FOOTER */243244function Mark() {245 const site = conf();246 const text = site.title.toUpperCase();247 const { rows, cols } = letters(text);248 return (249 <>250 <canvas className="mark" width={cols + 2} height={rows + 2} data-text={text} role="img" aria-label={site.title}></canvas>251 <Wordmark className="still" />252 </>253 );254}255256function Footer() {257 const site = conf();258 const year = new Date().getFullYear();259 const span = site.since < year ? `${site.since}-${year}` : String(year);260 return (261 <footer className="base">262 <a href="/" aria-label={`${site.title} home`}><Mark /></a>263 <p className="legal fine">Copyright © {site.company || site.title} {span}. All rights reserved.</p>264 </footer>265 );266}267268/* SHELL */269270export function Shell({ route = '/', title, lead, tree = [], current = route, contents = [], controls, wide = false, children }) {271 useEffect(() => {272 wire();273 }, []);274 return (275 <>276 <a className="skip" href="#main">Skip to content</a>277 <Header />278 <Dock route={route} />279 <div className="panes">280 <nav className="pane left" id="left" aria-label="Site">281 <Tree nodes={tree} current={current} />282 </nav>283 <main id="main" tabIndex={-1} className={wide ? 'wide' : undefined}>284 {title && (285 <div className="lede">286 <h1>{title}</h1>287 {lead && <p className="lead">{lead}</p>}288 </div>289 )}290 {children}291 </main>292 <aside className="pane right" id="right" aria-label="Page tools">293 {controls && <Controls>{controls}</Controls>}294 {contents.length > 0 && <Contents items={contents} />}295 <Settings />296 </aside>297 <div className="scrim"></div>298 </div>299 <Footer />300 </>301 );302}