No description
  • TypeScript 98.6%
  • CSS 0.6%
  • JavaScript 0.4%
  • Shell 0.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Max Katz-Christy cd2a125c17
All checks were successful
Vulnerability scan / vuln-scan (push) Successful in 7s
docs: install interlocking from GitHub in README
2026-09-30 21:37:08 +02:00
.forgejo/workflows ci: check out from the public Forgejo URL in the vuln scan 2026-09-30 20:01:13 +02:00
.githooks chore: add typecheck, lint, format and dead-export checks 2026-09-17 17:11:42 +02:00
.github/workflows ci: add osv-scanner vulnerability gate on GitHub and Forgejo Actions 2026-09-30 19:49:39 +02:00
scripts ci: add osv-scanner vulnerability gate on GitHub and Forgejo Actions 2026-09-30 19:49:39 +02:00
src docs(ui): describe GitHub as a second remote, not a mirror 2026-09-30 19:19:13 +02:00
.cz.toml chore(release): bump version 3.7.0 -> 4.0.0 2026-09-30 19:09:26 +02:00
.gitignore Add route-colors.ts as the first shared module 2026-08-27 01:52:27 +02:00
.prettierrc chore: add typecheck, lint, format and dead-export checks 2026-09-17 17:11:42 +02:00
AGENTS.md docs: rename CLAUDE.md to AGENTS.md and point install and repo links at GitHub 2026-09-30 20:09:18 +02:00
CHANGELOG.md chore(release): bump version 3.7.0 -> 4.0.0 2026-09-30 19:09:26 +02:00
CODE_OF_CONDUCT.md docs: add OpenRail code of conduct, governance, maintainers and security policy 2026-09-30 20:39:14 +02:00
CURRENT_PLAN.md fix: hold shared module state on globalThis 2026-09-20 09:54:07 +02:00
eslint.config.js feat(scripts): ship generate-atlas-data 2026-09-17 17:15:02 +02:00
GOVERNANCE.md docs: add OpenRail code of conduct, governance, maintainers and security policy 2026-09-30 20:39:14 +02:00
LICENSE.txt chore: relicense under AGPL-3.0 2026-08-27 15:34:33 +02:00
MAINTAINERS.md docs: add OpenRail code of conduct, governance, maintainers and security policy 2026-09-30 20:39:14 +02:00
package.json ci: add osv-scanner vulnerability gate on GitHub and Forgejo Actions 2026-09-30 19:49:39 +02:00
pnpm-lock.yaml feat(map)!: require maplibre-gl 6 and update dependencies 2026-09-30 19:08:50 +02:00
pnpm-workspace.yaml chore: add typecheck, lint, format and dead-export checks 2026-09-17 17:11:42 +02:00
README.md docs: install interlocking from GitHub in README 2026-09-30 21:37:08 +02:00
SECURITY.md docs: add OpenRail code of conduct, governance, maintainers and security policy 2026-09-30 20:39:14 +02:00
tsconfig.json feat(map)!: require maplibre-gl 6 and update dependencies 2026-09-30 19:08:50 +02:00

interlocking

Shared browser-side modules for the gtfs.zone apps: coloring-book (edit.gtfs.zone), test-track (viz.rt.gtfs.zone) and yard-master (manage.rt.gtfs.zone).

Ships raw .ts source under src/. There is no build step: each app's vite compiles it as source. Consumed as a pinned git dependency:

pnpm add "interlocking@github:gtfs-zone/interlocking#vX.Y.Z"

maplibre-gl, @leeoniya/ufuzzy, jszip and papaparse are peer dependencies. Every consumer already carries all four, and a second copy of maplibre is a broken map rather than a duplicate. gtfs-realtime-bindings is an optional peer: gtfs/rt-types.ts imports its namespace as a type and nothing here pulls protobufjs into a bundle, so only the apps that read GTFS-RT need it installed.

What it is

A browser-side library for GTFS and GTFS-RT frontends: the UI chrome (navbar, modals, notifications, theme, search), the GTFS domain modules (route ordering, route diagrams, colors, feed loading) and the MapLibre layer specs. Nothing in here is specific to one of the three apps; anything that is belongs in the app.

Modules arrive by moving out of an app, not by being copied from it: once a module lives here it is edited here.

CURRENT_PLAN.md holds the roadmap, including what is still hand-copied between the apps and the layout this package is moving to.

Checks

pnpm run check runs all three, and a pre-commit hook runs them on every commit (enable it with git config core.hooksPath .githooks).

script what it does
typecheck tsc --noEmit over src/ and scripts/
lint eslint src/ scripts/ --max-warnings 0
check:exports reports exports no consumer imports

format runs prettier over the same directories.

check:exports replaces knip, which is vacuous for a library with no barrel files: every module is its own entry point, so nothing ever looks unused. Instead it resolves the sibling checkouts, collects every interlocking/... import across them and diffs that against what src/ exports. An export only another module here imports is reported as internal rather than unused. A sibling that is not checked out is skipped, and the run exits 0 when all three are absent. Unused exports warn; --strict makes them fatal.

Layout

Four peers, organised by domain rather than by the modules/utils/types split the apps use:

src/ui/     chrome that knows nothing about GTFS
src/gtfs/   the transit domain, including its own rendering
src/map/    everything that imports maplibre-gl
src/util/   pure, domain-free

No barrel index.ts files: every module is its own entry point, imported as interlocking/ui/navbar-actions and resolved through each consumer's tsconfig path and vite alias.

App shell

The layout every app shares, in four parts that go together:

  • ui/app-shell.ts mounts the markup: navbar, map with its search card and auto-zoom toggle, the resizable right panel with #panel-content, and an optional mobile dock. Its element ids are what search-controller, bottom-sheet, panel-resizer and navbar-actions bind to.
  • ui/app-shell.css styles it: the map/panel grid, the <=767px bottom-sheet drawer and the map controls. Imported from the app's stylesheet.
  • ui/page-state-manager.ts owns the current page state, its history and the URL hash, generic over the app's page-state union. The app supplies the hash codec and the validator.
  • ui/focus-controller.ts (setFocus, onFocusChange / onStateChange) and ui/panel-host.ts (the data-nav dispatch, the breadcrumb header, and the scroll and <details> restore) sit on top of it. The app supplies the pages.

Published data

The load modal's feed catalog is fetched at runtime from https://data.gtfs.zone (gtfs/data-origin.ts), published daily by geometry-car: search.json, a compact cut of feeds.json listed in manifest.json with its hash, one entry per transit system from Transitland, the Mobility Database and rt.gtfs.zone, with the last reachability check of each of its roles. gtfs/feed-catalog.ts loads it (falling back to feeds.json when the manifest does not list it), gtfs/feed-search.ts searches it and gtfs/feed-badges.ts renders a feed's state and role chips. The modal lists the feeds the host app can use by default (a schedule that answered; in the visualiser, plus a realtime role that did), newest schedule first, with a "show all" toggle. Nothing is baked into a consumer's public/.

Releasing

cz bump on main, which writes the version into package.json, updates CHANGELOG.md and cuts the annotated vX.Y.Z tag. Push the commit and the tag, then repin each consumer. A shared change is one commit here, one tag, and three consumer bumps.

Restart any dev server the repinned app has running. The interlocking alias resolves through a pnpm symlink to a path in the store, and a repin swaps that symlink for a new one. Vite does not watch node_modules, so every app file whose transform is still cached keeps importing the old store path: the browser then loads two copies of a shared module, one per path, and each copy gets its own module-level state. util/module-state.ts keeps that from corrupting anything and logs loaded twice when it happens; the restart is still the fix.