Small, experimental web widgets — quickly generated by AI to visualise or demonstrate an idea. Each is an independent, iframe-embeddable Vite SPA, deployed automatically to its own subdomain on Cloudflare Workers via GitHub Actions, and designed to be embedded in any page without coordination with the host origin. Treat them as quick demos, not production-grade components.
| Widget | Live URL | Description |
|---|---|---|
| function-plotter | https://function-plotter.widgets.beshir.org | Plot any function of x as an SVG curve. |
| image-comparison-table | https://image-comparison-table.widgets.beshir.org | Side-by-side comparison grids of labelled images, with click-to-zoom lightbox and per-row prompt info. |
| japanese-verb-tower | https://japanese-verb-tower.widgets.beshir.org | Assemble a Japanese verb morpheme-by-morpheme through its conjugation layers; search 26,784 verbs by romaji, kana, or kanji. |
| pennsic-planner | https://pennsic-planner.widgets.beshir.org | Browse the Pennsic 53 (2026) class schedule and build a shareable personal calendar with conflict detection and .ics export. |
<iframe src="https://function-plotter.widgets.beshir.org" loading="lazy" style="width:100%;height:480px;border:0"></iframe>- Runtime: Preact core (not React). Tiny, framework-agnostic libraries pair well with it; React-only packages should generally be avoided unless they justify the
preact/compatcost. - Charts: Observable Plot (
@observablehq/plot) as the default for chart-like widgets — grammar-of-graphics, SVG output, no CDN required. - Don't optimize for bundle size. Pick whatever makes the best widget; ~400 KB gzip is a comfortable ceiling, and larger is fine when it's the best fit (show a loading state if it's big or waits on data). Reserve caution for true heavyweights like Plotly or MathJax.
- See
LIBRARIES.mdfor the curated library menu, andDATA.mdfor the per-widget data contract (static/prebake/live). - Widgets must render offline in dev and the render check — bundle the code, and data widgets ship a
sample(seeDATA.md). In production a widget may fetch at runtime (live data, map tiles); prefer bundling for reliability and give remote data a loading state.
Each widget lives under widgets/<slug>/ and contains:
package.json— npm manifest with build scriptspackage-lock.json— lockfile committed to the repowidget.json— metadata: slug, title, description, build config, Cloudflare routingwrangler.jsonc— Cloudflare Workers deployment config
The slug determines the worker name (widget-<slug>) and the hostname (<slug>.widgets.beshir.org).
Pushes to main trigger .github/workflows/deploy-widgets.yml, which discovers all widget directories, builds each one with npm ci && npm run build, and deploys via wrangler deploy. Pull requests trigger .github/workflows/check-widgets.yml, which validates all widget.json/wrangler.jsonc configs and builds each widget without deploying.
- Create a Cloudflare API token with account scope
Workers Scripts: Editplus zone scope (beshir.org)Workers Routes: Edit. Do NOT grantDNS: Write;custom_domain: trueinwrangler.jsoncprovisions DNS automatically. - Add GitHub repo secrets
CLOUDFLARE_API_TOKENandCLOUDFLARE_ACCOUNT_ID. - Ensure no conflicting CNAME exists on the widget hostnames before first deploy.
New widgets are normally scaffolded via the build-widget Claude skill, which copies the function-plotter template and customises it.