@sakuzu/maplibre-gl-draw-ui 1.0.0 → 1.1.0

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/CHANGELOG.md CHANGED
@@ -7,6 +7,138 @@ follows semantic versioning.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.1.0] - 2026-10-01
11
+
12
+ The basemaps as the back of the stack in the layer panel, the datasets
13
+ as rows of the stack, a limit on the features the panel lists, the
14
+ snapping settings on the magnet, the actions of the application in a
15
+ card, the inspector on the first tab of its options, and the layer panel
16
+ without a pencil, in-place renaming and row counts. The word
17
+ `activeLayer` leaves the `Messages` type (a locale that still sets it is
18
+ ignored at runtime).
19
+
20
+ ### Added in 1.1.0
21
+
22
+ - The datasets are rows of the layer panel, in their place in
23
+ the stack: those of `above-store` in front of every layer, those of
24
+ `layer-order` where `layers.getOrder()` places them among the layers,
25
+ and those of `below-store` behind every layer. A row shows the ID of
26
+ the dataset with a database mark, and the eye shows and hides it with
27
+ `setVisible`. It has no lock, and a press
28
+ on it leaves the selection as it is. A
29
+ `layer-order` dataset is dragged among the layers (`layers.reorder`
30
+ with its ID); the others stay. The rows follow `dataset.added`,
31
+ `dataset.removed`, `dataset.reordered` and the `changed` event of each
32
+ dataset. The option `datasets: false` (`LayerPanelOptions`) leaves
33
+ them out. New word: `datasets`.
34
+ - The basemaps of the layer panel, `basemaps` (`{ id, label,
35
+ style, preview }`), `basemap` and `onbasemap`, in the options of
36
+ `createDrawUI` and of `createLayerPanel` (`LayerPanelOptions`). With
37
+ two or more, the basemap row opens them on the right, in the place of
38
+ the inspector: a list of their labels, each with its `preview` (a
39
+ value of CSS `background`; a neutral square without one) and a check
40
+ on the current one. Opening it clears the selection; a selection made
41
+ on the map or in the tree, its close button and Escape close it.
42
+ Choosing one replaces the map's style, calls `onbasemap` and leaves
43
+ the list open; `basemap` names the current one at the start, and
44
+ `ui.setBasemap(id)` and `ui.getBasemap()` change and read it. The
45
+ drawing is drawn again on top of the new style. The layer panel put
46
+ alone opens the list in the place of its sections.
47
+ - The legend lists the style rules of the datasets as well as
48
+ those of the layers, in the order of the stack: a block for each
49
+ dataset with a rule (`Dataset.getStyleRule()` of core), titled with
50
+ its ID, with the rows `deriveLegend` gives and swatches shaped after
51
+ the types of its first rows. It follows `dataset.added`,
52
+ `dataset.removed`, `dataset.reordered` and the `changed` event of each
53
+ dataset (the reasons `style` and `rows`). The word `noLegend` now says
54
+ that no layer or dataset has a style rule. It needs core with
55
+ `Dataset.getStyleRule()`.
56
+ - The actions of the application, rows of a card at the bottom
57
+ left of the map, above maplibre-gl's scale: `ui.actions` (`add`,
58
+ `remove`, `list`, `refresh`) and the options `actions` and
59
+ `actionsTitle` of `createDrawUI`. An action (`ActionSpec`) is a switch
60
+ (`kind: 'toggle'`), which shows what `checked` returns, or a button
61
+ (`kind: 'action'`), and its `shortcut` is a key of the interface,
62
+ listed with `?` under the title of the card. `run` is called by a
63
+ press and by the key; `checked` and `disabled` are read again after
64
+ each run and on `refresh()`, and `hint` is a caption under the row. A
65
+ key the interface or another action uses is refused with an error, and
66
+ `tools.add` refuses the key of an action. The card folds into one
67
+ button from its head, starts folded on a map narrower than 48rem, and
68
+ stands above the toolbar where it would reach it across; the layer
69
+ panel floating at the left ends above it. New word: `actions`.
70
+ `actionsOpen: false` starts the card folded.
71
+
72
+ ### Changed in 1.1.0
73
+
74
+ - The layer panel lists up to 1,000 features in a layer, those
75
+ of its groups included. A layer that holds more lists none of them and
76
+ none of its groups: its one child is a row with their number and a
77
+ hint, "12,345 features. Select them on the map.", which is not
78
+ pressed, hidden, locked or dragged. The row of the layer works as
79
+ before. Each row of the panel costs about a third of a millisecond to
80
+ draw, so a layer of 20,000 features took seconds. `features`
81
+ (`LayerPanelOptions`) is now `boolean | number`: `true` (the default)
82
+ for the limit of 1,000, a number for another limit, `false` for no
83
+ features; a number less than 0 throws. New word: `manyFeatures` (with
84
+ `{count}`).
85
+ - The layer panel is a stack of two sections, as in the
86
+ reference layout. The first, Stack (the new word `stack`), is the
87
+ tree with the add menu in its head; the second, Basemap, holds one
88
+ row with a globe mark and the name of the basemap the map shows: the
89
+ label of the current one of `basemaps`, else the `name` of the map's
90
+ style, else the word `basemap`. The panel lists the stack from the
91
+ front, and the basemap is its back. The row is not a node of the tree:
92
+ it is not dragged, hidden, locked or selected. Before the release, the
93
+ menu of the basemaps was a button at the top right of the map, beside
94
+ the theme button, and then a menu under the tree; both are gone.
95
+ - The inspector of a feature opens on the first tab of
96
+ `inspector.tabs`, so `['attributes', 'style']` opens on Attributes.
97
+ The default stays `['style', 'attributes']`, which opens on Style. Once
98
+ a tab is chosen, it is kept from one feature to the next as before.
99
+ - The magnet of the toolbar opens the snapping settings above
100
+ the toolbar instead of switching snapping: snapping, the kinds of
101
+ target under it (vertices, edges, intersections and guides, off while
102
+ snapping is off), snapping to datasets, tracing edges and moving shared
103
+ vertices together, and the key that pauses snapping. Each switch writes
104
+ `draw.options.update` and follows `options.changed`. The magnet stays
105
+ pressed while snapping is on. New words: `snapVertex`, `snapEdge`,
106
+ `snapIntersection`, `snapGuide`, `snapDatasets`, `traceEdges`,
107
+ `sharedVertexDrag` and `snapPauseKey` (with `{key}`).
108
+
109
+ ### Removed in 1.1.0
110
+
111
+ - Renaming in place in the layer panel (F2 or a double click on
112
+ a row), as in the reference layout. The names of layers, groups and
113
+ features are changed in the head of the inspector, which also ends a
114
+ defect: a locked row could still be renamed in the tree.
115
+ - The pencil that marked the active layer (the layer drawn
116
+ features go into) in the layer panel, as in the reference layout: the
117
+ row of a layer looks the same whether it is active or not. Pressing a
118
+ layer still makes it active (`layers.setActive`). The word
119
+ `activeLayer` is gone with it; at run time a locale that still gives
120
+ it is accepted and the word is ignored.
121
+
122
+ ### Fixed in 1.1.0
123
+
124
+ - In the inspector of a feature, what a tab shows first is spaced
125
+ from the line of the tabs as the content of a panel is from its head
126
+ (pad-md to the first field, pad-lg to a section title). The panel of
127
+ the tab is now a Stack of its own under the tabs, so kata's spacing of
128
+ a first group applies; before, the title of the first section touched
129
+ the line.
130
+ - The fields of the style have no "Style" title any more: they
131
+ sit straight under the Style tab, and the sections of the application
132
+ and Operations keep their titles. The reset of the style, which was an
133
+ icon button in that title, is a "Reset the style" text action after
134
+ the fields. In the same way, the shared fields of a selection, the
135
+ fields of a group and the fields of a layer have no title repeating
136
+ what the head says; the style rule of a layer keeps its title.
137
+ - The page without a bundler in the README names
138
+ `@sakuzu/maplibre-gl-draw/geometry` in its import map, which the
139
+ single-file build imports. The README also shows the package from
140
+ React and from Vue, and a page that loads it from a CDN.
141
+
10
142
  ## [1.0.0] - 2026-10-01
