Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .github/workflows/refresh-downloads.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Refresh mod downloads

# Keeps the `downloads` branch: the hub's own copies of each mod's current ZIP (the release its Download button
# offers) and their manifest.json, which the "Download several at once" picker joins in the browser. GitHub
# doesn't let a page fetch release downloads; its raw file host does, so the picker reads this branch there.
# Runs after the hourly release refresh has had time to commit (:17), and whenever the page or these scripts
# change. The branch is replaced by one fresh commit each time, so it never grows.
on:
schedule:
- cron: '37 * * * *'
push:
branches: [main]
paths:
- index.html
- scripts/update-downloads.mjs
- scripts/downloads-lib.mjs
- .github/workflows/refresh-downloads.yml
workflow_dispatch:

permissions:
contents: write

concurrency:
group: refresh-downloads
cancel-in-progress: false

jobs:
refresh:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
ref: main
- uses: actions/setup-node@v7
with:
node-version: 22
- name: Fetch the current copies
run: |
mkdir -p mirror
if git fetch --depth=1 origin downloads; then
git archive FETCH_HEAD | tar -x -C mirror
else
echo "No downloads branch yet: starting empty."
fi
- id: update
run: node scripts/update-downloads.mjs mirror
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Publish the downloads branch
if: steps.update.outputs.changed == 'true'
run: |
cd mirror
git init -q -b downloads
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add -A
git commit -q -m "Mod downloads for the Download several picker"
git push --force "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" downloads
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.claude/
node_modules/
_mirror/
7 changes: 7 additions & 0 deletions .impeccable/design.json
Original file line number Diff line number Diff line change
Expand Up @@ -1008,6 +1008,13 @@
"description": "A paper panel without a mod colour, in a 5px double rule; fills a board's spare slot.",
"html": "<div class=\"ds-notice\"><h3>Which one? One BeaverBuddies at a time</h3><dl><div style=\"--c:#b8322a\"><dt>Stability Fork</dt><dd>Everyone builds one colony together.</dd></div><div style=\"--c:#1a6a77\"><dt>Timber Together</dt><dd>Each player runs a colony of their own.</dd></div></dl><p>Install only one of the two.</p></div>",
"css": ".ds-notice { max-width: 320px; background: var(--tile, #f5ecd9); color: var(--tile-ink, #2a2118); border: 5px double var(--tile-rule, #dccdab); border-radius: 8px; padding: 22px 20px; box-shadow: 0 2px 0 rgba(0,0,0,.22), 0 12px 20px -12px rgba(0,0,0,.65); font-family: var(--sans, system-ui, sans-serif); } .ds-notice h3 { font-family: var(--display, 'Anybody', 'Arial Narrow', sans-serif); font-weight: 800; font-size: 1.35rem; line-height: 1.08; margin: 0 0 14px; } .ds-notice dl { margin: 0 0 14px; } .ds-notice dl > div { padding: 10px 0; border-top: 1px solid var(--tile-rule, #dccdab); } .ds-notice dt { font-family: var(--display, 'Anybody', 'Arial Narrow', sans-serif); font-weight: 800; } .ds-notice dt::before { content: ''; display: inline-block; width: 10px; height: 10px; margin-right: 8px; border-radius: 2px; background: var(--c); } .ds-notice dd { margin: 4px 0 0; color: var(--tile-muted, #5d4c39); font-size: .95rem; } .ds-notice > p { margin: 0; padding-top: 12px; border-top: 1.5px solid var(--tile-ink, #2a2118); font-weight: 650; font-size: .95rem; }"
},
{
"name": "Download Picker",
"kind": "form",
"description": "\"Download several at once\": a paper sheet with a Multiplayer radio group (None, Stability Fork, Timber Together: only one can run) and a two-column checklist of the other mods, each row a pine control, the mod's swatch and name, and its version and size; a pine download button and an aria-live status line span the foot. assets/bundle.js joins the picked mods' ZIPs into one in the browser.",
"html": "<form class=\"ds-picker\"><fieldset><legend>More mods</legend><label class=\"ds-pick\" style=\"--c:#8a6a2c\"><input type=\"checkbox\" checked><span><b>MixedStorage</b><small>v1.2.0 · 42 KB</small></span></label><label class=\"ds-pick\" style=\"--c:#1d4a86\"><input type=\"checkbox\"><span><b>Hungry Pathing</b><small>v0.3.1 · 28 KB</small></span></label></fieldset><button class=\"ds-pick-go\" type=\"button\">Download 1 mod · 42 KB</button></form>",
"css": ".ds-picker{max-width:520px;background:#f5ecd9;color:#2a2118;border:1px solid #dccdab;border-radius:6px;box-shadow:0 2px 0 rgba(0,0,0,.12),0 12px 24px -14px rgba(0,0,0,.45);font:16px/1.5 system-ui,sans-serif}.ds-picker fieldset{border:0;margin:0;padding:20px 24px 12px}.ds-picker legend{font:800 1.15rem/1.2 Anybody,'Arial Narrow',sans-serif;padding:0;margin-bottom:10px}.ds-pick{display:grid;grid-template-columns:auto 1fr;gap:12px;align-items:center;min-height:54px;border-bottom:1px solid #dccdab}.ds-pick input{width:20px;height:20px;accent-color:#2f5d3a}.ds-pick b{display:inline-flex;align-items:center;gap:8px;font:700 .98rem/1.2 Anybody,'Arial Narrow',sans-serif}.ds-pick b::before{content:'';width:12px;height:12px;border-radius:3px;background:var(--c);box-shadow:0 0 0 2px #f5ecd9,0 0 0 3px var(--c)}.ds-pick small{display:block;padding-left:20px;color:#5d4c39;font-size:.85rem}.ds-pick-go{margin:4px 24px 20px;min-height:48px;padding:8px 18px;border:0;border-radius:8px;background:#2f5d3a;color:#f5ecd9;font-weight:750}"
}
],
"narrative": {
Expand Down
18 changes: 13 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ main).

