Repository navigation
feat: the site demonstrates order
#26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| import { useMemo, useState } from "react"; | ||
| import { Matrix } from "./Matrix.js"; | ||
| import { m } from "../i18n.js"; | ||
| import { chevron, nearestCell, ringsFrom } from "../traversals.js"; | ||
| // The source of the traversals file, at build time. Shown beside the field so the code a | ||
| // visitor reads IS the code that is running — a hand-kept copy of a function is a copy that | ||
| // lies the first time anyone edits one of them. | ||
| import traversalSource from "../traversals.ts?raw"; | ||
|
|
||
| // The file, from the first export onward. The header comment explains WHY these exist, which | ||
| // is for us; a visitor's first question is what to copy, and scrolling past five paragraphs | ||
| // of rationale to reach it is the wrong first impression of a code sample. | ||
| const COPYABLE = traversalSource.slice(traversalSource.indexOf("export")); | ||
|
|
||
| const N = 9; | ||
|
|
||
| /** | ||
| * The `order` seam, on the page. | ||
| * | ||
| * A capability the page cannot demonstrate is a capability that quietly stops working, and | ||
| * this one has a specific hazard: `order` is a FUNCTION, so it cannot go in the copied | ||
| * snippet. A visitor who reproduced the settings from the copy button would get the preset's | ||
| * motion and no indication why. So this demo is deliberately separate from the playground — | ||
| * its own field, its own settings, its own copy button that copies the CODE — and the code is | ||
| * right there to paste. | ||
| * | ||
| * Two traversals, because they answer two different questions: | ||
| * | ||
| * chevron the SHAPE of the traversal can be anything you can compute per cell, including | ||
| * an easing along its own axis — which is what "accelerating" actually is. | ||
| * rings the ORIGIN can be anywhere, which the library's concentric preset pins to the | ||
| * middle. Click the field to move it. | ||
| */ | ||
| export function TraversalDemo() { | ||
| const [variant, setVariant] = useState<"chevron" | "rings">("chevron"); | ||
| const [origin, setOrigin] = useState({ col: 4, row: 4 }); | ||
|
|
||
| // rebuilt whenever the origin moves, and the field below is keyed on it so the engine | ||
| // re-ranks rather than remounting — the same path a real caller takes | ||
| const order = useMemo( | ||
| () => (variant === "chevron" ? chevron : ringsFrom(origin)), | ||
| [variant, origin], | ||
| ); | ||
|
|
||
| return ( | ||
| <div className="traversal-demo"> | ||
| <div className="traversal-demo-head"> | ||
| <div className="traversal-choices" role="group" aria-label={m.demo_traversal_which()}> | ||
| <button | ||
| type="button" | ||
| className={variant === "chevron" ? "is-on" : undefined} | ||
| aria-pressed={variant === "chevron"} | ||
| onClick={() => setVariant("chevron")} | ||
| > | ||
| {m.demo_traversal_chevron()} | ||
| </button> | ||
| <button | ||
| type="button" | ||
| className={variant === "rings" ? "is-on" : undefined} | ||
| aria-pressed={variant === "rings"} | ||
| onClick={() => setVariant("rings")} | ||
| > | ||
| {m.demo_traversal_rings()} | ||
| </button> | ||
| </div> | ||
| {variant === "rings" && <p className="traversal-hint">{m.demo_traversal_click()}</p>} | ||
| </div> | ||
|
|
||
| <div className="traversal-demo-body"> | ||
| <div | ||
| className="traversal-field" | ||
| onClick={(e) => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '1,180p' apps/site/src/components/TraversalDemo.tsx
sed -n '1,110p' apps/site/src/components/Matrix.tsxRepository: BotHarness/BotUI Length of output: 8740 Provide keyboard access to rings origin selection. When Add labelled row and column controls, or make the field focusable with arrow-key selection and a visible focus indicator. Enter/Space activation alone would not let users choose different cells. 🤖 Prompt for AI Agents |
||
| if (variant !== "rings") return; | ||
| const box = e.currentTarget.getBoundingClientRect(); | ||
| // the field is drawn from its centre outward, in the engine's own px geometry | ||
| const pitch = box.width / N; | ||
| setOrigin( | ||
| nearestCell( | ||
| { | ||
| x: e.clientX - box.left - box.width / 2, | ||
| y: e.clientY - box.top - box.height / 2, | ||
| }, | ||
| { x: pitch, y: pitch }, | ||
| N, | ||
| N, | ||
| ), | ||
| ); | ||
| }} | ||
| > | ||
| <Matrix | ||
| cols={N} | ||
| rows={N} | ||
| size={260} | ||
| silhouette="square" | ||
| dot="circle" | ||
| dotSize={0.8} | ||
| gapX={0.12} | ||
| gapY={0.12} | ||
| preset={variant === "chevron" ? "morph" : "ripple"} | ||
| renderer="css" | ||
| order={order} | ||
| /> | ||
| {variant === "rings" && ( | ||
| <span | ||
| className="traversal-origin" | ||
| style={{ | ||
| left: `${(origin.col / (N - 1)) * 100}%`, | ||
| top: `${(origin.row / (N - 1)) * 100}%`, | ||
| }} | ||
| aria-hidden="true" | ||
| /> | ||
| )} | ||
| </div> | ||
|
|
||
| <div className="traversal-code"> | ||
| {/* No caption here: the section's lede already says what these are, and a second | ||
| copy of the same sentence one inch away is the thing people stop reading. */} | ||
| <pre> | ||
| <code>{COPYABLE}</code> | ||
| </pre> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| import src from "./traversals.ts?raw"; | ||
| export const hasRaw = typeof src === "string" && src.includes("chevron"); |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,82 @@ | ||||||||||
| import type { OrderFn } from "@botharness/botui-core"; | ||||||||||
|
|
||||||||||
| /** | ||||||||||
| * Two traversals the library does not ship, written the way a reader would write them. | ||||||||||
| * | ||||||||||
| * These exist to demonstrate the `order` seam on the page rather than only in a README: a | ||||||||||
| * capability nobody can see is a capability that quietly stops working. Both are ordinary | ||||||||||
| * `OrderFn`s — a function of lattice position returning `0…1`, where `0` leads. | ||||||||||
| * | ||||||||||
| * The source of this file is shown verbatim beside the field, imported with `?raw`, so the | ||||||||||
| * code a visitor reads cannot drift from the code that is running. A hand-maintained copy | ||||||||||
| * of a function is a copy that lies the first time someone edits one of them. | ||||||||||
| */ | ||||||||||
|
|
||||||||||
| /** | ||||||||||
| * A CHEVRON that accelerates toward its tips. | ||||||||||
| * | ||||||||||
| * "Accelerating" is not a property of the traversal's SHAPE but of how the value is | ||||||||||
| * distributed along it: squaring compresses the early differences and stretches the late | ||||||||||
| * ones, so successive dots along each arm start closer together in phase near the tip. A | ||||||||||
| * linear chevron is a chevron; an eased one is a chevron that moves like it is going | ||||||||||
| * somewhere. | ||||||||||
| * | ||||||||||
| * The value mixes distance from the spine with progress along the field, which is what | ||||||||||
| * makes it fold rather than simply translate. | ||||||||||
| */ | ||||||||||
| export const chevron: OrderFn = (col, row, cols, rows) => { | ||||||||||
| const mid = (rows - 1) / 2; | ||||||||||
| // 0 at the spine, 1 at the outer tips. `|| 1` because a single row has no mid to divide by | ||||||||||
| const arm = Math.abs(row - mid) / (mid || 1); | ||||||||||
| const along = col / (cols - 1 || 1); | ||||||||||
| const v = arm * 0.6 + along * 0.4; | ||||||||||
| // ease-in | ||||||||||
| return v * v; | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| /** | ||||||||||
| * RINGS expanding from a dot the visitor chooses. | ||||||||||
| * | ||||||||||
| * Chebyshev distance — `max(|dc|, |dr|)` — rather than Euclidean, and that choice is the | ||||||||||
| * whole character: on an integer lattice the Chebyshev radius takes only a handful of | ||||||||||
| * distinct values, so every dot on a ring shares one phase and the field reads as discrete | ||||||||||
| * bands. Euclidean would give nearly every dot its own value and turn the same traversal | ||||||||||
| * into a smooth travelling wave. The concentric `ring` preset is this function with the | ||||||||||
| * origin pinned to the middle; here the origin is a parameter, which is the request the | ||||||||||
| * library's own table could not express. | ||||||||||
| * | ||||||||||
| * Normalised by the largest distance the lattice can hold, so the outermost dot leads at 1 | ||||||||||
| * whatever the grid size — otherwise a 9×9 and a 3×3 would animate at different rates for | ||||||||||
| * the same nominal shape. | ||||||||||
| */ | ||||||||||
| export function ringsFrom(origin: { col: number; row: number }): OrderFn { | ||||||||||
| return (col, row, cols, rows) => { | ||||||||||
| const d = Math.max(Math.abs(col - origin.col), Math.abs(row - origin.row)); | ||||||||||
| // The furthest cell in the lattice FROM THE ORIGIN, which is whichever corner is | ||||||||||
| // diagonally opposite it. Normalising by a fixed denominator — `cols`, or | ||||||||||
| // `max(cols, rows)` — is the tempting one-liner and it is wrong twice over: it makes | ||||||||||
| // the outermost dot's value depend on the GRID SIZE rather than on the origin, so a | ||||||||||
| // 9×9 and a 3×3 animate at different rates for the same nominal shape, and it leaves | ||||||||||
| // the last ring short of 1, so the wave never quite arrives. | ||||||||||
| const far = Math.max(origin.col, cols - 1 - origin.col, origin.row, rows - 1 - origin.row); | ||||||||||
| return far > 0 ? Math.min(1, d / far) : 0; | ||||||||||
| }; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| /** the cell nearest a click, so a visitor can pick the origin with a pointer */ | ||||||||||
| export function nearestCell( | ||||||||||
| at: { x: number; y: number }, | ||||||||||
| pitch: { x: number; y: number }, | ||||||||||
| cols: number, | ||||||||||
| rows: number, | ||||||||||
| ): { col: number; row: number } { | ||||||||||
| // the field is centred, so a click at (0,0) in field space is the top-left cell | ||||||||||
| const cx = (cols - 1) / 2; | ||||||||||
| const cy = (rows - 1) / 2; | ||||||||||
| const col = Math.round(cx + (at.x / pitch.x) * cx); | ||||||||||
| const row = Math.round(cy + (at.y / pitch.y) * cy); | ||||||||||
|
Comment on lines
+76
to
+77
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Remove the extra half-grid multiplier from click mapping. The caller supplies cell pitch as For a 9×9 field, a click one pitch right of centre selects column 8 instead of column 5. The same error affects rows. The existing tests check only the centre and clamped edges, which hide this error. Proposed correction- const col = Math.round(cx + (at.x / pitch.x) * cx);
- const row = Math.round(cy + (at.y / pitch.y) * cy);
+ const col = Math.round(cx + at.x / pitch.x);
+ const row = Math.round(cy + at.y / pitch.y);Add an interior-position assertion for a displacement of one pitch. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
| return { | ||||||||||
| col: Math.min(cols - 1, Math.max(0, col)), | ||||||||||
| row: Math.min(rows - 1, Math.max(0, row)), | ||||||||||
| }; | ||||||||||
| } | ||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Keep the required type import in the displayed source.
COPYABLEremovesimport type { OrderFn } from "@botharness/botui-core", but the displayed declarations still referenceOrderFn. Pasting this source into a new TypeScript module leaves that type unresolved. Imported types are module-scoped. (typescriptlang.org)Start the displayed source at the import, or prepend the required import. Update the source-display test so it requires that import instead of excluding it.
🤖 Prompt for AI Agents