---
name: vendor-shelf
description: The 36 /vendor/ library URLs — pickers, grids, maps, a view layer, icons, dates, charts, 3D, PDF, CSV, search, rich text, calendars, motion — and the CSP that blocks every CDN
when: Reaching for any library in a published app — a date picker, a table, a map, a multi-view app, an icon, a chart, a PDF — or whether to at all
---

# The shelf

Thirty-six libraries are served from the platform's own host. **Nothing else
loads.** `vendor-traps` has the version gotchas, `game-engines` covers the game
ones in depth, `game-prototype` covers writing a game by hand,
and `state-management` has the htmPreact view layer and the seven-line router.

For an app **published on the platform** (served from `/p/<token>/`) these are
root-relative on the same host: no CORS, no build step. Verified in production.

## ⚠️⚠️ A CDN IS BLOCKED, AND THAT IS THE #1 WAY A GENERATED APP SHIPS DEAD

`<script src="https://cdn.jsdelivr.net/…/phaser.min.js">` is the reflex, and
Content-Security-Policy refuses it. The page loads, the canvas is blank,
`Phaser` is undefined, and the only evidence anywhere is one console line. The
policy permits the app's own files and `/vendor/` — nothing else, no exceptions,
and no amount of retrying a different CDN host will change that.

⚠️ A jsDelivr URL returning **200 is not proof the file exists upstream**: it
minifies on the fly and serves a generated file with a "do NOT use SRI" banner.
Never treat a CDN 200 as evidence of a real build artifact.

## They are global scripts, not ES modules

Each tag defines the global named in its comment below. There is no `import`, no
`require`, no bundler, and `type="module"` will break the UMD builds. Put the
library tag **before** your own script, or your first line runs against an
undefined global and you spend a round debugging the wrong file.

## Prove it actually loaded — the step that gets skipped

```js
if (typeof Konva === 'undefined') {
  document.body.innerHTML = '<pre>Konva did not load — check the /vendor/ URL</pre>';
}
```

A missing library looks exactly like a logic bug. One guard per library turns a
silent blank page into a sentence naming the cause.

## ⭐⭐⭐ FOUR OF THEM NEED **TWO** TAGS

**flatpickr, Tabulator, Leaflet and Quill each need a `<link rel="stylesheet">`
as well as their `<script src>`.** Load only the script and the library *works*
and looks broken: flatpickr renders as a bare column of numbers, Tabulator as
unformatted text, Leaflet as a pile of unpositioned tiles, Quill as a column of
unstyled buttons. Worse than a blank page, because nothing in the console says why.

```html
<!-- pick a date / a time slot — global `flatpickr` -->
<link rel="stylesheet" href="/vendor/flatpickr@4.6.13/flatpickr.min.css">
<script src="/vendor/flatpickr@4.6.13/flatpickr.min.js"></script>

<!-- a sortable, filterable, EDITABLE data grid — global `Tabulator` -->
<link rel="stylesheet" href="/vendor/tabulator-tables@6.5.2/tabulator.min.css">
<script src="/vendor/tabulator-tables@6.5.2/tabulator.min.js"></script>

<!-- an interactive map / a service area — global `L` -->
<link rel="stylesheet" href="/vendor/leaflet@1.9.4/leaflet.css">
<script src="/vendor/leaflet@1.9.4/leaflet.js"></script>

<!-- a rich-text editor — global `Quill` -->
<link rel="stylesheet" href="/vendor/quill@2.0.3/quill.snow.css">
<script src="/vendor/quill@2.0.3/quill.js"></script>
```

```js
// A BOOKING SLOT PICKER. `enable` is what <input type="date"> cannot do: show
// which days are actually free. Use `disable` for the taken ones.
flatpickr('#when', {
  inline: true, enableTime: true, minuteIncrement: 30, minDate: 'today',
  enable: ['2026-09-02', '2026-09-03', '2026-09-05'],
  onChange: ([d]) => { chosen = luxon.DateTime.fromJSDate(d).toISO(); },
});

// A SHEET. `editor` is why this and not a hand-built <table>.
const grid = new Tabulator('#grid', {
  height: '320px',                       // ⚠️ or virtual scrolling collapses it
  data: rows, layout: 'fitColumns',
  columns: [
    { title: 'Item', field: 'item', editor: 'input' },
    { title: 'Qty', field: 'qty', editor: 'number', sorter: 'number' },
    { title: 'Total', field: 'total', sorter: 'number', bottomCalc: 'sum' },
  ],
});
grid.on('cellEdited', (c) => AcuvoData.set('rows', c.getRow().getData().id, c.getRow().getData()));

// A SERVICE AREA. Without the tileLayer the map is a grey rectangle.
const map = L.map('map').setView([-35.28, 149.13], 11);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png',
  { attribution: '© OpenStreetMap', maxZoom: 19 }).addTo(map);
L.circle([-35.28, 149.13], { radius: 25000 }).addTo(map).bindPopup('We cover 25km');
```