11
143
 
12
144
  The first release of the standard user interface of
package/README.md CHANGED
@@ -23,8 +23,14 @@ npm install @sakuzu/maplibre-gl-draw @sakuzu/maplibre-gl-draw-ui maplibre-gl
23
23
 
24
24
  `@sakuzu/maplibre-gl-draw` (2.x) and `maplibre-gl` are peer
25
25
  dependencies: the application installs them, and the interface uses the
26
- same copies as the application. Svelte is a dependency, and kata is
27
- compiled into the package.
26
+ same copies as the application.
27
+
28
+ The package is used from any framework or none: React, Vue, Svelte,
29
+ another framework, or a plain page. It ships compiled JavaScript, and
30
+ the page needs no Svelte of its own. The interface is written in
31
+ Svelte, but Svelte is inside the package: its components are compiled
32
+ before they are published, the Svelte runtime they use comes as a
33
+ dependency of the package, and kata is compiled into it.
28
34
 
29
35
  The package loads in two ways.
30
36
 
@@ -33,33 +39,156 @@ The package loads in two ways.
33
39
  `@sakuzu/maplibre-gl-draw-ui/style.css` is the style sheet.
34
40
  - Without a bundler: `dist/maplibre-gl-draw-ui.js` is one module with
35
41
  Svelte and kata in it. It loads with `<script type="module">` and an