- Never launch or drive Timberborn, and never touch installed mods or saves. The maintainer (Kyler) playtests himself.
- Commit on a branch and open a PR. Kyler has said to merge PRs automatically: merge, then check the page live.
- **Never edit `data/releases.json`.** `.github/workflows/refresh-releases.yml` regenerates it hourly
- **Never edit `data/releases.json` or the `downloads` branch.** The branch holds the download picker's copies of
each mod's ZIP; `.github/workflows/refresh-downloads.yml` replaces it hourly (`node scripts/update-downloads.mjs`).
- About `data/releases.json`: `.github/workflows/refresh-releases.yml` regenerates it hourly
(`node scripts/update-releases.mjs`) and commits straight to `main` as github-actions[bot]. So `main` moves several
times a day: before a PR run `git fetch origin && git merge origin/main`, and on a conflict in that file take
main's copy (`git checkout --theirs data/releases.json`).
Expand Down Expand Up @@ -41,7 +43,8 @@ text and the player docs follows these rules.

- **Where:** the repo root: `index.html` (opening with the need index, three walnut boards of mods, install, footer),
`404.html` (uses root paths `/assets/...`; every other path is relative), `assets/style.css`, `assets/site.js`
(download buttons, theme toggle), `assets/fonts/`, `assets/img/`, `assets/favicon.svg`. Live at
(download buttons, theme toggle), `assets/bundle.js` + `assets/zipmerge.js` (the "Download several at once"
picker, `#bundle`: joins the picked mods' ZIPs in the browser), `assets/fonts/`, `assets/img/`, `assets/favicon.svg`. Live at
https://timbermods.github.io/.
- **Published:** GitHub Pages "legacy" build from `main`, folder `/` (`.nojekyll` stays). Merging to main publishes;
a build takes about a minute.
Expand Down Expand Up @@ -82,7 +85,8 @@ text and the player docs follows these rules.
#f0d27a, caution #f4dfb3 on #663a04 / #3b301b on #f1cf8b, Stable pill #2c6536 / #9fd49a, Preview pill #7a4a06 /
#f0c070.
- Fonts: Anybody 700/800, self-hosted in `assets/fonts/` (`anybody-latin-{700,800}-normal.woff2`, `OFL-Anybody.txt`).
No other webfonts, nothing from a CDN; the only external requests are GitHub's API.
No other webfonts, nothing from a CDN; the only external requests are GitHub's API and the picker's reads of the
`downloads` branch from raw.githubusercontent.com.
- Textures: `birch.webp`, `forest-floor.webp`, `walnut.webp`, `walnut-dark.webp`, made by `assets/img/make_textures.py`
(numpy + Pillow, fixed seeds; run it from `assets/img/`). Change the script, don't edit images. Every shipping raster
carries provenance (`.webp.json` sidecar, or a tEXt chunk in `og.png`): after adding or changing one, run (with `$IMP`
Expand Down Expand Up @@ -118,7 +122,10 @@ text and the player docs follows these rules.
- A caution is `<p class="caution">…<svg/>Label: …</p>` inside the card; `data-maturity` equals Label (e.g. `Beta`,
`Young mod`). Remove both together when a mod is ready. Any `.caution` outside a card fails the test.
- Footer: one `[data-generated]` with default text "Release data loads from GitHub.".
- Anchors: `#top`, `#mods`, `#install` (the org profile links `/#install`) and the eight card ids.
- The picker: `form[data-bundle]`, inputs named `multiplayer` (radios, `value=""` = None) and `mod` whose values are
the panel ids, `[data-ver]` per row, `[data-bundle-all]`, `[data-bundle-go]`, `[data-bundle-size]`,
`[data-bundle-note]`. A new mod gets a row there too (Multiplayer only for a BeaverBuddies mod).
- Anchors: `#top`, `#mods`, `#bundle`, `#install` (the org profile links `/#install`) and the eight card ids.

### Content rules

Expand Down Expand Up @@ -151,6 +158,7 @@ changes its look. Write every change by *Writing README and website text* above.
unofficial…") when the count, game version or a one-line summary changes (`grep -n "1.1.2.4\|[Ee]ight" *.html README.md PRODUCT.md`);
- the install `.notes` if requirements in general change; the footer credit if a new BeaverBuddies derivative lands;
- `404.html`'s mod list (a copy of the cards' names, URLs and `--c`; **kept in sync by hand**);
- the download picker's row (`#bundle`: name, `--c`, and the panel id as the input's value);
- PRODUCT.md's Operating Context and README's mod list; the org profile (`timbermods/.github`,
`profile/README.md`) repeats each card's text, accent, category and group: update it in a PR to that repo. Its
images come from this site: after card art changes here (and is live), run `python profile/make_images.py` there.
Expand All @@ -159,7 +167,7 @@ changes its look. Write every change by *Writing README and website text* above.
`scripts/card-art.py` (add a `JOBS` entry) + provenance, a need-index row, the 404 row, and the counts in step 2.
A board with an empty spot fills it with an `.insert` notice in an `.insert-sleeve`. The tests read cards from
`index.html`, so no test edit is needed unless you change `site.js`.
4. Test: `node scripts/test-site.mjs` must print `15/15 checks passed`.
4. Test: `node scripts/test-site.mjs` must print `20/20 checks passed`.
5. Preview: `node scripts/serve.mjs` (serves the repo root at http://127.0.0.1:8765/; missing paths get a plain 404,
so open `/404.html` directly). Capture light, dark and a 390px phone: with the personal skill,
`python ~/.claude/skills/impeccable-site-flow/scripts/capsite.py http://127.0.0.1:8765/ <out> "" 404.html`;
Expand Down
10 changes: 7 additions & 3 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,11 +289,11 @@ A woodland palette: pale birch and dark forest floor for the ground, walnut and

