From b1261f4606afe51c92e261da22beae656a3d32ff Mon Sep 17 00:00:00 2001 From: NGPixel Date: Sat, 22 Aug 2026 18:31:50 -0400 Subject: [PATCH] fix: scroll to tab header + add crowdin config --- .github/workflows/build.yml | 13 +++++++++-- CLAUDE.md | 7 ++++++ backend/package.json | 4 ++-- blocks/block-tabs/component.js | 41 ++++++++++++++++++++++++++-------- dev/build/Dockerfile | 9 +++++++- dev/crowdin.yml | 6 +++++ 6 files changed, 66 insertions(+), 14 deletions(-) create mode 100644 dev/crowdin.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 19ccea1c3..69f0bcaad 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,6 +29,7 @@ jobs: run: | yq -iP '.version = strenv(REL_VERSION)' backend/package.json -o json yq -iP '.version = strenv(REL_VERSION)' frontend/package.json -o json + - name: Build Assets working-directory: frontend run: | @@ -57,8 +58,8 @@ jobs: context: . file: dev/build/Dockerfile push: true - platforms: linux/amd64 - # platforms: linux/amd64,linux/arm64 + # platforms: linux/amd64 + platforms: linux/amd64,linux/arm64 tags: | ghcr.io/requarks/wiki:3.0.0-alpha ghcr.io/requarks/wiki:${{ env.REL_VERSION }} @@ -82,3 +83,11 @@ jobs: name: build path: wiki-js.tar.gz + - name: Upload translations source file + uses: crowdin/github-action@v2 + env: + CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} + with: + command: 'upload' + command_args: '--config dev/crowdin.yml' + diff --git a/CLAUDE.md b/CLAUDE.md index 07dbe4ccd..c359f87e9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -45,6 +45,13 @@ from inside `backend/`. It boots in three phases: `preBoot()` (config → db → scheduler → event emitters), `initHTTPServer()` (Fastify plugins, auth, routes), `postBoot()` (refresh locales/strategies/sites from disk & db, start scheduler). +Started with **`--no-experimental-webstorage`** — by the npm scripts and by the production image's +`CMD`, which is the only reason a bare `node backend` still opens with an experimental warning about +`localStorage`. Nothing here uses Web Storage; `lib0`, under yjs, probes for it as it loads the way a +library that runs in a browser too has to, and Node 26 answers that probe with a warning instead of a +value unless `--localstorage-file` is given. Off, the global is absent and the probe takes its node +path in silence. + - `api/` — REST route plugins, one file per resource (`sites.ts`, `users.ts`, `pages.ts`, `system.ts`, `locales.ts`, `authentication.ts`), registered by `api/index.ts` under the `/_api` prefix. diff --git a/backend/package.json b/backend/package.json index 58007409d..8593e2569 100644 --- a/backend/package.json +++ b/backend/package.json @@ -25,8 +25,8 @@ "type": "module", "main": "index.ts", "scripts": { - "start": "cd .. && node backend", - "dev": "cd .. && nodemon backend --watch backend --ext js,ts,json", + "start": "cd .. && node --no-experimental-webstorage backend", + "dev": "cd .. && nodemon --no-experimental-webstorage backend --watch backend --ext js,ts,json", "typecheck": "tsc", "typecheck:watch": "tsc --watch", "ncu": "ncu -i", diff --git a/blocks/block-tabs/component.js b/blocks/block-tabs/component.js index c515cb612..5a184495b 100644 --- a/blocks/block-tabs/component.js +++ b/blocks/block-tabs/component.js @@ -12,6 +12,16 @@ import { DarkMode } from '../shared/theme.js' */ const REVEAL_EVENT = 'block-reveal' +/** + * The room a heading in an article is given above it when it is scrolled to, in pixels. + * + * The article scrolls in its own column inside a fixed shell, so a heading brought into view stops + * clear of the column's top edge rather than flush against it. Written out because a block cannot read + * the app's stylesheet: this is the `scroll-margin-top: 1.25rem` that `_page-contents.scss` puts on + * every heading, and the two have to be kept in step. + */ +const HEADING_CLEARANCE = 20 + /** * Block Tabs */ @@ -230,27 +240,40 @@ Content of the second tab. } /** - * Keep the strip on screen when something inside a panel is scrolled to. + * Keep the whole block on screen when something inside a panel is scrolled to. * * A heading carries a `scroll-margin-top` so it does not land flush against the top edge, but that * margin knows nothing about the strip standing above it — following a link to a heading in a tab * would scroll the tabs themselves out of view, leaving the reader in a panel with no way to see * which one they were in. Set on the elements because the content is slotted, and measured because - * the strip is as tall as the labels wrapped onto however many rows. + * a block cannot be told in CSS how tall its own strip is. + * + * Measured from the panel back up to the block, so that what a scroll clears is everything above + * the panel rather than the strip alone: the strip may have wrapped onto two rows, the frame draws + * a border, and the panel pads itself. Clearing only the strip's height put the tabs *just* on the + * edge of the column with nothing above them, and a tab acting as a page heading — `header` on + * `block-tab`, anchored on the panel because the label is an attribute and no heading in the page + * carries it — is aimed at from the contents list, so it landed with its own label against the + * edge. `HEADING_CLEARANCE` on top is what every heading in an article gets, so a tab arrived at + * from the contents list sits where a section heading would. */ _applyScrollMargin() { const strip = this.renderRoot.querySelector('.strip') if (!strip) { return } - const margin = `${strip.offsetHeight + 20}px` + /* + From the open panel, which is the only one with a box to measure. They all sit in the same + place, so its offset is every panel's — and a block inside a panel that is not showing measures + nothing at all, which is why the strip's height stands in until there is something to read. + */ + const open = this._tabs[this.active]?.panel + const above = open + ? Math.round(open.getBoundingClientRect().top - this.getBoundingClientRect().top) + : strip.offsetHeight + const margin = `${above + HEADING_CLEARANCE}px` for (const { panel } of this._tabs) { - /* - The panel as well as what is in it. A tab whose label is a page heading — `header` on - `block-tab` — is anchored on the panel element itself, since the label is an attribute and - there is no heading in the page to carry the anchor, so the panel is what a contents click - scrolls to and it needs the same margin as any heading in it. - */ + // -> The panel as well as what is in it: either can be what a link or the contents list aims at panel.style.setProperty('scroll-margin-top', margin) for (const child of panel.children) { child.style.setProperty('scroll-margin-top', margin) diff --git a/dev/build/Dockerfile b/dev/build/Dockerfile index fb17f98d2..9968b0ef0 100644 --- a/dev/build/Dockerfile +++ b/dev/build/Dockerfile @@ -58,4 +58,11 @@ VOLUME ["/wiki/data/content"] EXPOSE 3000 EXPOSE 3443 -CMD ["node", "backend"] +# Web Storage off, which is what it was before Node 26 turned it on by default. +# +# Nothing here uses `localStorage`, but `lib0` -- under yjs, which is what makes an editing session +# collaborative -- probes for it as it loads, the way a library that runs in both a browser and node +# has to. Without `--localstorage-file` that probe is answered with an experimental warning rather +# than a value, so the first line of every container's log was a warning about a feature the server +# does not use. Off, the global is absent and the probe takes its node path in silence. +CMD ["node", "--no-experimental-webstorage", "backend"] diff --git a/dev/crowdin.yml b/dev/crowdin.yml new file mode 100644 index 000000000..47756d591 --- /dev/null +++ b/dev/crowdin.yml @@ -0,0 +1,6 @@ +project_id: '921805' +preserve_hierarchy: false +files: + - source: 'backend/locales/en.json' + dest: 'app.json' + update_option: 'update_as_unapproved'