Tavotto
AI drew your research figure. Tavotto makes it publication-ready: edit matplotlib figures on a canvas, lay out in mm, check journal rules. Free, open source.
翻译准备中
尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证正在加载完整 README。 以下为已保存的摘录。
Web tool for making printable member and leader cards for a church photo board.
Enter names, upload a photo, frame and colour-correct it, and download a
1800×1200 JPEG ready to print and pin up.
pnpm's version is pinned by the packageManager field, so it self-selects the
right release. The first install asks you to approve native build scripts for
esbuild and workerd; these are pre-approved in pnpm-workspace.yaml.查看完整 README ↗Web tool for making printable member and leader cards for a church photo board. Enter names, upload a photo, frame and colour-correct it, and download a 1800×1200 JPEG ready to print and pin up.
Live at https://photoboard.mluther.org
| Concern | Choice |
|---|---|
| Build | Vite 8 + TypeScript |
| UI | React 19 |
| Styling | Tailwind CSS 4, themed to match Bootswatch Lux |
| Photo framing | react-easy-crop |
| HEIC support | heic-to, lazily loaded |
| Icons | lucide-react |
| Fonts | Merriweather + Nunito Sans, self-hosted via Fontsource |
| Hosting | Cloudflare Workers (static assets) |
| Packages | pnpm |
pnpm install
pnpm dev
pnpm's version is pinned by the packageManager field, so it self-selects the
right release. The first install asks you to approve native build scripts for
esbuild and workerd; these are pre-approved in pnpm-workspace.yaml.
Other scripts:
pnpm check # everything CI runs, in one go
pnpm build # typecheck, then build to dist/
pnpm preview # serve the production build locally
pnpm typecheck # types only
pnpm lint # eslint
pnpm lint:fix # eslint, fixing what it can
pnpm format # prettier, writing changes
pnpm format:check # prettier, check only (what CI runs)
.github/workflows/ci.yml runs format check, lint, typecheck and build on
every pull request and on pushes to master. The steps use if: !cancelled()
so one failure doesn't mask the rest — a PR shows every problem in a single
run rather than one per push. pnpm check runs the same set locally.
Dependencies must be at least 24h old. pnpm-workspace.yaml sets
minimumReleaseAge: 1440, so a version published in the last day is rejected —
that window is when a compromised package reaches you before anyone has noticed.
pnpm's resolver honours it, so an ordinary ^ range simply resolves to the
newest release that is old enough; you rarely notice it.
If CI reports ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION but the same install
passes locally, it's because pnpm caches a per-lockfile verdict in
~/Library/Caches/pnpm/lockfile-verified.jsonl (~/.cache/pnpm/ on Linux) and
reuses it — the message says "verified Nm ago". Delete that file to force a real
check. To fix a genuine violation, rm pnpm-lock.yaml && pnpm install and let
it re-resolve onto settled versions.
TypeScript is pinned to 6.0.3 on purpose. typescript-eslint hard-errors on TypeScript 7 (it refuses to load, rather than warning), and 6.0.3 is the newest release it supports. Revisit once typescript-eslint#10940 lands; until then, upgrading TypeScript will break linting.
Linting is type-aware (recommendedTypeChecked), so rules like
no-floating-promises can see across the async upload/render/download paths.
pnpm deploy
That builds and runs wrangler deploy, which uploads dist/ to Cloudflare.
You'll need to pnpm exec wrangler login once first.
wrangler.jsonc claims photoboard.mluther.org as a custom domain. For that
to work, the mluther.org zone must be on the same Cloudflare account — once
it is, Cloudflare creates and manages the DNS record itself, so there is
nothing to add by hand. If the zone lives elsewhere, either move it to
Cloudflare or drop the routes block and use the *.workers.dev URL.
The printed card is drawn directly onto a canvas, not screenshotted from the DOM. Everything flows from one place:
src/lib/cardLayout.ts — the card's geometry and type scale, in a 600×400
design space. The export is the same numbers at 3×, giving exactly 1800×1200.src/lib/renderCard.ts — draws a card at any scale from that spec.The live preview and the downloaded JPEG call the same renderer, so the preview cannot drift from what prints. The only part of the preview that isn't canvas is the photo itself, which is the interactive cropper layered over the photo frame; its crop region feeds straight into the export.
Two things worth knowing if you ever adjust the layout:
CARD.textPadLeft / textPadRight are deliberately
asymmetric (5px / 15px), nudging the text block left of true centre to
correct for drift on the printer these are produced on. Retune that pair,
not the centre point, if you change printers.family-placeholder.webp is
trimmed so its very top row of ink is the crown of the father's head, so
PLACEHOLDER_BOX simply puts that edge on the red guide line and the art
demonstrates the alignment it asks the user for. Width is the frame less
PLACEHOLDER_INSET_X, and the aspect fixes the height — which deliberately
overruns the frame, cropping the family mid-shin, so it must be clipped.
Replacement art has to be trimmed the same way, with the tallest head at the
very top, and PLACEHOLDER_ASPECT updated to match.fillText, the text is silently already wrong.
src/lib/fonts.ts awaits every face the card uses before any render. This is
why the fonts are self-hosted rather than pulled from a CDN.The Lux look was reproduced by measuring the old build's computed styles rather than eyeballing screenshots, so a few values look odd but are deliberate:
51fr 106fr 51fr, not 1fr 2fr 1fr. Bootstrap's col-md-3/6/3
sit on a row that overhangs the container's padding, so the real content
columns are 306/636/306 either side of 24px gutters.font-weight: 400, though Lux declares 200. Lux only ever
loaded Nunito Sans 400 and 600, so its 200 had no face to resolve to and
always rendered at 400.-webkit-font-smoothing: antialiased. Lux doesn't set it, and it renders
noticeably thinner and lighter than the platform default.inline-block +
24px line-height, which an icon-only flex button would otherwise collapse.\n in any single-line field to force a line break.This replaced a Sergey-built static site deployed to surge.sh. Things that changed beyond the framework:
html-to-image is gone. It was the source of the intermittent "try
uploading a smaller image" failures; the canvas renderer is deterministic and
has no image-size ceiling.