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}