36
- import map that names `@sakuzu/maplibre-gl-draw` and `maplibre-gl`,
37
- together with `dist/style.css`.
42
+ import map that names `@sakuzu/maplibre-gl-draw`,
43
+ `@sakuzu/maplibre-gl-draw/geometry` and `maplibre-gl`, together with
44
+ `dist/style.css`.
38
45
 
39
- ```html
40
- <link rel="stylesheet" href="/vendor/maplibre-gl.css" />
41
- <link rel="stylesheet" href="/vendor/maplibre-gl-draw-ui/style.css" />
42
- <script type="importmap">
43
- {
44
- "imports": {
45
- "maplibre-gl": "/vendor/maplibre-gl.mjs",
46
- "@sakuzu/maplibre-gl-draw": "/vendor/maplibre-gl-draw.js"
47
- }
48
- }
49
- </script>
50
- <script type="module">
51
- import { Map } from 'maplibre-gl';
52
- import { createDraw } from '@sakuzu/maplibre-gl-draw';
53
- import { createDrawUI } from '/vendor/maplibre-gl-draw-ui/maplibre-gl-draw-ui.js';
46
+ In every case the interface is created after the draw instance and
47
+ destroyed before it: `createDrawUI(draw)` after `createDraw(map)`, and
48
+ `ui.destroy()`, then `draw.destroy()`, then `map.remove()`.
49
+
50
+ ### From React
51
+
52
+ ```tsx
53
+ import { useEffect, useRef } from 'react';
54
+ import * as maplibregl from 'maplibre-gl';
55
+ import 'maplibre-gl/dist/maplibre-gl.css';
56
+ import { createDraw } from '@sakuzu/maplibre-gl-draw';
57
+ import { createDrawUI } from '@sakuzu/maplibre-gl-draw-ui';
58
+ import '@sakuzu/maplibre-gl-draw-ui/style.css';
59
+
60
+ const style = 'https://tiles.openfreemap.org/styles/liberty';
61
+
62
+ export function DrawMap() {
63
+ const container = useRef<HTMLDivElement>(null);
64
+
65
+ useEffect(() => {
66
+ if (!container.current) return;
67
+ const map = new maplibregl.Map({ container: container.current, style });
68
+ const draw = createDraw(map);
69
+ const ui = createDrawUI(draw);
70
+ return () => {
71
+ ui.destroy();
72
+ draw.destroy();
73
+ map.remove();
74
+ };
75
+ }, []);
76
+
77
+ return <div ref={container} style={{ height: 400 }} />;
78
+ }
79
+ ```
80
+
81
+ The interface lies over the map's container, so the component renders
82
+ the container alone. In development, Strict Mode runs the effect twice;
83
+ the cleanup destroys the first set, so this is safe.
84
+
85
+ ### From Vue
54
86
 
55
- const map = new Map({ container: 'map', style: '/style.json' });
56
- createDrawUI(createDraw(map));
87
+ ```vue
88
+ <script setup lang="ts">
89
+ import { onBeforeUnmount, onMounted, ref } from 'vue';
90
+ import * as maplibregl from 'maplibre-gl';
91
+ import 'maplibre-gl/dist/maplibre-gl.css';
92
+ import { createDraw, type Draw } from '@sakuzu/maplibre-gl-draw';
93
+ import { createDrawUI, type DrawUI } from '@sakuzu/maplibre-gl-draw-ui';
94
+ import '@sakuzu/maplibre-gl-draw-ui/style.css';
95
+
96
+ const style = 'https://tiles.openfreemap.org/styles/liberty';
97
+ const container = ref<HTMLDivElement>();
98
+ let map: maplibregl.Map | undefined;
99
+ let draw: Draw | undefined;
100
+ let ui: DrawUI | undefined;
101
+
102
+ onMounted(() => {
103
+ map = new maplibregl.Map({ container: container.value!, style });
104
+ draw = createDraw(map);
105
+ ui = createDrawUI(draw);
106
+ });
107
+
108
+ onBeforeUnmount(() => {
109
+ ui?.destroy();
110
+ draw?.destroy();
111
+ map?.remove();
112
+ });
57
113
  </script>
