col-browser 2.0.0-rc.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,798 @@
1
+ # ChecklistBank ReactJS components
2
+
3
+ This is a small ReactJS component library to visualise datasets in [checklistbank.org](https://www.checklistbank.org/dataset).
4
+ The components work against a single dataset, configured by a datasetKey, and can be used to interact with each other.
5
+
6
+ 1. [Tree](https://catalogueoflife.github.io/portal-components/#tree) - navigate the taxonomic tree of a dataset in checklistbank, linking out to individual taxa
7
+ 2. [Search](https://catalogueoflife.github.io/portal-components/#search) - a search form with filters and a tabular response view
8
+ 3. [Taxon](https://catalogueoflife.github.io/portal-components/#taxon/6W3C4) - comprehensive information of a single taxon including the taxon breakdown and distribution map
9
+ 4. [TaxonBreakdown](https://catalogueoflife.github.io/portal-components/#breakdown/ST) - one or two level pie chart of a taxon's major groups by rank
10
+ 5. [TaxonDistribution](https://catalogueoflife.github.io/portal-components/#distribution/HWCX) - the Taxon page's distribution map (MapLibre + optional GBIF overlay) as a standalone component
11
+ 6. [SourceDatasetList](https://catalogueoflife.github.io/portal-components/#sources) - table of source datasets contributing to a compiled project
12
+ 7. [SourceDataset](https://catalogueoflife.github.io/portal-components/#source/1010) - source dataset details. Relevant for projects compiled from several source datasets providing taxonomic 'sectors'
13
+ 8. [BibTex](https://catalogueoflife.github.io/portal-components/#bibtex/1010) - simple icon that provides a BibTex citation for a dataset
14
+
15
+ ## Examples
16
+
17
+ ### Live demo
18
+
19
+ A hosted demo of the latest released version is at <https://catalogueoflife.github.io/portal-components/>. It exercises every top-level component against the production ChecklistBank API. The page is rebuilt and redeployed by `.github/workflows/pages.yml` on every `v*` tag push.
20
+
21
+ ### Catalogue of Life
22
+ All components are in use on the [main Catalogue of Life portal](https://www.catalogueoflife.org).
23
+
24
+ ![tree](https://user-images.githubusercontent.com/327505/111465911-ed038200-8722-11eb-925c-4d836efe6e1b.png)
25
+
26
+ ![search](https://user-images.githubusercontent.com/327505/111465903-ea089180-8722-11eb-985c-0cbaefba0880.png)
27
+
28
+ ![details](https://user-images.githubusercontent.com/327505/111465894-e6750a80-8722-11eb-8bd7-005f41f023f3.png)
29
+
30
+ ### Catalogue of the Pterophoroidea & Alucitoidea
31
+ - https://pterophoroidea.hobern.net
32
+ - https://alucitoidea.hobern.net
33
+
34
+ ![tree](https://user-images.githubusercontent.com/327505/111465866-dceba280-8722-11eb-9368-31d056593058.png)
35
+
36
+
37
+ ## Usage
38
+
39
+ > The documentation below describes the **2.x** version. For the 1.x version (React 16 / antd 4 era), see the [README on the `v1` branch](https://github.com/CatalogueOfLife/portal-components/blob/v1/README.md). If you're moving from 1.x to 2.x, jump to [Upgrading from 1.x to 2.0](#upgrading-from-1x-to-20) first.
40
+
41
+ These components can be included in any html page. React 19 no longer ships a UMD build of its own, so the UMD bundle of `col-browser` includes React and `react-dom/client` and re-exposes them as `ColBrowser.React` and `ColBrowser.ReactDOM`. No separate React `<script>` tag is needed.
42
+
43
+ Include the library — pin to a major so you don't get migrated unexpectedly:
44
+
45
+ ```html
46
+ <script src="https://cdn.jsdelivr.net/gh/CatalogueOfLife/portal-components@2/umd/col-browser.min.js"></script>
47
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/CatalogueOfLife/portal-components@2/umd/main.css">
48
+ ```
49
+
50
+ Then mount any component using `ColBrowser.React` and `ColBrowser.ReactDOM`:
51
+
52
+ ```html
53
+ <div id="tree"></div>
54
+ <script>
55
+ ColBrowser.ReactDOM.createRoot(document.querySelector('#tree')).render(
56
+ ColBrowser.React.createElement(ColBrowser.Tree, {
57
+ datasetKey: '3LR',
58
+ hrefForTaxon: (id) => `/taxon/${id}`,
59
+ })
60
+ );
61
+ </script>
62
+ ```
63
+
64
+ (`ColBrowser.Taxon` and `ColBrowser.TaxonDistribution` additionally need MapLibre GL JS loaded separately — see those components below.)
65
+
66
+ The full version list is on the [releases page](https://github.com/CatalogueOfLife/portal-components/releases).
67
+
68
+ ### jsDelivr version selectors
69
+
70
+ jsDelivr resolves semver-style git tags, so you can pick the granularity that suits you:
71
+
72
+ | Selector | Resolves to | Use when |
73
+ |---|---|---|
74
+ | `@2` | latest 2.x release | recommended — gets bugfixes, no breaking changes |
75
+ | `@1` | latest 1.x release (React 16 / antd 4 era) | you can't yet migrate the host page to React 19 |
76
+ | `@v2.0.0` | exactly v2.0.0 | you need byte-for-byte reproducibility |
77
+ | `@latest` | most recent release across all majors | **avoid for production** — a future v3 release will silently migrate you |
78
+
79
+ > v1 is in maintenance mode on the [`v1`](https://github.com/CatalogueOfLife/portal-components/tree/v1) branch. Critical fixes will be backported there as v1.x patch releases; no new features.
80
+
81
+ After a versioned release, the `@latest`, `@1`, or `@2` URLs cache for up to ~12 h on the jsDelivr edge — purge them via <https://www.jsdelivr.com/tools/purge> if you need to roll out faster.
82
+
83
+ ### URL adapter (optional)
84
+
85
+ The 2.x components are fully controlled: they take their identifier (e.g. `taxonKey`, `sourceDatasetKey`, `taxonId`) and their navigation as plain props. To wire identifiers to your URL and links into the right paths, you can either:
86
+
87
+ 1. Pass `hrefForTaxon` / `onNavigateToTaxon` (and the equivalents for Tree, Search and Source) yourself — see the [per-component examples below](#colbrowsertree) and the [bypassing-the-adapter section](#bypassing-the-adapter-custom-router-redux-no-url-at-all).
88
+ 2. Wrap the component with the built-in **`withRouting`** adapter, which reads the identifier from `window.location` and provides all four navigation pairs from a single `paths` map.
89
+
90
+ `withRouting` is exported from the main entry — available both as `ColBrowser.withRouting` in UMD `<script>` tags and as `import { withRouting } from 'col-browser'` in ES module builds.
91
+
92
+ ```html
93
+ <!-- UMD -->
94
+ <script>
95
+ const URLTaxon = ColBrowser.withRouting(ColBrowser.Taxon, {
96
+ kind: 'taxon',
97
+ mode: 'path', // or 'hash' — see the GitHub Pages demo
98
+ navigation: 'reload', // static portal; use 'spa' (the default) for SPA hosts — see below
99
+ paths: {
100
+ taxon: '/taxon/', // /taxon/{taxonKey}
101
+ tree: '/tree', // /tree?taxonKey={…}
102
+ search: '/search', // /search?q=…
103
+ source: '/source/', // /source/{sourceDatasetKey}
104
+ },
105
+ });
106
+
107
+ ColBrowser.ReactDOM.createRoot(document.querySelector('#taxon'))
108
+ .render(ColBrowser.React.createElement(URLTaxon, { datasetKey: '3LR' }));
109
+ </script>
110
+ ```
111
+
112
+ ```jsx
113
+ // ES module
114
+ import { Taxon, withRouting } from 'col-browser';
115
+
116
+ const URLTaxon = withRouting(Taxon, {
117
+ kind: 'taxon',
118
+ mode: 'path',
119
+ navigation: 'reload',
120
+ paths: { taxon: '/taxon/', tree: '/tree', search: '/search', source: '/source/' },
121
+ });
122
+
123
+ <URLTaxon datasetKey="3LR" />
124
+ ```
125
+
126
+ One wrapper per top-level component. The `mode` controls whether identifiers are read from `location.pathname` (`"path"`) or from `location.hash` (`"hash"`). The `paths` map gives each navigation target a prefix; the adapter appends the identifier when navigating (e.g. `/taxon/` + `6W3C4` → `/taxon/6W3C4`).
127
+
128
+ #### `navigation: 'spa'` vs `navigation: 'reload'`
129
+
130
+ A third option controls what the four `onNavigateToX` callbacks do when an in-component action triggers cross-page navigation:
131
+
132
+ - `navigation: 'spa'` (default) — calls `history.pushState`. The URL changes but the page is not reloaded; an SPA host re-renders in response. Right for react-router, Next.js, TanStack Router, Redux-driven hosts.
133
+ - `navigation: 'reload'` — calls `window.location.assign(url)`, forcing the browser to load the target page. Right for **static / multi-page hosts**: a Jekyll site, the GitHub Pages demo, any plain-HTML embedder where each `/taxon/<id>` is a separate page.
134
+
135
+ If you're embedding into a static portal, use `navigation: 'reload'`. Otherwise links that don't have an `<a href>` fallback (most visible: Highcharts pie segments in `TaxonBreakdown`) will update the URL without loading the new page, leaving the user stuck on the current one.
136
+
137
+ ```js
138
+ ColBrowser.withRouting(ColBrowser.Taxon, {
139
+ kind: 'taxon',
140
+ mode: 'path',
141
+ navigation: 'reload', // ← static / multi-page hosts
142
+ paths: { taxon: '/data/taxon/', tree: '/data/browse', search: '/data/search', source: '/data/dataset/' },
143
+ });
144
+ ```
145
+
146
+ The in-component state callbacks (`onExpandedTaxonKeyChange` on Tree, `onFiltersChange` on Search) always use `pushState` regardless of `navigation` — those represent local state within a single component (tree expansion, filter typing) and reloading the page on each change would be disruptive.
147
+
148
+ `navigation: 'reload'` is only meaningful with `mode: 'path'`. With `mode: 'hash'`, hash changes never reload the page by definition, so `'reload'` and `'spa'` behave identically.
149
+
150
+ Available kinds and what each one wires up:
151
+
152
+ | `kind` | Controlled identifier from URL | State callbacks |
153
+ | --- | --- | --- |
154
+ | `taxon` | `taxonKey` from path after `paths.taxon` | — |
155
+ | `tree` | `expandedTaxonKey` from `?taxonKey=…` | `onExpandedTaxonKeyChange` writes to URL |
156
+ | `search` | `filters` from query string | `onFiltersChange` writes to query string |
157
+ | `source` | `sourceDatasetKey` from `paths.source` | — |
158
+ | `sourceList` | none (it's a static-by-datasetKey list) | — |
159
+ | `taxonBreakdown` | `taxonId` from `paths.taxonBreakdown` | — |
160
+ | `taxonDistribution` | `taxonId` from `paths.taxonDistribution` | — |
161
+ | `bibtex` | `sourceDatasetKey` from `paths.bibtex` | — |
162
+
163
+ The COL portal uses `mode: 'path'`. The GitHub Pages demo uses `mode: 'hash'` against the same components — see [`demo/src/index.js`](demo/src/index.js).
164
+
165
+ For SPA frameworks where the adapter doesn't fit (a custom router, Redux state, no URL at all), every component also works as a plain controlled component — see [Bypassing the adapter](#bypassing-the-adapter-custom-router-redux-no-url-at-all).
166
+
167
+ ### Theming (optional)
168
+
169
+ Every top-level component accepts two optional theming props. When neither is set, the library's defaults are used and no Ant Design `ConfigProvider` is mounted.
170
+
171
+ - `theme` — full antd `ThemeConfig` forwarded straight to `ConfigProvider.theme`. Use it to override design tokens, component-level styles, or the algorithm.
172
+ - `darkMode` — convenience boolean. When `true` and `theme.algorithm` is not set, `theme.darkAlgorithm` is applied. The `TaxonBreakdown` chart also uses this flag to colour the outer-ring gap arcs; otherwise it falls back to `prefers-color-scheme`.
173
+
174
+ ```jsx
175
+ // ES module
176
+ import { Search } from 'col-browser';
177
+
178
+ <Search
179
+ datasetKey="3LR"
180
+ darkMode
181
+ theme={{ token: { colorPrimary: '#d97706', borderRadius: 6 } }}
182
+ />
183
+ ```
184
+
185
+ ```html
186
+ <!-- UMD -->
187
+ <script>
188
+ ColBrowser.ReactDOM.createRoot(document.querySelector('#search')).render(
189
+ ColBrowser.React.createElement(ColBrowser.Search, {
190
+ datasetKey: '3LR',
191
+ darkMode: true,
192
+ theme: { token: { colorPrimary: '#d97706' } },
193
+ })
194
+ );
195
+ </script>
196
+ ```
197
+
198
+
199
+ This will create a global `ColBrowser` library variable with the components documented below.
200
+
201
+ > The prop tables below describe each component's **component-specific** props (identifier, content options). Every component also accepts the four navigation pairs — `hrefForTaxon` / `onNavigateToTaxon`, `hrefForTree` / `onNavigateToTree`, `hrefForSearch` / `onNavigateToSearch`, `hrefForSource` / `onNavigateToSource`. Each section below shows two examples: one wiring the navigation pairs manually, and one using the built-in [`withRouting` adapter](#url-adapter-optional).
202
+
203
+ ### ColBrowser.Tree
204
+
205
+ A [browsable taxonomic tree](https://www.catalogueoflife.org/data/browse). Component-specific props:
206
+
207
+ 1. `datasetKey` - the dataset key from the [Catalogue of Life ChecklistBank](https://www.checklistbank.org/).
208
+ 2. `expandedTaxonKey` - (Optional, controlled) which taxon the tree is expanded down to. Pair with `onExpandedTaxonKeyChange` to keep host state in sync.
209
+ 3. `onExpandedTaxonKeyChange` - (Optional) called with the new key when the user expands a different taxon.
210
+ 4. `defaultTaxonKey` - (Optional, uncontrolled fallback) initial taxon to expand to when `expandedTaxonKey` is not provided.
211
+ 5. `showTreeOptions` - (Optional) show toggles for extinct taxa and info (estimates, providers etc).
212
+ 6. `linkToSpeciesPage` - (Optional) when the searchbox finds a species or infraspecific taxon, jump directly to the taxon page rather than opening the tree.
213
+ 7. `citation` - (Optional) either `"top"` or `"bottom"`; include the necessary dataset citation above or below the tree component.
214
+ 8. `type` - (Optional) e.g. `type="project"` will show info about contributing sources on the tree nodes.
215
+ 9. `insertPlaceholder` - (Optional, defaults to `true`) when true, the API virtually groups children of lower ranks into a "Not assigned" placeholder node for a more compact browsing experience. Pass `false` to disable.
216
+
217
+ ```html
218
+ <div id="tree"></div> <!-- Dom element for the tree to attach to -->
219
+ <script>
220
+ 'use strict';
221
+ ColBrowser.ReactDOM.createRoot(document.querySelector('#tree')).render(
222
+ ColBrowser.React.createElement(ColBrowser.Tree, {
223
+ datasetKey: 9999,
224
+ hrefForTaxon: (id) => `/taxon/${id}`,
225
+ hrefForSource: (id) => `/source/${id}`,
226
+ })
227
+ );
228
+ </script>
229
+ ```
230
+
231
+ Wire only `hrefForX` for plain anchor navigation (the browser does a full page load on click — fine for static portals). Add `onNavigateToX` as well for SPA-style in-page navigation, or use the [`withRouting` adapter](#url-adapter-optional) to wire both from a single `paths` map.
232
+
233
+ With the URL adapter:
234
+
235
+ ```html
236
+ <div id="tree"></div>
237
+ <script>
238
+ 'use strict';
239
+ const URLTree = ColBrowser.withRouting(ColBrowser.Tree, {
240
+ kind: 'tree',
241
+ mode: 'path',
242
+ navigation: 'reload', // static portal; use 'spa' (the default) for SPA hosts
243
+ paths: { taxon: '/taxon/', tree: '/tree', search: '/search', source: '/source/' },
244
+ });
245
+
246
+ // /tree?taxonKey=… is read into `expandedTaxonKey`; changes write back.
247
+ ColBrowser.ReactDOM.createRoot(document.querySelector('#tree')).render(
248
+ ColBrowser.React.createElement(URLTree, { datasetKey: '3LR' })
249
+ );
250
+ </script>
251
+ ```
252
+
253
+
254
+ ### ColBrowser.Search
255
+
256
+ [Search component with table view](https://www.catalogueoflife.org/data/search). Component-specific props:
257
+
258
+ 1. `datasetKey` - the dataset key from the [Catalogue of Life ChecklistBank](https://www.checklistbank.org/).
259
+ 2. `filters` - (Optional, controlled) current filter object (`{ q, rank, status, TAXON_ID, sortBy, … }`). Pair with `onFiltersChange` to persist filters in your host's URL or state.
260
+ 3. `onFiltersChange` - (Optional) called with the next filter object whenever the user changes a filter.
261
+ 4. `defaultTaxonKey` - (Optional) if the search should default to a certain Family, Order etc.
262
+ 5. `citation` - (Optional) either `"top"` or `"bottom"`; include the necessary dataset citation above or below the search component.
263
+
264
+ ```html
265
+ <div id="search"></div>
266
+ <script>
267
+ 'use strict';
268
+ ColBrowser.ReactDOM.createRoot(document.querySelector('#search')).render(
269
+ ColBrowser.React.createElement(ColBrowser.Search, {
270
+ datasetKey: 9999,
271
+ hrefForTaxon: (id) => `/taxon/${id}`,
272
+ })
273
+ );
274
+ </script>
275
+ ```
276
+
277
+ With the URL adapter:
278
+
279
+ ```html
280
+ <div id="search"></div>
281
+ <script>
282
+ 'use strict';
283
+ const URLSearch = ColBrowser.withRouting(ColBrowser.Search, {
284
+ kind: 'search',
285
+ mode: 'path',
286
+ navigation: 'reload',
287
+ paths: { taxon: '/taxon/', tree: '/tree', search: '/search', source: '/source/' },
288
+ });
289
+
290
+ // /search?q=…&rank=… is read into `filters`; user changes write back.
291
+ ColBrowser.ReactDOM.createRoot(document.querySelector('#search')).render(
292
+ ColBrowser.React.createElement(URLSearch, { datasetKey: '3LR' })
293
+ );
294
+ </script>
295
+ ```
296
+
297
+
298
+ ### ColBrowser.Taxon
299
+
300
+ [Taxon detail page](https://www.catalogueoflife.org/data/taxon/623QT). Component-specific props:
301
+
302
+ 1. `datasetKey` - the dataset key from the [Catalogue of Life ChecklistBank](https://www.checklistbank.org/).
303
+ 2. `taxonKey` - (controlled) the taxon to render. Read by the host from its URL and passed in.
304
+ 3. `pageTitleTemplate` - (Optional) a template for formatting the page title. A string containing the variable `__taxon__` that will be replaced with the taxon name.
305
+ 4. `identifierLabel` - (Optional) label for the identifier listed on top of the taxon view. Defaults to `"Identifier"`.
306
+ 5. `showDistributionMap` - (Optional) When `true`, render an interactive MapLibre GL map (CARTO Positron vector basemap) for distributions whose areas have a known geometry, with a toggle to switch to the plain text list view. **Requires the consumer to load MapLibre GL JS 4+ or 5+ and its CSS** (peer dependency).
307
+ 6. `gbifChecklistKey` - (Optional) When set, the distribution map adds a GBIF occurrence overlay (iNaturalist.poly hex bins) for the focal taxon, using the GBIF v2 multitaxonomy tile endpoint. The value is passed as the `checklistKey` query parameter; the focal taxon's id is passed as `taxonKey`. **The consumer is responsible for only setting this when the configured `datasetKey` actually uses identifiers that GBIF recognises under the given checklist.** For datasets keyed by COL identifiers, use the Catalogue of Life backbone UUID:
308
+
309
+ ```
310
+ gbifChecklistKey="7ddf754f-d193-4cc9-b351-99906754a03b"
311
+ ```
312
+
313
+ To use the map, include MapLibre GL JS alongside React in your page:
314
+
315
+ ```html
316
+ <link rel="stylesheet" href="https://unpkg.com/maplibre-gl@5/dist/maplibre-gl.css" />
317
+ <script src="https://unpkg.com/maplibre-gl@5/dist/maplibre-gl.js"></script>
318
+ ```
319
+
320
+ ES module consumers: `npm install maplibre-gl` and `import "maplibre-gl/dist/maplibre-gl.css"` in your bundle entry.
321
+
322
+ ```html
323
+ <div id="taxon"></div>
324
+ <script>
325
+ 'use strict';
326
+ // Read the taxonKey from your host's URL however you like. For a portal
327
+ // where each taxon has its own page at /taxon/<id>, the trailing path
328
+ // segment works:
329
+ const taxonKey = location.pathname.split('/').pop();
330
+
331
+ ColBrowser.ReactDOM.createRoot(document.querySelector('#taxon')).render(
332
+ ColBrowser.React.createElement(ColBrowser.Taxon, {
333
+ datasetKey: 9999,
334
+ taxonKey: taxonKey,
335
+ pageTitleTemplate: 'COL | __taxon__',
336
+ showDistributionMap: true,
337
+ gbifChecklistKey: '7ddf754f-d193-4cc9-b351-99906754a03b',
338
+ hrefForTaxon: (id) => `/taxon/${id}`,
339
+ hrefForTree: ({ taxonKey }) => `/tree?taxonKey=${taxonKey || ''}`,
340
+ hrefForSearch: (filters) => `/search?${new URLSearchParams(filters)}`,
341
+ hrefForSource: (id) => `/source/${id}`,
342
+ })
343
+ );
344
+ </script>
345
+ ```
346
+
347
+ With the URL adapter:
348
+
349
+ ```html
350
+ <div id="taxon"></div>
351
+ <script>
352
+ 'use strict';
353
+ const URLTaxon = ColBrowser.withRouting(ColBrowser.Taxon, {
354
+ kind: 'taxon',
355
+ mode: 'path',
356
+ navigation: 'reload',
357
+ paths: { taxon: '/taxon/', tree: '/tree', search: '/search', source: '/source/' },
358
+ });
359
+
360
+ // On a route like /taxon/6W3C4, the adapter reads `taxonKey` itself.
361
+ ColBrowser.ReactDOM.createRoot(document.querySelector('#taxon')).render(
362
+ ColBrowser.React.createElement(URLTaxon, { datasetKey: '3LR', showDistributionMap: true })
363
+ );
364
+ </script>
365
+ ```
366
+
367
+
368
+ ### ColBrowser.TaxonBreakdown
369
+
370
+ A Highcharts pie chart breaking the accepted children of a taxon down by rank, with click-to-drill into each child. Rendered inside the Taxon page but also exported standalone for use on dashboards or summary pages. Component-specific props:
371
+
372
+ 1. `datasetKey` - the dataset key from the [Catalogue of Life ChecklistBank](https://www.checklistbank.org/).
373
+ 2. `taxonId` - (controlled) the taxon to break down. The chart loads the taxon, its rank vocabulary, and the dataset citation itself, so no preloading is needed.
374
+ 3. `level` - (Optional, `1` or `2`, default `1`) controls how deep the breakdown nests. `1` renders a single-ring donut (direct children of `taxonId`). `2` renders a two-ring donut adding grandchildren. The value is passed through as the `level` query parameter on the `/dataset/{key}/taxon/{id}/breakdown` API.
375
+ 4. `showLevelSwitch` - (Optional, default `false`) when `true`, render an in-chart antd `Switch` that lets the user toggle between level 1 and 2 at runtime.
376
+ 5. `darkMode` - (Optional) when `true`, use the dark-mode outer-ring gap colour. Falls back to `prefers-color-scheme` when unset.
377
+
378
+ ```html
379
+ <div id="breakdown"></div>
380
+ <script>
381
+ 'use strict';
382
+ ColBrowser.ReactDOM.createRoot(document.querySelector('#breakdown')).render(
383
+ ColBrowser.React.createElement(ColBrowser.TaxonBreakdown, {
384
+ datasetKey: '3LR',
385
+ taxonId: 'ST',
386
+ showLevelSwitch: true,
387
+ hrefForTaxon: (id) => `/taxon/${id}`,
388
+ })
389
+ );
390
+ </script>
391
+ ```
392
+
393
+ With the URL adapter:
394
+
395
+ ```html
396
+ <div id="breakdown"></div>
397
+ <script>
398
+ 'use strict';
399
+ const URLBreakdown = ColBrowser.withRouting(ColBrowser.TaxonBreakdown, {
400
+ kind: 'taxonBreakdown',
401
+ mode: 'path',
402
+ navigation: 'reload',
403
+ paths: { taxonBreakdown: '/breakdown/', taxon: '/taxon/' },
404
+ });
405
+
406
+ // On a route like /breakdown/ST, the adapter reads `taxonId` itself.
407
+ ColBrowser.ReactDOM.createRoot(document.querySelector('#breakdown')).render(
408
+ ColBrowser.React.createElement(URLBreakdown, { datasetKey: '3LR', showLevelSwitch: true })
409
+ );
410
+ </script>
411
+ ```
412
+
413
+
414
+ ### ColBrowser.TaxonDistribution
415
+
416
+ The Taxon page's distribution block — a MapLibre GL vector map of the taxon's distribution polygons, with an optional GBIF occurrence overlay and a Map/List toggle — exposed as a standalone component. Useful when you want to surface a taxon's distribution on a non-COL page without rendering the whole Taxon view. Component-specific props:
417
+
418
+ 1. `datasetKey` - the dataset key from the [Catalogue of Life ChecklistBank](https://www.checklistbank.org/).
419
+ 2. `taxonId` - (controlled) the taxon to render. The component loads the taxon, its distributions, and the rank vocabulary itself.
420
+ 3. `gbifChecklistKey` - (Optional) when set, adds the GBIF occurrence overlay (iNaturalist.poly hex bins) for the focal taxon. See the same prop on `ColBrowser.Taxon` for the caveat about checklist-key alignment.
421
+ 4. `style` - (Optional) inline style passed through to the outer wrapper.
422
+
423
+ **Requires the consumer to load MapLibre GL JS 4+ or 5+ and its CSS** (peer dependency), same as `ColBrowser.Taxon` with `showDistributionMap`:
424
+
425
+ ```html
426
+ <link rel="stylesheet" href="https://unpkg.com/maplibre-gl@5/dist/maplibre-gl.css" />
427
+ <script src="https://unpkg.com/maplibre-gl@5/dist/maplibre-gl.js"></script>
428
+ ```
429
+
430
+ ```html
431
+ <div id="distribution"></div>
432
+ <script>
433
+ 'use strict';
434
+ ColBrowser.ReactDOM.createRoot(document.querySelector('#distribution')).render(
435
+ ColBrowser.React.createElement(ColBrowser.TaxonDistribution, {
436
+ datasetKey: '3LR',
437
+ taxonId: '6W3C4',
438
+ gbifChecklistKey: '7ddf754f-d193-4cc9-b351-99906754a03b',
439
+ hrefForSource: (id) => `/source/${id}`,
440
+ })
441
+ );
442
+ </script>
443
+ ```
444
+
445
+ With the URL adapter:
446
+
447
+ ```html
448
+ <div id="distribution"></div>
449
+ <script>
450
+ 'use strict';
451
+ const URLDistribution = ColBrowser.withRouting(ColBrowser.TaxonDistribution, {
452
+ kind: 'taxonDistribution',
453
+ mode: 'path',
454
+ navigation: 'reload',
455
+ paths: { taxonDistribution: '/distribution/', source: '/source/' },
456
+ });
457
+
458
+ // On a route like /distribution/6W3C4, the adapter reads `taxonId` itself.
459
+ ColBrowser.ReactDOM.createRoot(document.querySelector('#distribution')).render(
460
+ ColBrowser.React.createElement(URLDistribution, { datasetKey: '3LR' })
461
+ );
462
+ </script>
463
+ ```
464
+
465
+
466
+ ### ColBrowser.SourceDatasetList
467
+
468
+ [Sortable table of the source datasets contributing to a project](https://www.catalogueoflife.org/data/contributors), grouped by publisher. Each row shows the source dataset's alias, publisher, number of taxa and last import — and the merged-data badge where applicable. Only useful for *compiled* datasets like the Catalogue of Life that pull from many sources; for standalone datasets the table will be empty. Component-specific props:
469
+
470
+ 1. `datasetKey` - the dataset key of the catalogue/project from the [Catalogue of Life ChecklistBank](https://www.checklistbank.org/).
471
+
472
+ Per-row links into the source-dataset and search pages are built from the four navigation pairs (`hrefForSource` and `hrefForSearch` in particular).
473
+
474
+ ```html
475
+ <div id="contributors"></div>
476
+ <script>
477
+ 'use strict';
478
+ ColBrowser.ReactDOM.createRoot(document.querySelector('#contributors')).render(
479
+ ColBrowser.React.createElement(ColBrowser.SourceDatasetList, {
480
+ datasetKey: 9999,
481
+ hrefForSource: (id) => `/source/${id}`,
482
+ hrefForSearch: (filters) => `/search?${new URLSearchParams(filters)}`,
483
+ })
484
+ );
485
+ </script>
486
+ ```
487
+
488
+ With the URL adapter:
489
+
490
+ ```html
491
+ <div id="contributors"></div>
492
+ <script>
493
+ 'use strict';
494
+ const URLSourceList = ColBrowser.withRouting(ColBrowser.SourceDatasetList, {
495
+ kind: 'sourceList',
496
+ mode: 'path',
497
+ navigation: 'reload',
498
+ paths: { search: '/search', source: '/source/' },
499
+ });
500
+
501
+ ColBrowser.ReactDOM.createRoot(document.querySelector('#contributors')).render(
502
+ ColBrowser.React.createElement(URLSourceList, { datasetKey: '3LR' })
503
+ );
504
+ </script>
505
+ ```
506
+
507
+
508
+ ### ColBrowser.SourceDataset
509
+
510
+ [Source-dataset detail page](https://www.catalogueoflife.org/data/dataset/2073). Component-specific props:
511
+
512
+ 1. `datasetKey` - the dataset key from the [Catalogue of Life ChecklistBank](https://www.checklistbank.org/). When `sourceDatasetKey` is set, this is the catalogue/project containing the source; otherwise it's the dataset being rendered directly.
513
+ 2. `sourceDatasetKey` - (Optional, controlled) the source dataset to render within the catalogue identified by `datasetKey`.
514
+ 3. `pageTitleTemplate` - (Optional) a template for formatting the page title. A string containing the variable `__dataset__` that will be replaced with the dataset title.
515
+
516
+ ```html
517
+ <div id="dataset"></div>
518
+ <script>
519
+ 'use strict';
520
+ ColBrowser.ReactDOM.createRoot(document.querySelector('#dataset')).render(
521
+ ColBrowser.React.createElement(ColBrowser.SourceDataset, {
522
+ datasetKey: 9999,
523
+ sourceDatasetKey: 2073,
524
+ pageTitleTemplate: 'COL | __dataset__',
525
+ hrefForTree: ({ taxonKey }) => `/tree?taxonKey=${taxonKey || ''}`,
526
+ hrefForSearch: (filters) => `/search?${new URLSearchParams(filters)}`,
527
+ })
528
+ );
529
+ </script>
530
+ ```
531
+
532
+ With the URL adapter:
533
+
534
+ ```html
535
+ <div id="dataset"></div>
536
+ <script>
537
+ 'use strict';
538
+ const URLSourceDataset = ColBrowser.withRouting(ColBrowser.SourceDataset, {
539
+ kind: 'source',
540
+ mode: 'path',
541
+ navigation: 'reload',
542
+ paths: { source: '/source/', tree: '/tree', search: '/search' },
543
+ });
544
+
545
+ // On a route like /source/2073, the adapter reads `sourceDatasetKey` itself.
546
+ ColBrowser.ReactDOM.createRoot(document.querySelector('#dataset')).render(
547
+ ColBrowser.React.createElement(URLSourceDataset, { datasetKey: '3LR' })
548
+ );
549
+ </script>
550
+ ```
551
+
552
+
553
+ ### ColBrowser.BibTex
554
+
555
+ A small icon that links to the BibTex citation for a dataset on [ChecklistBank](https://www.checklistbank.org/). Component-specific props:
556
+
557
+ 1. `datasetKey` - the dataset key from the [Catalogue of Life ChecklistBank](https://www.checklistbank.org/). When `sourceDatasetKey` is also given, this is the catalogue/project containing the source; otherwise it's the dataset being cited directly.
558
+ 2. `sourceDatasetKey` - (Optional) the source dataset to cite within the catalogue identified by `datasetKey`. Use this when the citation is for a source compiled into a larger dataset such as the Catalogue of Life.
559
+ 3. `style` - (Optional) to set margins, height etc. Defaults to `{ height: "40px" }`.
560
+
561
+ ```html
562
+ <div id="bibtex"></div>
563
+ <script>
564
+ 'use strict';
565
+ // Cite a standalone dataset
566
+ ColBrowser.ReactDOM.createRoot(document.querySelector('#bibtex')).render(
567
+ ColBrowser.React.createElement(ColBrowser.BibTex, { datasetKey: 9999 })
568
+ );
569
+
570
+ // Or cite a source within a catalogue:
571
+ // ColBrowser.React.createElement(ColBrowser.BibTex, { datasetKey: 9837, sourceDatasetKey: 1010 })
572
+ </script>
573
+ ```
574
+
575
+ With the URL adapter:
576
+
577
+ ```html
578
+ <div id="bibtex"></div>
579
+ <script>
580
+ 'use strict';
581
+ const URLBibTex = ColBrowser.withRouting(ColBrowser.BibTex, {
582
+ kind: 'bibtex',
583
+ mode: 'path',
584
+ navigation: 'reload',
585
+ paths: { bibtex: '/bibtex/' },
586
+ });
587
+
588
+ // On a route like /bibtex/1010, the adapter reads `sourceDatasetKey` itself.
589
+ ColBrowser.ReactDOM.createRoot(document.querySelector('#bibtex')).render(
590
+ ColBrowser.React.createElement(URLBibTex, { datasetKey: '3LR' })
591
+ );
592
+ </script>
593
+ ```
594
+
595
+
596
+ ## Upgrading from 1.x to 2.0
597
+
598
+ v2.0 brings the React / Ant Design / React Router stack up to current versions, and turns every top-level component into a fully controlled component — the library no longer reads or writes the URL. Embedders need to update their host page in a few places (peer dependencies, root API, and the routing wiring), and then either drop in the built-in URL adapter or pass identifiers and navigation callbacks as props.
599
+
600
+ ### 1. Drop the React / ReactDOM `<script>` tags
601
+
602
+ React 19 dropped UMD builds entirely, so the v2 col-browser UMD bundle now bundles React 19 and `react-dom/client` and re-exposes them as `ColBrowser.React` and `ColBrowser.ReactDOM`. Remove the separate React `<script>` tags from your page:
603
+
604
+ ```html
605
+ <!-- old (v1.x) -->
606
+ <script src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
607
+ <script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
608
+ <script src="https://cdn.jsdelivr.net/gh/CatalogueOfLife/portal-components@1/umd/col-browser.min.js"></script>
609
+
610
+ <!-- new (v2.x): just col-browser -->
611
+ <script src="https://cdn.jsdelivr.net/gh/CatalogueOfLife/portal-components@2/umd/col-browser.min.js"></script>
612
+ ```
613
+
614
+ ### 2. Mount via `ColBrowser.ReactDOM.createRoot` (was bare `ReactDOM.render`)
615
+
616
+ Use the React + ReactDOM exposed on the `ColBrowser` global:
617
+
618
+ ```js
619
+ // old (v1.x): bare React / ReactDOM globals + ReactDOM.render
620
+ ReactDOM.render(React.createElement(Tree), document.querySelector('#tree'));
621
+
622
+ // new (v2.x): ColBrowser-scoped React / ReactDOM + createRoot
623
+ ColBrowser.ReactDOM.createRoot(document.querySelector('#tree'))
624
+ .render(ColBrowser.React.createElement(ColBrowser.Tree));
625
+ ```
626
+
627
+ ### 3. Keep loading the JS bundle alongside the CSS
628
+
629
+ Ant Design 5+ injects component styles via cssinjs at runtime when components mount. `umd/main.css` is now ~87 KB (was ~557 KB) — it only contains the library's custom overrides. The antd component styling now travels inside `col-browser.min.js`, so do **not** load just the CSS without the JS.
630
+
631
+ Net wire size for a typical embedder (`col-browser.min.js` + `main.css`) — including the bundled React 19 + `react-dom/client`: ~836 KB gzipped (v1 was ~532 KB gzipped split across three CDN files for React + ReactDOM + col-browser). The single-file load is friendlier to static-portal embedders even if the byte count is higher.
632
+
633
+ ### 4. Components are now fully controlled
634
+
635
+ The biggest API change in 2.0 is that **the library no longer reads or writes the URL**. Every top-level component takes its identifier and navigation as plain props:
636
+
637
+ - **Identifier** (whichever of `taxonKey`, `sourceDatasetKey`, `taxonId`, `expandedTaxonKey`, `filters` applies) is a controlled prop.
638
+ - **Navigation** is two optional callbacks per target — `onNavigateToTaxon(id)` for the click handler and `hrefForTaxon(id)` for the `<a href>`. When neither is wired, the affected link renders as plain text. When both are wired, you get full anchor semantics (right-click → open in new tab, etc.).
639
+ - **Component-local state changes** (e.g. tree expansion, search filters) emit through `onExpandedTaxonKeyChange` / `onFiltersChange` callbacks — never via a URL write inside the library.
640
+
641
+ This makes the components host-agnostic: they work the same inside react-router, Next.js, TanStack Router, an unrouted page, a Storybook story, or a Redux app.
642
+
643
+ For new code, the easiest way to wire identifiers and links is the built-in [URL adapter](#url-adapter-optional) — its kinds table lists exactly which identifier each component reads.
644
+
645
+ #### Migrating from v1
646
+
647
+ ```diff
648
+ -import { Taxon } from 'col-browser';
649
+ +import { Taxon, withRouting } from 'col-browser';
650
+ +
651
+ +const URLTaxon = withRouting(Taxon, {
652
+ + kind: 'taxon',
653
+ + mode: 'path',
654
+ + paths: { taxon: '/taxon/', tree: '/tree', search: '/search', source: '/source/' },
655
+ +});
656
+
657
+ -<Taxon datasetKey="3LR" pathToTaxon="/taxon/" pathToTree="/tree" pathToSearch="/search" pathToDataset="/source/" />
658
+ +<URLTaxon datasetKey="3LR" />
659
+ ```
660
+
661
+ #### Bypassing the adapter (custom router, Redux, no URL at all)
662
+
663
+ You don't have to use `withRouting`. Every top-level component is a plain controlled React component — feed it the props it needs and the library will never touch the URL.
664
+
665
+ ```jsx
666
+ function MyTaxonPage() {
667
+ const { taxonKey } = useParams(); // react-router
668
+ const navigate = useNavigate();
669
+ return (
670
+ <Taxon
671
+ datasetKey="3LR"
672
+ taxonKey={taxonKey}
673
+ hrefForTaxon={(id) => `/taxon/${id}`} // your URL shape
674
+ onNavigateToTaxon={(id) => navigate(`/taxon/${id}`)}
675
+ hrefForTree={({ taxonKey }) => `/tree?t=${taxonKey || ''}`}
676
+ onNavigateToTree={({ taxonKey }) => navigate(`/tree?t=${taxonKey || ''}`)}
677
+ hrefForSearch={(filters) => `/search?${new URLSearchParams(filters)}`}
678
+ onNavigateToSearch={(filters) => navigate(`/search?${new URLSearchParams(filters)}`)}
679
+ hrefForSource={(id) => `/source/${id}`}
680
+ onNavigateToSource={(id) => navigate(`/source/${id}`)}
681
+ />
682
+ );
683
+ }
684
+ ```
685
+
686
+ Tree and Search take additional state callbacks:
687
+
688
+ ```jsx
689
+ const [expanded, setExpanded] = useState(undefined);
690
+ <Tree
691
+ datasetKey="3LR"
692
+ expandedTaxonKey={expanded}
693
+ onExpandedTaxonKeyChange={setExpanded}
694
+ /* …plus the four nav pairs from above */
695
+ />
696
+
697
+ const [filters, setFilters] = useState({});
698
+ <Search
699
+ datasetKey="3LR"
700
+ filters={filters} // { q, rank, status, TAXON_ID, sortBy, … }
701
+ onFiltersChange={setFilters}
702
+ />
703
+ ```
704
+
705
+ Any callback you don't supply just makes that link render as plain text — no crash, no warning. So you can wire the navigations gradually.
706
+
707
+ #### Building your own adapter
708
+
709
+ `withRouting` is ~150 LOC; nothing stops you from writing your own. The shape is simply:
710
+
711
+ ```jsx
712
+ function withMyRouting(Component) {
713
+ return function Wrapped(props) {
714
+ // 1. Derive the controlled identifier from wherever your state lives
715
+ const taxonKey = useMyRouter().params.taxonKey;
716
+
717
+ // 2. Build the four nav pairs
718
+ const hrefForTaxon = (id) => `/taxon/${id}`;
719
+ const onNavigateToTaxon = (id) => myNavigate(`/taxon/${id}`);
720
+ // …same for tree / search / source
721
+
722
+ return (
723
+ <Component
724
+ {...props}
725
+ taxonKey={taxonKey}
726
+ hrefForTaxon={hrefForTaxon}
727
+ onNavigateToTaxon={onNavigateToTaxon}
728
+ /* …rest */
729
+ />
730
+ );
731
+ };
732
+ }
733
+ ```
734
+
735
+ You can copy `src/url/index.js` as a starting template — it covers both path and hash modes plus the per-kind state wiring (Tree's `expandedTaxonKey` ↔ `?taxonKey=`, Search's `filters` ↔ query string).
736
+
737
+ ### 5. `Dataset` and `DatasetSearch` were renamed
738
+
739
+ The two source-dataset components were renamed to make their purpose explicit (a "dataset" in COL terms is a source dataset contributing to a project, not a generic CRUD-style dataset):
740
+
741
+ | Old export | New export |
742
+ |---|---|
743
+ | `Dataset` | `SourceDataset` |
744
+ | `DatasetSearch` | `SourceDatasetList` |
745
+
746
+ Update imports:
747
+
748
+ ```js
749
+ // old
750
+ import { Dataset, DatasetSearch } from 'col-browser';
751
+ // new
752
+ import { SourceDataset, SourceDatasetList } from 'col-browser';
753
+ ```
754
+
755
+ ```html
756
+ <!-- old -->
757
+ ColBrowser.Dataset
758
+ ColBrowser.DatasetSearch
759
+ <!-- new -->
760
+ ColBrowser.SourceDataset
761
+ ColBrowser.SourceDatasetList
762
+ ```
763
+
764
+ Props and behaviour are unchanged.
765
+
766
+ ### 6. `catalogueKey` was renamed to `datasetKey`
767
+
768
+ The prop previously called `catalogueKey` is now called `datasetKey`. The old name still works (with a console warning) but will be removed in a future major release — please update embeddings.
769
+
770
+ ### 7. The shared `history` singleton is gone
771
+
772
+ v1 exported a `history` singleton from `col-browser/history` (used internally for the in-component navigation). It is no longer needed and no longer exported, because the library no longer mutates the URL.
773
+
774
+ If your host page subscribes to its own history@5 listener and the signature change matters to you, note that v5's listener receives `({ location, action })` instead of `(location, action)`:
775
+
776
+ ```js
777
+ // old (history@4)
778
+ history.listen((location) => { /* ... */ });
779
+ // new (history@5)
780
+ history.listen(({ location }) => { /* ... */ });
781
+ ```
782
+
783
+
784
+ ## Releasing
785
+
786
+ After a tagged release, purge the floating jsDelivr URLs at <https://www.jsdelivr.com/tools/purge> so embedders pick up the new build within minutes instead of waiting out the ~12 h edge cache:
787
+
788
+ ```
789
+ # v2.x release — purge @2 and @latest
790
+ https://cdn.jsdelivr.net/gh/CatalogueOfLife/portal-components@2/umd/col-browser.min.js
791
+ https://cdn.jsdelivr.net/gh/CatalogueOfLife/portal-components@2/umd/main.css
792
+ https://cdn.jsdelivr.net/gh/CatalogueOfLife/portal-components@latest/umd/col-browser.min.js
793
+ https://cdn.jsdelivr.net/gh/CatalogueOfLife/portal-components@latest/umd/main.css
794
+
795
+ # v1.x backport — purge @1 only (do NOT purge @latest)
796
+ https://cdn.jsdelivr.net/gh/CatalogueOfLife/portal-components@1/umd/col-browser.min.js
797
+ https://cdn.jsdelivr.net/gh/CatalogueOfLife/portal-components@1/umd/main.css
798
+ ```