⚠️ Traps that each cost a round:

- **flatpickr positions itself absolutely** against the input, so a parent with
  `overflow: hidden` clips the calendar. `inline: true` sidesteps it and is what
  a booking page wants anyway.
- ⚠️⚠️ **Never pass `persistence: true` to Tabulator.** It reads `localStorage`,
  and a published app has no storage at all — it throws `SecurityError`. Persist
  from `cellEdited` into `AcuvoData` instead.
- **Leaflet's container `<div>` needs a real height in CSS** or the map is 0px,
  and **a map with no `tileLayer` is a grey box**. Keep the OpenStreetMap
  attribution; it is a condition of use.
- **Leaflet's global is `L`.** Its UMD also defines `window.leaflet`; ignore it.

## ⭐⭐⭐ THE URLS. Copy them exactly; do not paraphrase a version.

**Pickers, grids and maps** — flatpickr, Tabulator and Leaflet need a `<link>`
too, so copy their pairs from the two-tag block above.

**A real view layer — components, hooks and keyed re-rendering**

```html
<script src="/vendor/htm@3.1.1/standalone.umd.js"></script> <!-- htmPreact -->
```

⚠️ Three idioms are wrong by default with it — keyed list rows, `hashchange`
screens, and a JS router NOT being page navigation. They are in
`state-management`; read it before writing a multi-screen app.

**Games, physics and sound**

```html
<script src="/vendor/phaser@3.90.0/phaser.min.js"></script> <!-- Phaser -->
<script src="/vendor/pixi.js@7.4.3/pixi.min.js"></script> <!-- PIXI -->
<script src="/vendor/matter-js@0.20.0/matter.min.js"></script> <!-- Matter -->
<script src="/vendor/howler@2.2.4/howler.min.js"></script> <!-- Howl -->
```

**Charts, 3D, dataviz, PDF and synthesised audio**

```html
<script src="/vendor/chart.js@4.5.0/chart.umd.min.js"></script> <!-- Chart -->
<script src="/vendor/echarts@6.1.0/echarts.min.js"></script> <!-- echarts — sankey/treemap/sunburst/gauge/candlestick/funnel/network -->
<link rel="stylesheet" href="/vendor/katex@0.18.7/katex.min.css">
<script src="/vendor/katex@0.18.7/katex.min.js"></script> <!-- katex — real maths; NEEDS the CSS above, fonts ship beside it -->
<script src="/vendor/three@0.160.1/three.min.js"></script> <!-- THREE -->
<script src="/vendor/three-gltfloader@0.147.0/GLTFLoader.js"></script> <!-- THREE.GLTFLoader — AFTER three -->
<script src="/vendor/three-orbitcontrols@0.147.0/OrbitControls.js"></script> <!-- THREE.OrbitControls — AFTER three -->
<script src="/vendor/d3@7.9.0/d3.min.js"></script> <!-- d3 -->
<script src="/vendor/jspdf@3.0.3/jspdf.umd.min.js"></script> <!-- jspdf -->
<script src="/vendor/pptxgenjs@4.0.1/pptxgen.bundle.js"></script> <!-- PptxGenJS — .pptx decks -->
<script src="/vendor/docx@9.7.1/index.iife.js"></script> <!-- docx — .docx Word files -->
<script src="/vendor/tone@15.1.22/tone.js"></script> <!-- Tone -->
<script src="/vendor/sortablejs@1.15.6/sortable.min.js"></script><!-- Sortable -->
<script src="/vendor/marked@15.0.7/marked.min.js"></script> <!-- marked -->
```

**Icons, dates, sanitising, PDF tables, screenshots and canvas diagrams**

