diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 36b46d4..cde97dd 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -6,6 +6,15 @@ on:
paths:
- "docs/**"
- ".github/workflows/docs.yml"
+ # The live demo is built from the app + bake pipeline and deployed under the
+ # docs site, so redeploy when any of those change too.
+ - "web/**"
+ - "cmd/**"
+ - "internal/**"
+ - "scripts/fetch-demo-cells.sh"
+ - "Makefile"
+ - "go.mod"
+ - "go.sum"
workflow_dispatch:
permissions:
@@ -21,20 +30,49 @@ concurrency:
jobs:
build:
runs-on: ubuntu-latest
- defaults:
- run:
- working-directory: docs
steps:
- uses: actions/checkout@v7
+
+ - uses: actions/setup-go@v6
+ with:
+ go-version: "1.26"
+ cache: true
+
+ # Cache the curated NOAA demo-cell downloads (one per band over Annapolis),
+ # keyed on the cell list / fetch script so a change re-downloads. Lets the
+ # bake reuse the same cells across runs.
+ - name: Cache demo ENC cells
+ uses: actions/cache@v4
+ with:
+ path: ${{ runner.temp }}/demo-cells
+ key: demo-cells-${{ hashFiles('scripts/fetch-demo-cells.sh', 'Makefile') }}
+
+ # Build the read-only widget demo bundle into docs/static/demo so Docusaurus
+ # copies it into the published site at /chartplotter/demo/. Needs the S-101
+ # catalogue (IHO material kept out of the repo): clone it and let `make build`
+ # (a dep of `make demo`) embed it. The bundle is pure static files — no backend.
+ - name: Build live demo bundle (docs/static/demo)
+ run: |
+ git clone --depth 1 https://github.com/iho-ohi/S-101_Portrayal-Catalogue.git "$RUNNER_TEMP/s101-pc"
+ git clone --depth 1 https://github.com/iho-ohi/S-101-Documentation-and-FC.git "$RUNNER_TEMP/s101-fc"
+ make demo \
+ S101_PC="$RUNNER_TEMP/s101-pc/PortrayalCatalog" \
+ S101_FC="$RUNNER_TEMP/s101-fc/S-101FC/FeatureCatalogue.xml" \
+ DEMO_CACHE="$RUNNER_TEMP/demo-cells" \
+ DEMO_OUT="docs/static/demo"
+
- uses: actions/setup-node@v6
with:
node-version: "20"
cache: npm
cache-dependency-path: docs/package-lock.json
- name: Install
+ working-directory: docs
run: npm ci
- name: Build
+ working-directory: docs
run: npm run build
+
- uses: actions/upload-pages-artifact@v3
with:
path: docs/build
diff --git a/Makefile b/Makefile
index e41788d..038a65d 100644
--- a/Makefile
+++ b/Makefile
@@ -29,7 +29,7 @@ S101_PC ?= $(HOME)/Projects/s101-portrayal-catalogue/PortrayalCatalog
S101_FC ?= $(HOME)/Projects/s101-feature-catalogue/S-101FC/FeatureCatalogue.xml
S101_CACHE ?= $(CACHE)/s101
-.PHONY: build xbuild test vet fmt fmt-check tidy clean clear-cache serve docs docs-shots bake-ienc bake-noaa serve-prod
+.PHONY: build xbuild test vet fmt fmt-check tidy clean clear-cache serve docs docs-shots bake-ienc bake-noaa serve-widget demo serve-demo
# Prebaked prod test set (US Inland ENC bundle + the NOAA world archive).
# NB: keep these as bare values with NO inline `#` comments — Make folds any
@@ -146,10 +146,11 @@ noaa-d%.stamp: $(NOAA_CACHE)/%CGD_ENCs.zip | $(BIN)
@touch "$@"
# Serve the per-district NOAA archives + the baked IENC archive TOGETHER,
-# prebaked, in production mode on 0.0.0.0:8080. Every .pmtiles lives at the project
-# root; they're symlinked into web/ (the served asset dir) and listed in a combined
-# charts-index.json manifest the prod app loads via ?catalog=. Open the printed URL.
-serve-prod: build bake-noaa ## Serve per-district per-band NOAA + IENC prebaked pmtiles together, prod mode, on 0.0.0.0:8080
+# prebaked, in read-only widget mode on 0.0.0.0:8080. Every .pmtiles lives at the
+# project root; they're symlinked into web/ (the served asset dir) and listed in a
+# combined charts-index.json manifest the widget app loads via ?catalog=. Open the
+# printed URL.
+serve-widget: build bake-noaa ## Serve per-district per-band NOAA + IENC prebaked pmtiles together, read-only widget mode, on 0.0.0.0:8080
@ln -sf "$(abspath $(IENC_PMTILES))" web/ienc.pmtiles
@for d in $(DISTRICTS); do for s in $(NOAA_BANDS); do \
f="noaa-d$$d-$$s.pmtiles"; [ -f "$$f" ] && ln -sf "$(abspath .)/$$f" "web/$$f" || true; \
@@ -162,12 +163,45 @@ serve-prod: build bake-noaa ## Serve per-district per-band NOAA + IENC prebaked
printf ' { "file": "ienc.pmtiles", "band": "all" }\n ]\n}\n'; \
} > web/charts-index.json
@echo
- @echo " Prebaked prod test server — open:"
- @echo " http://localhost:8080/?prod&catalog=/charts-index.json"
- @echo " (binds 0.0.0.0 — reachable from the LAN at http://:8080/?prod&catalog=/charts-index.json)"
+ @echo " Prebaked widget test server — open:"
+ @echo " http://localhost:8080/?widget&catalog=/charts-index.json"
+ @echo " (binds 0.0.0.0 — reachable from the LAN at http://:8080/?widget&catalog=/charts-index.json)"
@echo
$(BIN) serve --host 0.0.0.0 --port 8080 --assets web
+# ---- read-only demo bundle (the `widget` mode, packaged for static hosting) ----
+# A self-contained, no-backend chart viewer over ONE location (Annapolis) with all
+# the bands NOAA publishes there, so a visitor can zoom from the whole bay down to
+# the docks on a few MB of tiles. `make demo` assembles dist/demo/ (override with
+# DEMO_OUT, e.g. DEMO_OUT=docs/static/demo in CI): the per-band .pmtiles + manifest,
+# the generated S-101 client assets, and the committed static frontend (demo.html
+# as index.html). Serve it from ANY static host / CDN — no server logic required.
+DEMO_CELLS ?= US2EC03M US3EC08M US4MD1DC US5MD1MC
+DEMO_CACHE ?= $(CACHE)/demo
+DEMO_OUT ?= dist/demo
+DEMO_MAXZOOM ?= 16
+
+demo: build ## Assemble the read-only Annapolis widget demo bundle into $(DEMO_OUT)
+ DEMO_CACHE="$(DEMO_CACHE)" DEMO_CELLS="$(DEMO_CELLS)" NOAA_URL_BASE="$(NOAA_URL_BASE)" scripts/fetch-demo-cells.sh
+ @mkdir -p "$(DEMO_OUT)"
+ $(BIN) bake "$(DEMO_CACHE)" -o "$(DEMO_OUT)/demo.pmtiles" --bands --max-zoom $(DEMO_MAXZOOM) --manifest "$(DEMO_OUT)/charts-index.json"
+ $(BIN) emit-assets "$(DEMO_OUT)" $(if $(wildcard $(S101_PC)),--s101 "$(S101_PC)")
+ @echo "assembling static frontend → $(DEMO_OUT)"
+ @cp web/demo.html "$(DEMO_OUT)/index.html"
+ @cp web/manifest.webmanifest web/catalog.json web/icon-192.png web/icon-512.png web/apple-touch-icon.png "$(DEMO_OUT)/"
+ @cp -R web/src web/vendor web/glyphs web/basemap "$(DEMO_OUT)/"
+ @echo " demo bundle ready: $(DEMO_OUT)/ — host it on any static server / CDN"
+
+# LOCAL PREVIEW ONLY. The bundle is pure static files — deploy it to ANY
+# range-capable static host (GitHub Pages, S3/CloudFront, nginx, `npx serve`); it
+# needs no backend. PMTiles are read with HTTP Range, which python's http.server
+# does NOT support, so we preview with the chartplotter binary acting purely as a
+# range-capable static file server (the widget page makes no /api calls).
+serve-demo: demo ## Preview the static demo bundle locally (range-capable static serve; HOST/PORT overridable)
+ @echo " Read-only widget demo — open: http://$(HOST):$(PORT)/"
+ $(BIN) serve --host $(HOST) --port $(PORT) --assets "$(DEMO_OUT)" \
+ $(if $(wildcard $(S101_PC)),--s101 "$(S101_PC)" --s101-fc "$(S101_FC)" --cache "$(S101_CACHE)")
+
docs: ## Run the documentation site dev server (Docusaurus; DOCS_HOST/DOCS_PORT overridable)
cd docs && { [ -d node_modules ] || npm install; } && npm start -- --host $(DOCS_HOST) --port $(DOCS_PORT)
diff --git a/README.md b/README.md
index 73689cc..64fe7cc 100644
--- a/README.md
+++ b/README.md
@@ -13,12 +13,16 @@
-
- The North Carolina coast off Cape Lookout — NOAA ENC data, S-101 portrayal, ~1:700,000.
+
+
+
+ ▶ Open the live, interactive demo — official NOAA charts of Annapolis, rendered in your browser. No install, no server.
---
diff --git a/cmd/chartplotter/serve.go b/cmd/chartplotter/serve.go
index 1d521bb..860957d 100644
--- a/cmd/chartplotter/serve.go
+++ b/cmd/chartplotter/serve.go
@@ -35,6 +35,7 @@ func (c serveCmd) Run() error {
// on its own (baker.applyPortrayer); here we emit the matching client assets
// (colortables/sprite/patterns/linestyles) into a temp dir and serve them.
var catalogFS fs.FS
+ var s101AssetDir string // freshly-emitted S-101 client assets (temp dir), or ""
switch {
case c.S101 != "":
if c.S101FC == "" {
@@ -63,7 +64,12 @@ func (c serveCmd) Run() error {
if _, err := assets.EmitS101FS(catalogFS, "daySvgStyle.css", assetDir); err != nil {
return fmt.Errorf("emit S-101 assets: %w", err)
}
- c.Assets = assetDir // override colortables/linestyles/sprite; rest falls back to embedded
+ // The emitted S-101 client assets (colortables/linestyles/sprite/patterns)
+ // are a FALLBACK, not a replacement: an explicit --assets dir stays primary
+ // (so a prebaked widget bundle serves its own index.html / charts-index.json /
+ // .pmtiles), this temp dir fills in the generated S-101 files it lacks, and the
+ // embedded bundle backs the rest. Registered on the Server below.
+ s101AssetDir = assetDir
}
cacheDir := c.Cache
@@ -87,6 +93,7 @@ func (c serveCmd) Run() error {
// other bind means the operator opted into network exposure.
allowRemote := !(c.Host == "127.0.0.1" || c.Host == "localhost" || c.Host == "::1")
srv := server.New(c.Assets, cacheDir, dataDir, allowRemote)
+ srv.SetAssetFallback(s101AssetDir) // emitted S-101 assets, searched after --assets, before embedded
srv.Version = version
srv.ReportStaleCache() // loud warning if any served pack predates this binary
diff --git a/docs/.gitignore b/docs/.gitignore
index 4bbd4fe..7ecad62 100644
--- a/docs/.gitignore
+++ b/docs/.gitignore
@@ -8,6 +8,10 @@
.docusaurus
.cache-loader
+# Live demo bundle — generated by `make demo DEMO_OUT=docs/static/demo` in CI
+# (the read-only widget app + baked Annapolis .pmtiles), never committed.
+/static/demo/
+
# Misc
.DS_Store
.env.local
diff --git a/docs/docs/getting-started.md b/docs/docs/getting-started.md
index 6a9c0dc..404500d 100644
--- a/docs/docs/getting-started.md
+++ b/docs/docs/getting-started.md
@@ -64,6 +64,8 @@ restrictions, light characteristics, depths, source dates, and any attached text
## Where to go next
+- Put a chart on your own page — see [Widget mode](./widget.md) for the read-only,
+ no-server build and how to package and deploy it.
- Learn how a cell becomes tiles in [Architecture](./architecture.md).
- See the exact tile layers and fields in the [Tile Schema](./tile-schema.md).
- Look up the commands in the [CLI Reference](./cli.md) if you want to bake
diff --git a/docs/docs/intro.md b/docs/docs/intro.mdx
similarity index 91%
rename from docs/docs/intro.md
rename to docs/docs/intro.mdx
index 6ee5fb5..cdae1db 100644
--- a/docs/docs/intro.md
+++ b/docs/docs/intro.mdx
@@ -5,6 +5,8 @@ slug: /
sidebar_position: 1
---
+import LiveChart from '@site/src/components/LiveChart';
+
# chartplotter
:::warning Not for navigation
@@ -19,7 +21,12 @@ real-world navigation.**
**chartplotter** is a marine chart engine written in Go. It turns official NOAA
nautical charts into fast, offline map tiles that you can view in a web browser.
-
+The chart below is **live** — official NOAA charts of Annapolis, Maryland, running
+right here in your browser. Pan, zoom from the whole Chesapeake to the docks, and
+switch Day / Dusk / Night. It is the read-only [**widget**](./widget.md) build:
+static, embeddable, no server.
+
+
It reads **S-57** electronic navigational chart (ENC) cells, draws them with the
**S-101 Portrayal Catalogue** — the modern IHO standard for how charts look — and
diff --git a/docs/docs/widget.md b/docs/docs/widget.md
new file mode 100644
index 0000000..2b32840
--- /dev/null
+++ b/docs/docs/widget.md
@@ -0,0 +1,108 @@
+---
+id: widget
+title: Widget
+sidebar_position: 4
+---
+
+# Widget mode
+
+**Widget mode** is a static, embeddable, read-only build of `` — the
+same viewer you see [on the home page](./intro.mdx). It has **no backend and does no
+in-browser baking**: the chart is a handful of prebaked
+[PMTiles](./architecture.md) archives streamed straight from a static host over HTTP
+range requests. It is how you put a chartplotter chart on a web page or ship a
+self-contained demo.
+
+Widget mode strips the management chrome down to a pure viewer:
+
+- **No chart library, import, or downloads** — the charts are fixed.
+- **No share link, no connections (NMEA/AIS), no Advanced / developer tools.**
+- **Only the client-side display settings** — General, Text, Units, and Depths —
+ applied live and stored locally in the browser. Nothing is sent anywhere.
+
+:::warning Not for navigation
+A widget renders a fixed set of cached cells. **Do not use it for real-world
+navigation.** See [Known limitations](./limitations.md).
+:::
+
+## Enable it
+
+Widget mode is turned on by the `widget` attribute (or the `?widget` query param).
+Point the viewer at a prebaked archive — either a per-band set discovered from a
+`charts-index.json` next to the page, or a single archive via `pmtiles="…"`:
+
+```html
+
+
+```
+
+The `assets` attribute is the key: the widget resolves **everything** relative to it —
+`vendor/maplibre-gl.js`, the S-101 client assets, `glyphs/`, `basemap/`,
+`catalog.json`, and `charts-index.json`. Put the whole bundle in one directory and
+point `assets` at it. (That is exactly how the [home page](./intro.mdx) embeds the
+live chart, via the `` React component in `docs/src/components/`.)
+
+## Package it
+
+One make target assembles a complete bundle — it downloads the curated cells, bakes
+the per-band archives, emits the S-101 client assets, and copies in the static
+frontend:
+
+```sh
+make demo # → dist/demo/
+make demo DEMO_OUT=path/to/out # → a directory of your choosing
+make serve-demo # build it, then preview at http://127.0.0.1:8080
+```
+
+The result is a self-contained static site. `dist/demo/` contains:
+
+| What | Files |
+| --- | --- |
+| Per-band tiles + manifest | `demo-.pmtiles`, `charts-index.json` |
+| External text/picture files | `demo-aux.zip` |
+| S-101 client assets | `colortables.json`, `linestyles.json`, `sprite.{json,png}`, `patterns.{json,png}` |
+| Frontend | `index.html`, `src/`, `vendor/`, `glyphs/`, `basemap/`, `catalog.json` |
+
+To bake a different location, override the cells (and re-centre the page):
+
+```sh
+make demo DEMO_CELLS="US2EC03M US3EC08M US4MD1DC US5MD1MC"
+```
+
+You can also assemble a bundle by hand from the CLI:
+
+```sh
+chartplotter bake CELLS… -o out/demo.pmtiles --bands --manifest out/charts-index.json
+chartplotter emit-assets out/ # the S-101 client assets
+# then copy web/{index.html,src,vendor,glyphs,basemap,catalog.json} into out/
+```
+
+## Deploy it
+
+The bundle is **pure static files** — copy the directory to any static host. The only
+hard requirement is **HTTP range request support**, because the PMTiles archives are
+read with byte ranges. Every real static host has it:
+
+- **GitHub Pages** — what this project uses; CI builds the bundle into
+ `docs/static/demo/` so it deploys at `//demo/`.
+- **Amazon S3 + CloudFront** — serve `.pmtiles` as `application/octet-stream` and
+ allow long-lived immutable caching (see `web/customHttp.yml` for the header set).
+- **nginx / Caddy / Netlify / Cloudflare Pages** — work out of the box.
+
+What does **not** work: `python3 -m http.server` — it ignores `Range` and returns the
+whole archive for every tile fetch. For a quick local preview without deploying,
+`make serve-demo` runs the `chartplotter` binary purely as a range-capable static file
+server (the widget page itself makes no API calls).
+
+A few small things hosts occasionally need:
+
+- Serve `.pmtiles` with `Content-Type: application/octet-stream` and
+ `Accept-Ranges: bytes`.
+- Keep relative paths intact — the bundle uses only relative URLs, so it works under
+ any sub-path (e.g. `/chartplotter/demo/`) without configuration.
+- **Cross-origin embedding:** if the page and the bundle are on *different* origins
+ (e.g. your blog embeds a chart hosted on GitHub Pages), the bundle's host must send
+ `Access-Control-Allow-Origin` — the assets are fetched with `fetch()` and the sprite
+ image with `crossOrigin="anonymous"`. GitHub Pages, S3/CloudFront, and the
+ `chartplotter` server already send `*`. A **same-origin** embed — like this docs
+ site, where the page and the bundle share a host — needs nothing.
diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js
index 56ed4d0..6f42480 100644
--- a/docs/docusaurus.config.js
+++ b/docs/docusaurus.config.js
@@ -50,6 +50,11 @@ const config = {
navbar: {
title: 'chartplotter',
items: [
+ {
+ href: 'https://beetlebugorg.github.io/chartplotter/demo/',
+ label: 'Live demo',
+ position: 'right',
+ },
{
href: 'https://github.com/beetlebugorg/chartplotter',
label: 'GitHub',
diff --git a/docs/sidebars.js b/docs/sidebars.js
index ec8ea03..cc80ea9 100644
--- a/docs/sidebars.js
+++ b/docs/sidebars.js
@@ -6,6 +6,7 @@ const sidebars = {
'intro',
'installation',
'getting-started',
+ 'widget',
'cli',
'architecture',
'tile-schema',
diff --git a/docs/src/components/LiveChart.js b/docs/src/components/LiveChart.js
new file mode 100644
index 0000000..c7793db
--- /dev/null
+++ b/docs/src/components/LiveChart.js
@@ -0,0 +1,46 @@
+import React, {useEffect} from 'react';
+import BrowserOnly from '@docusaurus/BrowserOnly';
+import useBaseUrl from '@docusaurus/useBaseUrl';
+
+// LiveChart embeds the read-only "widget" build of live in the
+// page. The chart, MapLibre, and every asset (tiles, sprite, glyphs, basemap,
+// catalog) load from the prebaked demo bundle the docs build assembles under
+// //demo/ — see `make demo` and .github/workflows/docs.yml. Build it
+// locally first with: make demo DEMO_OUT=docs/static/demo
+function Chart() {
+ // useBaseUrl prefixes the site baseUrl, e.g. "/chartplotter/demo/". The widget
+ // resolves ALL of its assets (incl. vendor/maplibre-gl.js and charts-index.json)
+ // relative to this, so the whole demo is self-contained in that one directory.
+ const base = useBaseUrl('/demo/');
+ useEffect(() => {
+ const id = 'chartplotter-widget-module';
+ if (document.getElementById(id)) return; // define once
+ const s = document.createElement('script');
+ s.type = 'module';
+ s.id = id;
+ s.src = `${base}src/chartplotter.mjs`;
+ document.head.appendChild(s);
+ }, [base]);
+ return (
+ <>
+
+ {/* widget = read-only viewer; assets points every fetch at the demo bundle */}
+
+
+ >
+ );
+}
+
+export default function LiveChart() {
+ return (
+ Loading live chart…}
+ >
+ {() => }
+
+ );
+}
diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css
index 7f8a45e..c5bfc35 100644
--- a/docs/src/css/custom.css
+++ b/docs/src/css/custom.css
@@ -24,3 +24,31 @@
--ifm-color-primary-lightest: #b3e0f5;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
}
+
+/* The embed (the read-only widget chart, mounted on the intro page).
+ A fixed-height, rounded frame; the widget fills it and floats its own chrome. */
+.liveChart {
+ height: 68vh;
+ min-height: 440px;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 2px 20px rgba(0, 0, 0, 0.18);
+ margin: 1.5rem 0 0.5rem;
+ background: #0b1b2b;
+}
+.liveChart chart-plotter {
+ display: block;
+ width: 100%;
+ height: 100%;
+}
+.liveChart--loading {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--ifm-color-emphasis-600);
+}
+.liveChart__caption {
+ margin: 0 0 1.5rem;
+ font-size: 0.85rem;
+ text-align: right;
+}
diff --git a/internal/engine/server/http.go b/internal/engine/server/http.go
index 58a241d..641c2e8 100644
--- a/internal/engine/server/http.go
+++ b/internal/engine/server/http.go
@@ -30,13 +30,14 @@ import (
// embedded copy. Downloaded raw cells are cached under cacheDir/ENC_ROOT. The
// zero value is not usable; use New.
type Server struct {
- assetsDir string // optional on-disk asset override (dev); "" → embedded only
- cacheDir string // XDG cache root: REGENERABLE baked tile sets (NOAA//*.pmtiles)
- dataDir string // XDG data root: SOURCE ENC (district zips, raw cells) — safe, not auto-deleted
- allowRemote bool
- share shareStore // latest "share my view" snapshot (camera + cell list)
- settings settingsStore // persisted client display settings (/client-settings.json)
- Version string // build version
+ assetsDir string // optional on-disk asset override (dev); "" → embedded only
+ assetsFallback string // secondary on-disk asset root (emitted S-101 assets), searched after assetsDir, before embedded
+ cacheDir string // XDG cache root: REGENERABLE baked tile sets (NOAA//*.pmtiles)
+ dataDir string // XDG data root: SOURCE ENC (district zips, raw cells) — safe, not auto-deleted
+ allowRemote bool
+ share shareStore // latest "share my view" snapshot (camera + cell list)
+ settings settingsStore // persisted client display settings (/client-settings.json)
+ Version string // build version
sets *tileSets // registry of ENABLED tile sets served at /tiles/{set}/…
imports *importJobs // background server-side bake jobs (POST /api/import)
@@ -361,8 +362,15 @@ func isCellName(s string) bool {
return true
}
+// SetAssetFallback registers a secondary on-disk asset root, searched AFTER the
+// primary --assets dir and BEFORE the embedded bundle. Used to serve the freshly
+// emitted S-101 client assets (sprite/colortables/…) from a temp dir without
+// shadowing an explicit --assets directory (e.g. a prebaked widget bundle that
+// already carries its own copies). Pass "" to disable.
+func (s *Server) SetAssetFallback(dir string) { s.assetsFallback = dir }
+
// serveAsset serves a static web asset: an on-disk --assets override (if set and
-// present) or the embedded bundle.
+// present), then the emitted S-101 asset fallback, then the embedded bundle.
func (s *Server) serveAsset(w http.ResponseWriter, r *http.Request) {
rel := r.URL.Path
if rel == "" || rel == "/" {
@@ -384,6 +392,17 @@ func (s *Server) serveAsset(w http.ResponseWriter, r *http.Request) {
return
}
}
+ // Secondary on-disk root: the freshly-emitted S-101 client assets (sprite/
+ // colortables/…). Searched only after the primary --assets dir so an explicit
+ // bundle's own files win, and before the embedded copy so a `make`-less serve
+ // still gets the generated assets.
+ if s.assetsFallback != "" {
+ full := filepath.Join(s.assetsFallback, filepath.FromSlash(name))
+ if fi, err := os.Stat(full); err == nil && !fi.IsDir() {
+ s.serveFile(w, r, full, rel)
+ return
+ }
+ }
s.serveEmbedded(w, r, name, rel)
}
diff --git a/scripts/fetch-demo-cells.sh b/scripts/fetch-demo-cells.sh
new file mode 100755
index 0000000..66d5359
--- /dev/null
+++ b/scripts/fetch-demo-cells.sh
@@ -0,0 +1,29 @@
+#!/usr/bin/env bash
+# Download the curated NOAA ENC cells for the read-only Annapolis demo, one per
+# usage band. NOAA publishes no overview (band 1) or berthing (band 6) cell over
+# this spot, so the set is bands 2-5 (general -> harbor); the general band renders
+# the zoomed-out view, so the full zoom range still works on little disk.
+#
+# Idempotent: a cell already present in the cache dir is skipped, so CI can cache
+# the directory across runs. Driven by `make demo`; override via env:
+# DEMO_CELLS space-separated NOAA cell IDs (default below)
+# DEMO_CACHE output dir (default ./.demo-cache)
+# NOAA_URL_BASE download host (default https://www.charts.noaa.gov/ENCs)
+set -euo pipefail
+
+BASE="${NOAA_URL_BASE:-https://www.charts.noaa.gov/ENCs}"
+OUT="${DEMO_CACHE:-.demo-cache}"
+CELLS="${DEMO_CELLS:-US2EC03M US3EC08M US4MD1DC US5MD1MC}"
+
+mkdir -p "$OUT"
+for c in $CELLS; do
+ if [ -s "$OUT/$c.zip" ]; then
+ echo "cached $c"
+ continue
+ fi
+ echo "fetch $c.zip"
+ curl -fSL --retry 3 -o "$OUT/$c.zip" "$BASE/$c.zip"
+done
+
+echo "demo cells ready in $OUT:"
+ls -1 "$OUT"
diff --git a/scripts/shot-s64.mjs b/scripts/shot-s64.mjs
index 471fa85..ab37c29 100644
--- a/scripts/shot-s64.mjs
+++ b/scripts/shot-s64.mjs
@@ -1,7 +1,7 @@
// Render an S-64 test cell headlessly with explicit mariner settings, for
// comparing our portrayal against the S-64 reference plots. Presets localStorage
// (mariner settings, day scheme, NOAA agreement) BEFORE the app boots, points the
-// app at a single-cell catalog (?prod&catalog=…) which auto-frames the cell, then
+// app at a single-cell catalog (?widget&catalog=…) which auto-frames the cell, then
// screenshots the clean map after the wasm baker settles.
//
// Usage: node scripts/shot-s64.mjs [w] [h] [settle-ms]
diff --git a/web/demo.html b/web/demo.html
new file mode 100644
index 0000000..d920441
--- /dev/null
+++ b/web/demo.html
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+ chartplotter — live demo (Annapolis)
+
+
+
+
+
+
Loading charts…
+
+
+
+
+
+
diff --git a/web/index.html b/web/index.html
index 13e7946..ed10151 100644
--- a/web/index.html
+++ b/web/index.html
@@ -62,7 +62,7 @@
`.000`; the cells are uploaded and the server bakes them (POST /api/import)
into a tile set the renderer reads from /tiles/{set}. Every S-52 mariner
setting is then applied client-side. A hosted deployment can instead serve
- prebaked per-region `.pmtiles` (the `prod` mode / `pmtiles=` attribute).
+ prebaked per-region `.pmtiles` (the `widget` mode / `pmtiles=` attribute).
Prereqs in this dir:
colortables/sprite/linestyles/patterns + glyphs (`chartplotter --emit-assets web`),
vendor/maplibre-gl.*, basemap/coastline.geojson (`scripts/fetch-basemap.sh`),
diff --git a/web/src/chartplotter.mjs b/web/src/chartplotter.mjs
index 995ec45..e7fb5a4 100644
--- a/web/src/chartplotter.mjs
+++ b/web/src/chartplotter.mjs
@@ -278,17 +278,18 @@ export class ChartPlotter extends HTMLElement {
}
async boot() {
- // Prod (prebaked) mode: enabled by the `prod` attribute OR `?prod` (so a dev
- // build can be tested without rebuilding). Reflect it as the attribute so the
- // :host([prod]) styles apply either way.
- this._prod = this.hasAttribute("prod") || new URLSearchParams(location.search).has("prod");
- if (this._prod) this.setAttribute("prod", "");
+ // Widget (read-only, prebaked) mode: a static, embeddable chart viewer with no
+ // backend and no in-browser baking. Enabled by the `widget` attribute OR
+ // `?widget` (so a dev build can be tested without rebuilding). Reflect it as the
+ // attribute so the :host([widget]) styles apply either way.
+ this._widget = this.hasAttribute("widget") || new URLSearchParams(location.search).has("widget");
+ if (this._widget) this.setAttribute("widget", "");
// Display settings (scheme · basemap · mariner toggles · cell-boundary toggle ·
// bands-off) are persisted SERVER-side so every screen pointed at this boat's
// server shares them and they survive a restart. Adopt them BEFORE the renderer
// is configured below, overriding the localStorage values the constructor seeded
- // (localStorage stays as the offline cache). Prod (offline pmtiles) has no server.
- if (!this._prod) await this._loadServerSettings();
+ // (localStorage stays as the offline cache). The widget viewer (offline pmtiles) has no server.
+ if (!this._widget) await this._loadServerSettings();
this.renderChrome();
// What's already stored? We do NOT eagerly load it (that's the slow part on
@@ -331,7 +332,7 @@ export class ChartPlotter extends HTMLElement {
// .pmtiles archive path (the plotter is shell-owned).
this._chartLib = this.shadowRoot.getElementById("chart-lib");
if (this._chartLib) {
- this._chartLib.configure({ dl: this._dl, api: this._api, notify: this._notify, store: this._store, assets: this._assets, prod: this._prod });
+ this._chartLib.configure({ dl: this._dl, api: this._api, notify: this._notify, store: this._store, assets: this._assets, widget: this._widget });
this._chartLib.setHiddenCells([...this._hiddenCells]); // seed checkbox state from persisted prefs
this._chartLib.addEventListener("charts-changed", () => { this._renderInstalledSets().catch(() => {}); });
// Per-cell show/hide: apply the client filter to the live map + persist.
@@ -352,7 +353,12 @@ export class ChartPlotter extends HTMLElement {
// themselves as the first NON-core contribution (a DevTools instance built in
// onReady, once the map exists — see below); plugins will register here too.
this._settingsRegistry = new SettingsRegistry();
- for (const c of coreSettingsContributions(this)) this._settingsRegistry.register(c);
+ for (const c of coreSettingsContributions(this)) {
+ // The widget viewer is read-only: drop the Advanced tab (its only entry is the
+ // cell-boundary toggle; the dev tools never register in widget mode anyway).
+ if (this._widget && c.id === "core-advanced") continue;
+ this._settingsRegistry.register(c);
+ }
this._settingsDlg = this.shadowRoot.getElementById("settings-dlg");
if (this._settingsDlg) this._settingsDlg.configure({ registry: this._settingsRegistry });
@@ -385,12 +391,12 @@ export class ChartPlotter extends HTMLElement {
if (!["coastline", "osm", "osmvec", "none"].includes(this._basemap)) this._basemap = "coastline";
if (this._basemap === "osmvec" && !this._osmVecUrl) this._basemap = "coastline"; // vector not configured
plotter.setAttribute("basemap", this._basemap);
- // Render source. Prod (hosted): prebaked per-region .pmtiles, loaded by
+ // Render source. Widget (hosted): prebaked per-region .pmtiles, loaded by
// restoreArchive through the renderer's pmtiles path — no tile server needed.
// Local serve: server-baked MVT from /tiles/{set} (server mode); imported /
// downloaded cells are baked on the server (POST /api/import) and the renderer
// is pointed at the resulting set (see _refreshCharts).
- if (!this._prod) plotter.setAttribute("tiles", "server");
+ if (!this._widget) plotter.setAttribute("tiles", "server");
this._plotter = plotter;
this.shadowRoot.getElementById("map").appendChild(plotter);
@@ -483,7 +489,7 @@ export class ChartPlotter extends HTMLElement {
// external files); load it so the pick report can show that content inline.
this._aux = new AuxStore();
const dl = this._dl.loadCatalog().then(async (r) => {
- // Prod/hosted: a companion aux.zip named in the manifest (fetched whole once).
+ // Widget/hosted: a companion aux.zip named in the manifest (fetched whole once).
// Server: per-file on demand via GET api/aux — the raw zip is never exposed.
if (this._dl.auxUrl) await this._aux.load(this._dl.auxUrl);
else await this._aux.loadApi(this._assets);
@@ -532,9 +538,9 @@ export class ChartPlotter extends HTMLElement {
// sticks and you can magnify past the floor (the 1:900 over-zoom).
map.on("moveend", () => this._applyScaleFloor());
await this.restoreArchive();
- // Local serve: render every baked pack the server holds (survives reload). Prod
+ // Local serve: render every baked pack the server holds (survives reload). The widget viewer
// already loaded its prebaked archives in restoreArchive() above.
- if (!this._prod) {
+ if (!this._widget) {
try { await this._renderInstalledSets(); } catch (e) { console.warn("[charts] initial render", e); }
}
this._applyBandsOff(); // re-apply any persisted band on/off now that chart layers exist
@@ -587,7 +593,7 @@ export class ChartPlotter extends HTMLElement {
// settings registry. A plain class (like the map controllers), built now that
// the map exists; it registers itself as the Advanced-tab contribution and owns
// the rebake + feature-inspector tools (and their map listeners). Dev-only.
- if (!this._prod) {
+ if (!this._widget) {
this._devTools = new DevTools({
registry: this._settingsRegistry,
map,
@@ -614,8 +620,8 @@ export class ChartPlotter extends HTMLElement {
// connection manager). VesselStateStore streams /api/vessel for the render
// plugins (own-ship/AIS/HUD); ConnectionsController contributes the
// Connections settings tab for managing data sources.
- if (!this._prod) {
- this._vessel = new VesselStateStore({ assets: this._assets, prod: this._prod });
+ if (!this._widget) {
+ this._vessel = new VesselStateStore({ assets: this._assets, widget: this._widget });
this._vessel.start();
this._connections = new ConnectionsController({
registry: this._settingsRegistry,
@@ -632,7 +638,7 @@ export class ChartPlotter extends HTMLElement {
// re-centre chip mounted in the shell chrome) and streams fixes to the camera.
this._ownShip = new OwnShip({ map, plotter: this._plotter, vessel: this._vessel, host: this.shadowRoot, onSelect: showInfo, units: () => this._mariner });
// AIS targets (other vessels) from the live feed.
- this._ais = new AISOverlay({ map, assets: this._assets, prod: this._prod, onSelect: showInfo, units: () => this._mariner });
+ this._ais = new AISOverlay({ map, assets: this._assets, widget: this._widget, onSelect: showInfo, units: () => this._mariner });
}
// Persist the view so a refresh resumes where you were; refresh the coverage
@@ -849,7 +855,7 @@ export class ChartPlotter extends HTMLElement {
// popup" when entering via the welcome "Browse chart regions" button).
r.getElementById("drawer").classList.toggle("wide", true);
r.getElementById("drawer").classList.toggle("set-wide", false);
- r.getElementById("dtitle").textContent = this._prod ? "Add charts" : "Chart library";
+ r.getElementById("dtitle").textContent = this._widget ? "Add charts" : "Chart library";
r.getElementById("empty").hidden = true;
if (this._chartLib) this._chartLib.show(provider); // no default provider — nothing selected until the user picks
this.setDrawerOpen(true);
@@ -1490,7 +1496,7 @@ export class ChartPlotter extends HTMLElement {
// Re-bake the local OPFS store on the server (the User-Charts import path). The
// component owns this; the shell's debug tools delegate to it.
- // Returns a resolved promise when there's no component yet (boot/prod guards).
+ // Returns a resolved promise when there's no component yet (boot/widget guards).
_refreshCharts() {
return this._chartLib ? this._chartLib._refreshCharts() : Promise.resolve();
}
@@ -1536,12 +1542,12 @@ export class ChartPlotter extends HTMLElement {
_refreshInstalledBounds() {
if (!this._coverage) return; // coverage overlay not set up yet
const feats = [];
- // Per-CELL footprints are the prod (pmtiles) path only. In SERVER mode we draw
+ // Per-CELL footprints are the widget (pmtiles) path only. In SERVER mode we draw
// one box per ENABLED pack (below) instead — a full NOAA install has thousands
// of cells, and a box per cell (re-projected to its min on-screen size on every
// zoom frame) would freeze the map. Per-cell boxes also ignore the enabled flag,
// so they'd keep showing a disabled district's coverage. Per-pack boxes fix both.
- if (this._prod) {
+ if (this._widget) {
for (const name of this._installed) {
const bb = this._cellLocation(name); // catalog footprint
if (!bb) continue;
@@ -1697,7 +1703,7 @@ export class ChartPlotter extends HTMLElement {
// Fetch the server-persisted display settings at boot and adopt them over the
// localStorage values the constructor seeded. Best-effort: an older server, an
- // offline/prod load, or a malformed blob just keeps the local values.
+ // offline/widget load, or a malformed blob just keeps the local values.
async _loadServerSettings() {
let s = null;
try {
@@ -1717,10 +1723,10 @@ export class ChartPlotter extends HTMLElement {
}
// Persist the display settings server-side (shared across screens). Debounced so
- // a flurry of toggles coalesces into one POST. Server mode only — prod has no
+ // a flurry of toggles coalesces into one POST. Server mode only — the widget viewer has no
// server, and localStorage already holds the per-screen copy.
_persistSettings() {
- if (this._prod) return;
+ if (this._widget) return;
clearTimeout(this._settingsSaveT);
this._settingsSaveT = setTimeout(() => {
fetch(`${this._assets}api/settings`, {
@@ -1862,7 +1868,7 @@ export class ChartPlotter extends HTMLElement {
r.querySelectorAll(".panel").forEach((p) => p.classList.toggle("sel", p.dataset.panel === name));
r.getElementById("drawer").classList.toggle("wide", name === "charts"); // two-pane list+map
r.getElementById("drawer").classList.toggle("set-wide", name === "settings"); // rail + content
- r.getElementById("dtitle").textContent = name === "settings" ? "Settings" : (this._prod ? "Add charts" : "Chart library");
+ r.getElementById("dtitle").textContent = name === "settings" ? "Settings" : (this._widget ? "Add charts" : "Chart library");
// Charts = the panel; open it on its current provider (none
// selected by default — the user picks a source).
if (name === "charts" && this._chartLib) this._chartLib.show(this._chartLib._selProvider);
@@ -1943,7 +1949,7 @@ export class ChartPlotter extends HTMLElement {
_hasInstalledPacks() { return !!(this._installedSets && this._installedSets.size); }
// Installed packs exist but none render (all disabled) → nothing on the map.
- _noChartsEnabled() { return !this._prod && this._hasInstalledPacks() && !this._hasArchive; }
+ _noChartsEnabled() { return !this._widget && this._hasInstalledPacks() && !this._hasArchive; }
// The archive-list rendering + file-import wiring (renderArchiveList /
// _wireImport) moved into , which owns the User-Charts import UI.
diff --git a/web/src/chartplotter.view.mjs b/web/src/chartplotter.view.mjs
index 7f30692..74ea6d6 100644
--- a/web/src/chartplotter.view.mjs
+++ b/web/src/chartplotter.view.mjs
@@ -77,11 +77,14 @@ export const STYLE = `
.rbtn:active { transform:scale(.94); }
.rbtn.on { background:var(--ui-accent); color:var(--ui-accent-text); border-color:var(--ui-accent); }
.rbtn svg { width:21px; height:21px; display:block; }
- /* Prod / prebaked deployment: charts load from a configured hosted archive
- (pmtiles="…" / catalog="…"); there's no NOAA download and no Dev tools.
- The Charts button stays but its panel becomes import-only (drop your own
- ENC, baked server-side) — see renderCharts. */
- :host([prod]) #empty-add, :host([prod]) #empty .welcome-sub { display:none; }
+ /* Widget mode: a read-only, embeddable chart viewer (a "CDN"/widget deploy).
+ Charts load from a configured hosted archive (pmtiles="…" / catalog="…");
+ there's no backend, no NOAA download, no in-browser baking, no Dev tools.
+ It strips the management chrome down to a pure viewer: hide the Charts
+ (library/import) and Share buttons, and the welcome card's add/import
+ affordances. Settings drop the Advanced tab (gated in JS). */
+ :host([widget]) #empty-add, :host([widget]) #empty .welcome-sub { display:none; }
+ :host([widget]) #charts-btn, :host([widget]) #share-btn { display:none; }
.box-sel { position:absolute; z-index:5; border:2px solid var(--ui-accent); background:rgba(21,101,192,.12); pointer-events:none; }
/* charts panel: action header + "your charts" cards */
.charts-actions { display:flex; gap:8px; margin-bottom:10px; }
diff --git a/web/src/core/core-settings.mjs b/web/src/core/core-settings.mjs
index c6eecfa..8a75009 100644
--- a/web/src/core/core-settings.mjs
+++ b/web/src/core/core-settings.mjs
@@ -144,8 +144,8 @@ export function coreSettingsContributions(app) {
// inspector / coverage / bands / diagnostics) are rendered by the SHELL in its
// own shadow (#dev-region) because _renderDevPanel / _renderInspect reach into
// the shell's shadow by id; keeping them there avoids a risky refactor (option
- // B). The shell reveals #dev-region only when this tab is active. In prod there
- // are no dev tools, so the tab is just the toggle.
+ // B). The shell reveals #dev-region only when this tab is active. In the widget
+ // viewer there are no dev tools, so the tab is just the toggle.
const advanced = {
id: "core-advanced",
tab: { id: "advanced", label: "Advanced" },
diff --git a/web/src/core/settings-store.mjs b/web/src/core/settings-store.mjs
index ae12032..d877566 100644
--- a/web/src/core/settings-store.mjs
+++ b/web/src/core/settings-store.mjs
@@ -6,7 +6,7 @@
// app's own display settings and each plugin's settings live side by side in one
// store without colliding:
//
-// const settings = new SettingsStore({ assets, prod });
+// const settings = new SettingsStore({ assets, widget });
// settings.load(); // overlay the server copy at boot
// const core = settings.ns("core"); // the app's display settings
// const ais = settings.ns("ais"); // a plugin's settings
@@ -22,19 +22,19 @@
const LS_KEY = "chartplotter:settings";
export class SettingsStore {
- constructor({ assets = "", prod = false } = {}) {
+ constructor({ assets = "", widget = false } = {}) {
this._assets = assets;
- this._prod = prod;
+ this._widget = widget;
this._saveT = 0;
// Seed instantly from the localStorage mirror so boot has values before the
- // server responds (and so prod, which has no server, works at all).
+ // server responds (and so widget, which has no server, works at all).
this._blob = readLocal();
}
// Overlay the server-persisted copy (server mode). Best-effort: offline / older
- // server / prod just keeps the local values. Returns the merged blob.
+ // server / widget just keeps the local values. Returns the merged blob.
async load() {
- if (this._prod) return this._blob;
+ if (this._widget) return this._blob;
try {
const r = await fetch(`${this._assets}api/settings`, { cache: "no-store" });
if (r.ok) {
@@ -67,7 +67,7 @@ export class SettingsStore {
// Mirror locally now; debounce the server POST so a flurry of toggles coalesces.
_dirty() {
writeLocal(this._blob);
- if (this._prod) return; // no server in prod; localStorage is the store
+ if (this._widget) return; // no server in widget; localStorage is the store
clearTimeout(this._saveT);
this._saveT = setTimeout(() => {
fetch(`${this._assets}api/settings`, {
diff --git a/web/src/data/chart-downloader.mjs b/web/src/data/chart-downloader.mjs
index 1ee44dd..e3dc302 100644
--- a/web/src/data/chart-downloader.mjs
+++ b/web/src/data/chart-downloader.mjs
@@ -49,9 +49,15 @@ export class ChartDownloader {
const man = fetch(manUrl)
.then((r) => (r.ok ? r.json() : null))
.then((j) => {
- this.districts = (j && j.districts) || [];
- // The aux zip sits beside the manifest; resolve its basename against manUrl.
- this.auxUrl = j && j.aux ? manUrl.replace(/[^/]*$/, j.aux) : "";
+ // Manifest paths (the district archives and the aux zip) are relative to the
+ // MANIFEST, not to the page. Resolve them against manUrl so a widget embedded
+ // on a page in a DIFFERENT directory (assets ≠ the page's dir, e.g. a chart
+ // dropped onto a docs page) still finds the .pmtiles — otherwise the renderer
+ // resolves the bare filenames against the page URL and 404s.
+ const manAbs = new URL(manUrl, location.href);
+ this.districts = ((j && j.districts) || []).map((d) =>
+ d && d.file ? { ...d, file: new URL(d.file, manAbs).href } : d);
+ this.auxUrl = j && j.aux ? new URL(j.aux, manAbs).href : "";
})
.catch(() => { this.districts = []; this.auxUrl = ""; });
return Promise.all([cat, man]);
diff --git a/web/src/data/vessel-state-store.mjs b/web/src/data/vessel-state-store.mjs
index 42ae777..5840e92 100644
--- a/web/src/data/vessel-state-store.mjs
+++ b/web/src/data/vessel-state-store.mjs
@@ -5,13 +5,13 @@
// no DOM and renders nothing — mirroring the server-side "a source, not a
// renderer" split.
//
-// The feed requires the Go server, so in prod (server-less, prebaked) mode it
+// The feed requires the Go server, so in widget (server-less, prebaked) mode it
// stays idle. Where EventSource is unavailable it falls back to polling.
export class VesselStateStore {
- constructor({ assets = "/", prod = false } = {}) {
+ constructor({ assets = "/", widget = false } = {}) {
this._assets = assets;
- this._prod = prod;
+ this._widget = widget;
this._state = {};
this._listeners = new Set();
this._es = null;
@@ -39,9 +39,9 @@ export class VesselStateStore {
}
}
- /** Begin streaming. No-op in prod (no server feed) or if already started. */
+ /** Begin streaming. No-op in widget (no server feed) or if already started. */
start() {
- if (this._prod || this._es || this._polling) return;
+ if (this._widget || this._es || this._polling) return;
if (!window.EventSource) {
this._poll();
return;
diff --git a/web/src/plugins/ais-overlay.mjs b/web/src/plugins/ais-overlay.mjs
index 45818ce..a7871da 100644
--- a/web/src/plugins/ais-overlay.mjs
+++ b/web/src/plugins/ais-overlay.mjs
@@ -27,10 +27,10 @@ const GLYPH_STYLE =
"filter:drop-shadow(0 0 1px var(--ais-halo,#fff)) drop-shadow(0 0 1px var(--ais-halo,#fff));";
export class AISOverlay {
- constructor({ map, assets = "/", prod = false, onSelect, units } = {}) {
+ constructor({ map, assets = "/", widget = false, onSelect, units } = {}) {
this._map = map;
this._assets = assets;
- this._prod = prod;
+ this._widget = widget;
this._units = units; // () => mariner prefs, for SOG/CPA/draught units (live)
this._onSelect = onSelect; // tap → info picker
this._markers = new Map(); // mmsi -> {marker, el, hasDir}
@@ -40,7 +40,7 @@ export class AISOverlay {
}
start() {
- if (this._prod || this._es || this._polling) return; // AIS feed needs the server
+ if (this._widget || this._es || this._polling) return; // AIS feed needs the server
if (!window.EventSource) {
this._poll();
return;
diff --git a/web/src/plugins/chart-library.mjs b/web/src/plugins/chart-library.mjs
index 553cddb..08997da 100644
--- a/web/src/plugins/chart-library.mjs
+++ b/web/src/plugins/chart-library.mjs
@@ -30,7 +30,7 @@ import { esc, fmtIssue } from "../lib/util.mjs";
import { readCentralDirectory, cellEntries, extractEntry } from "../data/zip-import.mjs";
import { seaColor, landColor, coastColor } from "../chart-canvas/s52-style.mjs"; // our own basemap palette (consistent with the chart)
import {
- STYLE, prodBody, libraryBody, packSearch, providersCol, packsHeader,
+ STYLE, widgetBody, libraryBody, packSearch, providersCol, packsHeader,
packBadge, userPackRow, packRow, packsCol, emptyRow, downloadBtn,
detailEmpty, detailUnknownSet, detailPack, installedActions, previewMapHost,
importDetail, dataFreshness, agreementModal, archiveList, millerBack, packCellList,
@@ -130,7 +130,7 @@ export class ChartLibrary extends HTMLElement {
this._previewKey = null; // pack key the detail preview currently targets
this._snapEl = null; this._snapMap = null; // in-flight off-screen snapshot map
this._lightbox = null; this._lightboxKey = null; // tap-to-enlarge overlay
- this._prod = false; // prod (prebaked) build: import-only Library
+ this._widget = false; // widget (prebaked) build: import-only Library
this._active = false; // is the charts UI currently shown?
}
@@ -151,15 +151,15 @@ export class ChartLibrary extends HTMLElement {
if (this._previewMap) { try { this._previewMap.remove(); } catch (e) { /* ignore */ } this._previewMap = null; }
}
- // Inject dependencies (call once after creation). `prod` flips the Library to
- // import-only (no NOAA download/region picker), matching the shell's prod mode.
- configure({ dl, api, notify, store, assets, prod } = {}) {
+ // Inject dependencies (call once after creation). `widget` flips the Library to
+ // import-only (no NOAA download/region picker), matching the shell's widget mode.
+ configure({ dl, api, notify, store, assets, widget } = {}) {
this._dl = dl || null;
this._api = api || null;
this._notify = notify || null;
this._store = store || null;
if (assets) this._assets = assets;
- this._prod = !!prod;
+ this._widget = !!widget;
return this;
}
@@ -273,13 +273,13 @@ export class ChartLibrary extends HTMLElement {
}
// -- rendering ------------------------------------------------------------
- // The whole charts panel. Prod (prebaked) builds get the import-only Library;
+ // The whole charts panel. Widget (prebaked) builds get the import-only Library;
// otherwise the 3-pane drill-down + search + preview.
render() {
const el = this.shadowRoot.getElementById("body");
if (!el) return;
- if (this._prod) {
- el.innerHTML = prodBody();
+ if (this._widget) {
+ el.innerHTML = widgetBody();
this._wireImport();
return;
}
diff --git a/web/src/plugins/chart-library.view.mjs b/web/src/plugins/chart-library.view.mjs
index b343ab0..b08a14c 100644
--- a/web/src/plugins/chart-library.view.mjs
+++ b/web/src/plugins/chart-library.view.mjs
@@ -163,9 +163,9 @@ export const STYLE = `
}
`;
-// The prod (prebaked) Library body: import-only — no NOAA download / region
+// The widget (prebaked) Library body: import-only — no NOAA download / region
// picker. Wired by _wireImport via #file/#drop/#pick.
-export function prodBody() {
+export function widgetBody() {
return `
Add your own charts — drop a NOAA .zip / .000, or a baked .pmtiles. They're baked right here in your browser and kept offline alongside the prebaked charts.
Drop a .zip, .000 or .pmtiles here, or
@@ -174,7 +174,7 @@ export function prodBody() {
`;
}
-// The full (non-prod) Library body: search box + 3-pane miller + freshness
+// The full (non-widget) Library body: search box + 3-pane miller + freshness
// footer. The three columns are passed in pre-rendered (the logic computes the
// data they need).
export function libraryBody({ searchHtml, providersCol, packsCol, detailCol, freshnessHtml, level, backLabel }) {