114
+
115
+ <template>
116
+ <div ref="container" style="height: 400px"></div>
117
+ </template>
58
118
  ```
59
119
 
60
- The paths stand for where the page serves the files: maplibre-gl's
61
- `dist/maplibre-gl.mjs`, the `dist/` of this package, and a module of core
62
- with its own dependencies in it that imports `maplibre-gl` by that name.
120
+ `map`, `draw` and `ui` are plain variables, not `ref`s, which would wrap
121
+ them in proxies. The guide to
122
+ [using core with a framework](https://sakuzu.github.io/maplibre-gl-draw/guides/frameworks.html)
123
+ shows the same patterns for Svelte, and how to keep the map out of
124
+ server-side rendering. Under some bundlers, maplibre-gl 6 needs its
125
+ worker URL set once per page, as the
126
+ [README of core](https://github.com/sakuzu/maplibre-gl-draw#usage) shows
127
+ for Vite.
128
+
129
+ ### Without a bundler
130
+
131
+ A plain HTML page loads every module from a CDN through an import map.
132
+
133
+ ```html
134
+ <!doctype html>
135
+ <html lang="en">
136
+ <head>
137
+ <meta charset="utf-8">
138
+ <title>Draw</title>
139
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@6.11.1/dist/maplibre-gl.css">
140
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@sakuzu/maplibre-gl-draw-ui@1/dist/style.css">
141
+ <script type="importmap">
142
+ {
143
+ "imports": {
144
+ "maplibre-gl": "https://cdn.jsdelivr.net/npm/maplibre-gl@6.11.1/dist/maplibre-gl.mjs",
145
+ "@sakuzu/maplibre-gl-draw": "https://esm.sh/@sakuzu/maplibre-gl-draw@2?external=maplibre-gl",
146
+ "@sakuzu/maplibre-gl-draw/geometry": "https://esm.sh/@sakuzu/maplibre-gl-draw@2/geometry",
147
+ "@sakuzu/maplibre-gl-draw-ui": "https://cdn.jsdelivr.net/npm/@sakuzu/maplibre-gl-draw-ui@1/dist/maplibre-gl-draw-ui.js"
148
+ }
149
+ }
150
+ </script>
151
+ <style>
152
+ html, body, #map { height: 100%; margin: 0; }
153
+ </style>
154
+ </head>
155
+ <body>
156
+ <div id="map"></div>
157
+ <script type="module">
158
+ import * as maplibregl from 'maplibre-gl';
159
+ import { createDraw } from '@sakuzu/maplibre-gl-draw';
160
+ import { createDrawUI } from '@sakuzu/maplibre-gl-draw-ui';
161
+
162
+ const map = new maplibregl.Map({
163
+ container: 'map',
164
+ style: 'https://tiles.openfreemap.org/styles/liberty',
165
+ center: [139.767, 35.681],
166
+ zoom: 14,
167
+ });
168
+ createDrawUI(createDraw(map));
169
+ </script>
170
+ </body>
171
+ </html>
172
+ ```
173
+
174
+ The import map names four modules.
175
+
176
+ - `maplibre-gl` is maplibre-gl's own ES module build, at a version that
177
+ the peer of core accepts (`~6.11.1`). It starts its worker from the
178
+ same address by itself.
179
+ - `@sakuzu/maplibre-gl-draw` comes from esm.sh, which puts core's own
180
+ dependencies in it. `?external=maplibre-gl` leaves maplibre-gl to the
181
+ import map, so the page and the library share one copy.
182
+ - `@sakuzu/maplibre-gl-draw/geometry` is imported by the interface for
183
+ its measurements.
184
+ - `@sakuzu/maplibre-gl-draw-ui` is the single-file build of this
185
+ package, with Svelte and kata in it.
186
+
187
+ The same page works with files served by the application: put the
188
+ addresses of its own copies (maplibre-gl's `dist/maplibre-gl.mjs`, this
189
+ package's `dist/`, and a build of core with its dependencies in it that
190
+ imports `maplibre-gl` by that name) in the import map and the two
191
+ `<link>` elements. Pin exact versions in production.
63
192
 
64
193
  ## Use
65
194
 
@@ -85,8 +214,8 @@ The options of `createDrawUI`, all optional:
85
214
  | --- | --- | --- |
86
215
  | `container` | The positioned element it lies over | the map's container |
87
216
  | `toolbar` | `false`, or the `tools`, `delete` and `snapping` | `true` |
88
- | `inspector` | `false`, or the `tabs` and `operations` | `true` |
89
- | `layers` | `false`, or the `features`, `add` and `reorder` | `true` |
217
+ | `inspector` | `false`, or `tabs` (the first opens) and `operations` | `true` |
218
+ | `layers` | `false`, or `features`, `datasets`, `add`, `reorder` | `true` |
90
219
  | `legend` | The legend beside the layer panel | `true` |
91
220
  | `locale` | `en`, `ja`, or words laid over English | `en` |
92
221
  | `theme` | `light`, `dark` or `auto` (follows the system) | `auto` |
@@ -96,6 +225,12 @@ The options of `createDrawUI`, all optional:
96
225
  | `side` | The side panels `floating` over the map or `beside` it | `floating` |
97
226
  | `themeToggle` | The button that switches the look | `true` |
98
227
  | `mapControls` | maplibre-gl's globe, compass, zoom and scale | `true` |
228
+ | `basemaps` | The basemaps the basemap row opens | none |
229
+ | `basemap` | The ID of the basemap current at the start | the map's |
230
+ | `onbasemap` | Called with the basemap after it changed | none |
231
+ | `actions` | The actions of the application, in a card at the left | none |
232
+ | `actionsTitle` | The title of the card of the actions | `Actions` |
233
+ | `actionsOpen` | Whether the card of the actions starts unfolded | `true` |
99
234
 
100
235
  Each part also goes alone into an element of the page, with its own
101
236
  options and `target`, `locale` and `theme`: `createToolbar`,
@@ -103,6 +238,13 @@ options and `target`, `locale` and `theme`: `createToolbar`,
103
238
 
104
239
  The interface is laid over the map's container. The toolbar alone goes
105
240
  into any positioned element with `createToolbar(draw, { target })`.
241
+ The magnet at the end of the toolbar is pressed while snapping is on,
242
+ and opens the snapping settings above the toolbar: snapping itself and,
243
+ under it, the kinds of target (vertices, edges, intersections and
244
+ guides), then snapping to datasets, tracing edges and moving shared
245
+ vertices together, with the key that pauses snapping while it is held.
246
+ Each switch writes `draw.options.update`, and the settings follow
247
+ `options.changed`, so a change made by code shows too.
106
248
  The side panels float over the map, gap-md from its edges and as tall
107
249
  as their content; `side: 'beside'` docks them beside the map on a wide
108
250
  one instead. The map's padding follows the interface, the width of a
@@ -111,18 +253,36 @@ so that `fitBounds` and `easeTo` keep clear of them; `padding: false`
111
253
  leaves the map's padding alone.
112
254
 
113
255
  On the left, the layer panel and the legend share a panel, in two tabs.
114
- The layer panel is the tree of the layers, their groups and their
115
- features, from the front, with the eye, the lock, renaming in place (F2
116
- or a double click), reordering by dragging and an add menu (a new layer,
256
+ The layer panel has two sections, Stack and Basemap. Stack is the tree
257
+ of the layers, their groups and their features, from the front, with
258
+ the eye, the lock, reordering by dragging and an add menu (a new layer,
117
259
  a new group from the selected features). A feature is named by its
118
- `properties.name`, or by its type when it has none. The legend shows
119
- the rows of the style rule (`styleRule`) of each layer that has one.
260
+ `properties.name`, or by its type when it has none; names are changed
261
+ in the head of the inspector, not in the tree. Each row costs its
262
+ drawing, so a layer lists up to 1,000 features, those of its groups
263
+ included: a layer that holds more lists none of them and shows their
264
+ number instead, with a hint to select them on the map, and its own row
265
+ works as before (the eye, the lock, the active layer). `features` sets
266
+ the limit as a number, and `false` lists no features, only the groups.
267
+ The datasets (`draw.datasets`) are rows of the stack too, in their place
268
+ among the layers: those of `above-store` in front of every layer, those
269
+ of `layer-order` where `layers.getOrder()` places them, and those of
270
+ `below-store` behind every layer. A dataset row shows its ID (core gives
271
+ a dataset no name), with the eye (`setVisible`) and no lock; a press on
272
+ it leaves the selection as it is, and it is dragged among the layers
273
+ only when its order is
274
+ `layer-order`. Basemap, under it, is the back of the stack (see
275
+ [Basemaps](#basemaps)). The legend shows the rows of the style rule
276
+ (`styleRule`) of each layer and of each dataset that has one, in the
277
+ order of the stack.
120
278
  Shift+L opens and closes the panel, and while it is closed a button at
121
279
  the top left of the map opens it again.
122
280
 
123
281
  ```ts