```html
<script src="/vendor/lucide@0.544.0/lucide.min.js"></script> <!-- lucide -->
<script src="/vendor/luxon@3.7.2/luxon.min.js"></script> <!-- luxon -->
<script src="/vendor/dompurify@3.2.7/purify.min.js"></script> <!-- DOMPurify -->
<script src="/vendor/jspdf-autotable@5.0.2/jspdf.plugin.autotable.min.js"></script>
 <!-- autoTable, LOAD AFTER jspdf -->
<script src="/vendor/html2canvas@1.4.1/html2canvas.min.js"></script><!-- html2canvas -->
<script src="/vendor/konva@9.3.22/konva.min.js"></script> <!-- Konva -->
```

**CSV, fuzzy search, colour maths, rich text, diagrams and a calendar**

```html
<script src="/vendor/papaparse@5.5.3/papaparse.min.js"></script> <!-- Papa -->
<script src="/vendor/fuse.js@7.1.0/fuse.min.js"></script> <!-- Fuse -->
<script src="/vendor/chroma-js@2.4.2/chroma.min.js"></script> <!-- chroma -->
<script src="/vendor/quill@2.0.3/quill.js"></script> <!-- Quill, +CSS above -->
<script src="/vendor/turndown@7.2.0/turndown.js"></script> <!-- TurndownService -->
<script src="/vendor/mermaid@11.4.1/mermaid.min.js"></script> <!-- mermaid -->
<script src="/vendor/fullcalendar@6.1.15/index.global.min.js"></script> <!-- FullCalendar -->
```

- **Papa** reads/writes CSV. ⚠️ `{ header: true }` or rows are arrays; with a
  `File` it is ASYNC and returns undefined — use the `complete` callback.
- **Fuse** is typo-tolerant search over objects. Name the `keys`, and lower
  `threshold` toward 0.3 — the 0.6 default is very loose.
- **chroma** — `chroma.contrast(a, b)` is the WCAG ratio. ⚠️ Interpolate in
  `lch`/`lab`, never the default `rgb` (it passes through a muddy grey):
  `chroma.scale([a,b]).mode('lch').colors(n)`.
- **Quill** — read it with `quill.root.innerHTML`; `getText()` silently drops
  every bit of formatting the user just applied. **TurndownService** turns that
  HTML into Markdown (pass the string, not the Quill instance).
- ⚠️⚠️ **mermaid does NOT auto-run under our CSP.** Its docs assume
  `startOnLoad: true`, which fires before an inline script has defined the
  diagrams. Source in `<pre class="mermaid">`, then
  `mermaid.initialize({ startOnLoad: false })` and `await mermaid.run()`.
- **FullCalendar** needs no separate stylesheet. ⚠️ Name the view
  (`initialView: 'dayGridMonth'`) or you get a toolbar and no grid.

**Motion and the hand-drawn look — the explainer-video pair**

```html
<script src="/vendor/animejs@4.5.0/anime.umd.min.js"></script> <!-- anime -->
<script src="/vendor/roughjs@4.6.6/rough.js"></script> <!-- rough -->
```

Use them together: `rough` draws shapes as if by hand (wobbly strokes, stable
with a seed) and `anime` draws those strokes ON in sequence, so a diagram builds
itself the way it does in an explainer video.
⚠️ **anime.js v4 is not v3 — there is no default `anime({...})` call.** The four
recipes and the `seed` rule are in `vendor-traps`.

**Licences.** All **MIT** except d3 and lucide (**ISC**), DOMPurify (dual
`MPL-2.0 OR Apache-2.0`, we elect Apache-2.0), htm (**Apache-2.0**, with
preact's MIT `LICENSE.preact` beside it) and Leaflet (**BSD-2-Clause**). Each
ships its upstream `LICENSE` beside it.

## ⚠️ Every library has a version trap — they are in `vendor-traps`

The API you remember is often a DIFFERENT MAJOR VERSION from the one at the URL
above, and none of them errors with "wrong version". Read `vendor-traps` before
writing against any of them.

## Where this does NOT apply

A **local project on a real machine** (you have a shell, `npm install` works)
has no `/vendor/` — nothing is serving it. There, install the package
(`npm i phaser`) and import it normally. The URLs above are for pages the
platform publishes.