## Layout

A single centred column 1180px wide with a fluid gutter (clamp(16px, 4vw, 32px)). The order is fixed: header bar (min 68px, rule beneath), the valley with the wordmark, cover, three walnut boards, install, footer.
A single centred column 1180px wide with a fluid gutter (clamp(16px, 4vw, 32px)). The order is fixed: header bar (min 68px, rule beneath), the valley with the wordmark, cover, three walnut boards, the download picker, install, footer.

The valley runs the full width under the header (aspect 1280:360, min 230px, max 500px), with the wordmark over its sky at the top of the content column. Below it, the cover is two columns (0.9fr / 1.1fr): the lead and facts on the left, the need index on the right (rows of need and mod name, each mod name led by its colour swatch). Each board holds a head (title and lead, 760px max) and a grid of panels, three across, gap clamp(14px, 2vw, 22px), panels stretched to equal height with the download block pushed to the bottom. Where a board has a spare slot, a notice fills it (the Play together board's "Which one?"). Boards stack with clamp(24px, 4vw, 40px) between them. Install is a three-column steps sheet over three columns of notes.

Responsive: at 1000px the panels go two across and the notice spans the row; at 860px the cover, steps and notes collapse to one column and the wordmark runs on one line; at 620px the panels go one across, banners crop to 16:7, boards tighten (26px 12px 20px, 6px corners, nails inset 8px) and the download button stops wrapping. Interactive targets are at least 44px tall; download buttons 48px.
Responsive: at 1000px the panels go two across and the notice spans the row; at 860px the cover, steps, notes and the picker collapse to one column and the wordmark runs on one line; at 620px the panels go one across, banners crop to 16:7, boards tighten (26px 12px 20px, 6px corners, nails inset 8px) and the download button stops wrapping. Interactive targets are at least 44px tall; download buttons 48px.

### Named Rules
**The One Board Per Group Rule.** Every group of mods gets its own walnut board; a mod panel never sits directly on the ground, and nothing but panels and a notice sits on a board.
Expand Down Expand Up @@ -352,17 +352,21 @@ A 6px-cornered strip in caution wash and ink with a stroked warning-triangle ico
- **Header:** the logo and wordmark on the left; Mods, Install, GitHub as 44px-tall ink links (underline on hover) and the 44px round theme toggle (sun by day, moon at night) on the right.
- **Need index:** a list under a 1.5px ink rule; each 46px row pairs a need with a mod name led by a 12px swatch in the mod's colour ringed off the ground. The 404 reuses it with mod names only.

### Download picker
"Download several at once" (`#bundle`): a paper sheet like the install steps, split by a paper rule into two groups. **Multiplayer** (1fr) is a radio list, None, BeaverBuddies Stability Fork and Timber Together, under the hint "Only one can run at a time."; **More mods** (2fr) is a two-column list of the other six under a **Select all** / **Clear these** button. Every group has a subhead (800, 1.15rem) and a 44px hint line, then a 1.5px ink rule; each row (min 54px, paper rule beneath) is a pine-green checkbox or radio, the mod name led by its 12px colour swatch ringed off the paper (as in the need index), and its version and size beneath in muted tabular figures. The foot spans both groups: a 48px pine download button (pine field, paper lettering, min 250px; "Download 3 mods · 2.6 MB", or "Pick a mod first" while disabled at 55% opacity) beside an aria-live status line and one muted line pointing to Installing for Harmony and Mod Settings. At 860px the groups stack and the list runs one across; at 620px the button runs full width.

### Install steps
A paper sheet split into three steps by paper rules, each a pine numeral, title and short text; below it three notes, each under a 1.5px ink rule.

### Named Rules
**The Panel, Not Card Rule.** In user-facing text the mods are shown as panels, never "cards". The CSS classes `.card`, `.binder`, `.page`, `.pockets` and `.sleeve` are leftover names from the previous look. `.card` must stay: `assets/site.js` selects `.card[data-repo]` and `scripts/test-site.mjs` reads `<article class="card">`. The others are not read by either and are kept only to avoid churn.

### Markup contract (JS and test hooks)
`assets/site.js` reads these hooks and `scripts/test-site.mjs` (15 checks) tests them. `data/releases.json` is written by the scheduled bot and is never edited by hand.
`assets/site.js` reads these hooks and `scripts/test-site.mjs` (20 checks) tests them. `data/releases.json` is written by the scheduled bot and is never edited by hand.
- `article.card[data-repo][data-asset]` on every mod panel and nowhere else. `data-maturity` goes on every panel that carries a `.caution`, and equals the caution's label.
- Inside each panel: `[data-download]` (an `a.btn` whose fallback href is the repo's Releases page, containing `.btn-label` and `[data-size]`), `[data-note]` (aria-live), `[data-status]` (a hidden pill), `[data-notes]` (the release-notes link, falling back to Releases), and an `h3` with the mod name.
- `[data-generated]` in the footer for the release-data stamp.
- The picker (`assets/bundle.js`): `form[data-bundle]` holding inputs named `multiplayer` (radios; `value=""` is None) and `mod` (checkboxes), whose values are the panel ids; each row's `[data-ver]`; `[data-bundle-all]`, `[data-bundle-go]` (with `.btn-label` and `[data-bundle-size]`) and `[data-bundle-note]` (aria-live). It reads the `downloads` branch's `manifest.json`, whose ids are the panel ids.

## Do's and Don'ts

Expand Down
Loading
Loading