124
282
  const ui = createDrawUI(draw, {
125
- layers: { features: true, add: true, reorder: true }, // or false for none
283
+ // or false for none; features: true lists up to 1,000 in a layer, a number
284
+ // sets that limit, and false lists none
285
+ layers: { features: true, datasets: true, add: true, reorder: true },
126
286
  legend: true,
127
287
  });
128
288
  ```
@@ -163,6 +323,9 @@ const ui = createDrawUI(draw, {
163
323
  });
164
324
  ```
165
325
 
326
+ The tabs open on the first of `tabs`: `['attributes', 'style']` opens
327
+ on Attributes. A tab chosen stays open from one feature to the next.
328
+
166
329
  A value typed into an attribute is kept as the string typed. The
167
330
  inspector alone goes into any element with
168
331
  `createInspector(draw, { target })`, and an application adds a section
@@ -209,8 +372,102 @@ open over the map or beside it, the button stands to the left of it.
209
372
 
210
373
  Beyond the look, the options above choose the parts and what each shows,
211
374
  `ui.tools.add` adds a tool for a mode of the application (see
212
- [Use](#use)), and `ui.inspector.sections.add` adds a section to the
213
- inspector (see [Inspector](#inspector)).
375
+ [Use](#use)), `ui.inspector.sections.add` adds a section to the
376
+ inspector (see [Inspector](#inspector)), and `ui.actions.add` adds an
377
+ action of the application (see [Actions](#actions)).
378
+
379
+ ### Actions
380
+
381
+ An action of the application, such as saving the drawing or turning a
382
+ setting on and off, is a row of a card at the bottom left of the map,
383
+ above maplibre-gl's scale: a switch (`kind: 'toggle'`) or a button
384
+ (`kind: 'action'`), with its key at the end. A press on the row and its
385
+ key both call `run`; a switch shows what `checked` returns, read again
386
+ after each run and on `ui.actions.refresh()`, so the state stays with
387
+ the application. `disabled` dims the row and turns its key off, and
388
+ `hint` is a caption under it. The keys are listed with `?` under the
389
+ title of the card, and do nothing while a field has the focus. A key the
390
+ interface uses (the keys of the tools, Delete, Backspace, Escape, `?`
391
+ and Shift+L) or another action uses is refused with an error. The card
392
+ shows while there is an action, its head folds it into one button, and
393
+ on a map narrower than 48rem it starts folded; where it would reach the
394
+ toolbar across, it stands above the toolbar, and the layer panel
395
+ floating at the left ends above it. Its title is `actionsTitle`, or the
396
+ word for actions of the locale.
397
+
398
+ ```ts
399
+ const ui = createDrawUI(draw, {
400
+ actions: [
401
+ {
402
+ id: 'read-only',
403
+ label: 'Read-only',
404
+ kind: 'toggle',
405
+ shortcut: 'R',
406
+ run: () => draw.setReadOnly(!draw.isReadOnly()),
407
+ checked: () => draw.isReadOnly(),
408
+ },
409
+ ],
410
+ });
411
+
412
+ const remove = ui.actions.add({
413
+ id: 'save',
414
+ label: 'Save',
415
+ kind: 'action',
416
+ shortcut: 'S',
417
+ run: () => localStorage.setItem('drawing', JSON.stringify(draw.document.toJSON())),
418
+ });
419
+ ```
420
+
421
+ ## Basemaps
422
+
423
+ The last section of the layer panel, Basemap, is the back of the stack:
424
+ one row under the tree of the layers, apart from it as the basemap is no
425
+ layer, so it is not dragged, hidden, locked or selected. The row shows
426
+ the name of the basemap the map shows: the label of the current one of
427
+ `basemaps`, else the `name` of the map's style, read again on each
428
+ `style.load`, else the word for a basemap.
429
+
430
+ With two or more `basemaps`, pressing the row opens them on the right,
431
+ in the place of the inspector: a list of their labels, each with its
432
+ `preview` (a value of CSS `background`, such as a gradient in the colors
433
+ of the style; a neutral square without one), and a check on the current
434
+ one. Opening it clears the selection, and selecting a feature on the map
435
+ or in the tree closes it, as do its close button and Escape. Choosing a
436
+ basemap replaces the map's style with its `style` (a URL or a style
437
+ object) and calls `onbasemap`, and the list stays open.
438
+ `ui.setBasemap(id)` does the same, and `ui.getBasemap()` returns the
439
+ current one. The current one at the start is `basemap`, or else the
440
+ first whose `style` is the URL the map's style was loaded from. With
441
+ fewer, the row only shows the name.
442
+
443
+ ```ts
444
+ const styles = 'https://tiles.openfreemap.org/styles';
445
+ const ui = createDrawUI(draw, {
446
+ basemaps: [
447
+ {
448
+ id: 'bright',
449
+ label: 'Bright',
450
+ style: `${styles}/bright`,
451
+ preview: 'linear-gradient(135deg, #f4f1ea, #dfe7d5)',
452
+ },
453
+ { id: 'dark', label: 'Dark', style: `${styles}/dark` },
454
+ ],
455
+ onbasemap: (basemap) => console.log(basemap.id),
456
+ });
457
+ ```
458
+
459
+ The layer panel put alone takes the same options
460
+ (`LayerPanelOptions`):
461
+ `createLayerPanel(draw, { target, basemaps, basemap, onbasemap })`.
462
+ There, the row opens the list in the place of the panel's sections,
463
+ until its close button or Escape closes it.
464
+
465
+ The drawing stays: choosing a basemap calls
466
+ `map.setStyle(style, { diff: false })`, which replaces the style whole,
467
+ and the draw instance adds its layers again on top of the new style once
468
+ it has loaded. Sources, layers and the terrain that the application
469
+ added to the map itself go with the old style, so it adds them again on
470
+ the map's `style.load` event.
214
471
 
215
472
  ## Map controls
216
473
 
@@ -0,0 +1,46 @@
1
+ import type { Shortcut } from '@sakuzu/kata/svelte';
2
+ import type { Messages } from './messages.js';
3
+ import { Box } from './store.js';
4
+ import type { ActionSpec, ActionsHandle, ToolEntry } from './types.js';
5
+ /**
6
+ * A key written one way: the modifiers in a fixed order, then the key, all in lowercase, so that
7
+ * `Shift+R`, `shift+r` and `r+shift` compare equal
8
+ */
9
+ export declare function normalizeKey(shortcut: string): string;
10
+ /**
11
+ * Checks an action of the application
12
+ *
13
+ * @throws Error when a field is missing or has the wrong type
14
+ */
15
+ export declare function checkAction(spec: ActionSpec): void;
16
+ /**
17
+ * Checks that the key of an action is free
18
+ *
19
+ * @throws Error when it is a key of the interface, of a tool or of another action
20
+ */
21
+ export declare function checkActionKey(spec: ActionSpec, tools: readonly ToolEntry[], messages: Messages, actions: readonly ActionSpec[]): void;
22
+ /** The actions of the card, and the count of their changes that the rows follow */
23
+ export interface ActionsState {
24
+ /** The actions in the order of the card */
25
+ readonly list: Box<ActionSpec[]>;
26
+ /** Changed after each run and on `refresh()`, so that the rows read `checked` and `disabled` */
27
+ readonly version: Box<number>;
28
+ }
29
+ /**
30
+ * The actions of the options, checked
31
+ *
32
+ * @throws Error when an action is not valid, two share an ID, or a key is taken
33
+ */
34
+ export declare function actionsState(actions: readonly ActionSpec[] | undefined, tools: readonly ToolEntry[], messages: Messages): ActionsState;
35
+ /** Runs an action, unless it is disabled, and has the rows read their state again */
36
+ export declare function runAction(state: ActionsState, spec: ActionSpec): void;
37
+ /** The actions of the card, to add to and to remove from */
38
+ export declare function actionsHandle(state: ActionsState, tools: Box<ToolEntry[]>, messages: Box<Messages>): ActionsHandle;
39
+ /** The keys of the actions, as shortcuts of kata's Shell under the title of the card */
40
+ export declare function actionShortcuts(state: ActionsState, group: string): Shortcut[];
41
+ /**
42
+ * Checks that the key of a tool is not the key of an action
43
+ *
44
+ * @throws Error when an action has the key
45
+ */
46
+ export declare function checkToolKey(id: string, shortcut: string | undefined, actions: readonly ActionSpec[]): void;
@@ -0,0 +1,58 @@
1
+ import { Box } from './store.js';
2
+ import type { Basemap } from './types.js';
3
+ /** The members of the map the basemaps use */
4
+ export interface BasemapMap {
5
+ setStyle(style: Basemap['style'], options?: {
6
+ diff?: boolean;
7
+ }): unknown;
8
+ /** The URL the style was loaded from (maplibre-gl 6), to find the current basemap */
9
+ getStyleUrl?(): string | null;
10
+ /** The style as it is now, whose name names a basemap that is none of the list */
11
+ getStyle?(): {
12
+ name?: string;
13
+ } | undefined;
14
+ /** Follows the load of each new style, to read its name again */
15
+ on?(type: 'style.load', listener: () => void): unknown;
16
+ off?(type: 'style.load', listener: () => void): unknown;
17
+ }
18
+ /** The basemaps of the options, checked, and the ID of the current one */
19
+ export interface BasemapSettings {
20
+ list: readonly Basemap[];
21
+ current: string | null;
22
+ }
23
+ /**
24
+ * Checks the basemaps of the options and finds the current one: `initial` when given, else the
25
+ * first whose style is the URL of the map's style, else none.
26
+ *
27
+ * @throws Error when a basemap has no ID, no label or no style, has a preview that is not a
28
+ * string, two share an ID, or `initial` is the ID of none of them
29
+ */
30
+ export declare function basemapSettings(basemaps: readonly Basemap[] | undefined, initial: string | undefined, styleUrl: string | null): BasemapSettings;
31
+ /** The basemaps on a map: the list, the current one, its name, and the change */
32
+ export interface BasemapControl {
33
+ /** The basemaps, in the order they are offered */
34
+ readonly list: readonly Basemap[];
35
+ /** The ID of the current basemap, which the row and the basemaps to choose from follow */
36
+ readonly current: Box<string | null>;
37
+ /**
38
+ * The name of the basemap the map shows: the label of the current basemap, else the `name` of
39
+ * the map's style, else null. Read inside a component, it is followed
40
+ */
41
+ name(): string | null;
42
+ /**
43
+ * Replaces the map's style with the style of a basemap and calls `onchange` with it; nothing
44
+ * happens when it is current already
45
+ *
46
+ * @throws Error when no basemap has this ID
47
+ */
48
+ set(id: string): void;
49
+ /** The current basemap, or null */
50
+ get(): Basemap | null;
51
+ /** Stops following the map's style */
52
+ destroy(): void;
53
+ }
54
+ /**
55
+ * Keeps the current basemap of a map and changes its style. The name of the map's style is read
56
+ * now and again on each `style.load`
57
+ */
58
+ export declare function basemapControl(map: BasemapMap, settings: BasemapSettings, onchange?: (basemap: Basemap) => void): BasemapControl;