@sakuzu/maplibre-gl-draw-ui 1.0.0 → 1.2.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/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
54
51
 
55
- const map = new Map({ container: 'map', style: '/style.json' });
56
- createDrawUI(createDraw(map));
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
86
+
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>
118
+ ```
119
+
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>
58
172
  ```
59
173
 
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.
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,26 +238,58 @@ 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
- one instead. The map's padding follows the interface, the width of a
109
- panel that stands beside the map and the toolbar's height at the bottom,
110
- so that `fitBounds` and `easeTo` keep clear of them; `padding: false`
111
- leaves the map's padding alone.
250
+ one instead. The map's padding follows the interface: at the left, the
251
+ room the left panel takes (beside the map, or floating over it with its
252
+ gap), and at the bottom, the height of the sheets of a narrow map, as
253
+ kata's Shell reports them (`onlayout`'s inset), so that `fitBounds` and
254
+ `easeTo` keep clear of them. The right panel opens and closes with the
255
+ selection and leaves the padding alone, so that the view does not jump;
256
+ the controls of the bottom right move out of its way instead (see
257
+ [Map controls](#map-controls)). `padding: false` leaves the map's
258
+ padding alone.
112
259
 
113
260
  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,
261
+ The layer panel has two sections, Stack and Basemap. Stack is the tree
262
+ of the layers, their groups and their features, from the front, with
263
+ the eye, the lock, reordering by dragging and an add menu (a new layer,
117
264
  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.
265
+ `properties.name`, or by its type when it has none; names are changed
266
+ in the head of the inspector, not in the tree. Each row costs its
267
+ drawing, so a layer lists up to 1,000 features, those of its groups
268
+ included: a layer that holds more lists none of them and shows their
269
+ number instead, with a hint to select them on the map, and its own row
270
+ works as before (the eye, the lock, the active layer). `features` sets
271
+ the limit as a number, and `false` lists no features, only the groups.
272
+ The datasets (`draw.datasets`) are rows of the stack too, in their place
273
+ among the layers: those of `above-store` in front of every layer, those
274
+ of `layer-order` where `layers.getOrder()` places them, and those of
275
+ `below-store` behind every layer. A dataset row shows its ID (core gives
276
+ a dataset no name), with the eye (`setVisible`) and no lock; a press on
277
+ it leaves the selection as it is. Every dataset is dragged among the
278
+ layers: one of `above-store` or `below-store` dropped there is moved to
279
+ `layer-order` (`draw.datasets.move`) and placed where it was dropped
280
+ (`draw.layers.reorder`), and goes back to its own order when the reorder
281
+ is refused. Basemap, under it, is the back of the stack (see
282
+ [Basemaps](#basemaps)). The legend shows the rows of the style rule
283
+ (`styleRule`) of each layer and of each dataset that has one, in the
284
+ order of the stack.
120
285
  Shift+L opens and closes the panel, and while it is closed a button at
121
286
  the top left of the map opens it again.
122
287
 
123
288
  ```ts
124
289
  const ui = createDrawUI(draw, {
125
- layers: { features: true, add: true, reorder: true }, // or false for none
290
+ // or false for none; features: true lists up to 1,000 in a layer, a number
291
+ // sets that limit, and false lists none
292
+ layers: { features: true, datasets: true, add: true, reorder: true },
126
293
  legend: true,
127
294
  });
128
295
  ```
@@ -146,15 +313,18 @@ ui.tools.add({
146
313
 
147
314
  The inspector is the panel on the right of `createDrawUI`. It opens
148
315
  while something is selected and closes, clearing the selection, with its
149
- close button. For one feature it shows its name (`properties.name`,
150
- changed where it stands); under it, its measurements and its description
151
- (`properties.description`, changed where it stands); then a Style tab
152
- with the fields its type reads and an Attributes tab with its other
153
- attributes. Several features
316
+ close button. On a narrow map it is a sheet from the bottom, as wide as
317
+ the map. For one feature it shows its name (`properties.name`, changed
318
+ where it stands); under it, its measurements, and the tabs, which stay
319
+ while the content scrolls: a Style tab with the fields its type reads
320
+ and an Attributes tab with its other attributes. The content of each tab
321
+ starts with its description (`properties.description`, changed where it
322
+ stands). Several features
154
323
  show the fields they share, a field whose values differ being mixed, and
155
324
  the operations that apply to them (union, intersection, difference,
156
325
  split and buffer). A layer and a group show their name, whether they are
157
- visible and whether they are locked.
326
+ visible and whether they are locked. Delete is at the start of the foot
327
+ of every inspector, for one feature and for several things selected.
158
328
 
159
329
  ```ts
160
330
  const ui = createDrawUI(draw, {
@@ -163,6 +333,9 @@ const ui = createDrawUI(draw, {
163
333
  });
164
334
  ```
165
335
 
336
+ The tabs open on the first of `tabs`: `['attributes', 'style']` opens
337
+ on Attributes. A tab chosen stays open from one feature to the next.
338
+
166
339
  A value typed into an attribute is kept as the string typed. The
167
340
  inspector alone goes into any element with
168
341
  `createInspector(draw, { target })`, and an application adds a section
@@ -191,15 +364,27 @@ ui.inspector?.sections.add({
191
364
  ## Customize
192
365
 
193
366
  Everything the interface draws is inside its root element, which has the
194
- class `mgd-ui`, and its style sheet reaches nothing outside it. kata's
195
- tokens (the CSS custom properties `--kata-*`) are set on that element;
196
- set them on `.mgd-ui` to change the look. The `theme` option of
367
+ class `mgd-ui`, and its style sheet reaches nothing outside it but
368
+ maplibre-gl's controls of the map (see [Map controls](#map-controls)).
369
+ kata's tokens (the CSS custom properties `--kata-*`) are set on that
370
+ element; set them on `.mgd-ui` to change the look. The `theme` option of
197
371
  `createDrawUI` and of each part put alone is `light`, `dark` or `auto`
198
372
  (the default, which follows the system's `prefers-color-scheme` as it
199
373
  changes), and `ui.setTheme` changes it. kata's theme is dark;
200
374
  `data-color-mode="light"` on the root element, which `light` sets, or on
201
375
  any element around it, turns it light.
202
376
 
377
+ maplibre-gl's controls in the map's container, those of the application
378
+ too, follow the theme of `createDrawUI`: while it is on the map,
379
+ maplibre-gl's control container (`.maplibregl-control-container`) has
380
+ `data-mgd-ui-controls`, which takes kata's tokens, and the
381
+ `data-color-mode` of the root. The groups of buttons, the lines between
382
+ them, the attribution and the scale are painted with the tokens, and
383
+ maplibre-gl's icons, which are dark images, are inverted in the dark
384
+ look (`filter: invert(1)`). Set the tokens on
385
+ `[data-mgd-ui-controls]` to change their look. `destroy()` removes both
386
+ attributes.
387
+
203
388
  A button at the top right of the map switches the look: a sun while it
204
389
  is dark, a moon while it is light. It sets the theme to the look that is
205
390
  not shown, so from `auto` it keeps the one the system does not prefer;
@@ -209,8 +394,106 @@ open over the map or beside it, the button stands to the left of it.
209
394
 
210
395
  Beyond the look, the options above choose the parts and what each shows,
211
396
  `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)).
397
+ [Use](#use)), `ui.inspector.sections.add` adds a section to the
398
+ inspector (see [Inspector](#inspector)), and `ui.actions.add` adds an
399
+ action of the application (see [Actions](#actions)).
400
+
401
+ ### Actions
402
+
403
+ An action of the application, such as saving the drawing or turning a
404
+ setting on and off, is a row of a card at the bottom left of the map,
405
+ above maplibre-gl's scale and, where its box reaches the card across, the
406
+ attribution: a switch (`kind: 'toggle'`) or a button
407
+ (`kind: 'action'`), with its key at the end. A press on the row and its
408
+ key both call `run`; a switch shows what `checked` returns, read again
409
+ after each run and on `ui.actions.refresh()`, so the state stays with
410
+ the application. `disabled` dims the row and turns its key off, and
411
+ `hint` is a caption under it. The keys are listed with `?` under the
412
+ title of the card, and do nothing while a field has the focus. A key the
413
+ interface uses (the keys of the tools, Delete, Backspace, Escape, `?`
414
+ and Shift+L) or another action uses is refused with an error. The card
415
+ shows while there is an action, its head folds it into one button, and
416
+ on a map narrower than 48rem it starts folded; where it would reach the
417
+ toolbar across, it stands above the toolbar, and the layer panel
418
+ floating at the left ends above it. The card is no taller than the map
419
+ above its place, less gap-md at the top and the button that opens the
420
+ layer panel again while it shows; the whole card scrolls when its rows
421
+ do not fit. Its title is `actionsTitle`, or the
422
+ word for actions of the locale.
423
+
424
+ ```ts
425
+ const ui = createDrawUI(draw, {
426
+ actions: [
427
+ {
428
+ id: 'read-only',
429
+ label: 'Read-only',
430
+ kind: 'toggle',
431
+ shortcut: 'R',
432
+ run: () => draw.setReadOnly(!draw.isReadOnly()),
433
+ checked: () => draw.isReadOnly(),
434
+ },
435
+ ],
436
+ });
437
+
438
+ const remove = ui.actions.add({
439
+ id: 'save',
440
+ label: 'Save',
441
+ kind: 'action',
442
+ shortcut: 'S',
443
+ run: () => localStorage.setItem('drawing', JSON.stringify(draw.document.toJSON())),
444
+ });
445
+ ```
446
+
447
+ ## Basemaps
448
+
449
+ The last section of the layer panel, Basemap, is the back of the stack:
450
+ one row under the tree of the layers, apart from it as the basemap is no
451
+ layer, so it is not dragged, hidden, locked or selected. The row shows
452
+ the name of the basemap the map shows: the label of the current one of
453
+ `basemaps`, else the `name` of the map's style, read again on each
454
+ `style.load`, else the word for a basemap.
455
+
456
+ With two or more `basemaps`, pressing the row opens them on the right,
457
+ in the place of the inspector: a list of their labels, each with its
458
+ `preview` (a value of CSS `background`, such as a gradient in the colors
459
+ of the style; a neutral square without one), and a check on the current
460
+ one. Opening it clears the selection, and selecting a feature on the map
461
+ or in the tree closes it, as do its close button and Escape. Choosing a
462
+ basemap replaces the map's style with its `style` (a URL or a style
463
+ object) and calls `onbasemap`, and the list stays open.
464
+ `ui.setBasemap(id)` does the same, and `ui.getBasemap()` returns the
465
+ current one. The current one at the start is `basemap`, or else the
466
+ first whose `style` is the URL the map's style was loaded from. With
467
+ fewer, the row only shows the name.
468
+
469
+ ```ts
470
+ const styles = 'https://tiles.openfreemap.org/styles';
471
+ const ui = createDrawUI(draw, {
472
+ basemaps: [
473
+ {
474
+ id: 'bright',
475
+ label: 'Bright',
476
+ style: `${styles}/bright`,
477
+ preview: 'linear-gradient(135deg, #f4f1ea, #dfe7d5)',
478
+ },
479
+ { id: 'dark', label: 'Dark', style: `${styles}/dark` },
480
+ ],
481
+ onbasemap: (basemap) => console.log(basemap.id),
482
+ });
483
+ ```
484
+
485
+ The layer panel put alone takes the same options
486
+ (`LayerPanelOptions`):
487
+ `createLayerPanel(draw, { target, basemaps, basemap, onbasemap })`.
488
+ There, the row opens the list in the place of the panel's sections,
489
+ until its close button or Escape closes it.
490
+
491
+ The drawing stays: choosing a basemap calls
492
+ `map.setStyle(style, { diff: false })`, which replaces the style whole,
493
+ and the draw instance adds its layers again on top of the new style once
494
+ it has loaded. Sources, layers and the terrain that the application
495
+ added to the map itself go with the old style, so it adds them again on
496
+ the map's `style.load` event.
214
497
 
215
498
  ## Map controls
216
499
 
@@ -228,12 +511,21 @@ const ui = createDrawUI(draw, {
228
511
  });
229
512
  ```
230
513
 
231
- A page that adds controls of its own passes `mapControls: false`. Where
232
- the toolbar reaches a bottom corner of the map across, as on a narrow
233
- map, that corner (its controls and the attribution) is lifted above the
234
- toolbar. This is the one rule of the style sheet outside the root
235
- element: it applies to the map's container while the interface is on
236
- it.
514
+ A page that adds controls of its own passes `mapControls: false`. The
515
+ bottom corners of the map are kept clear of the interface:
516
+
517
+ - where the toolbar reaches a bottom corner across, as on a narrow map,
518
+ that corner (its controls and the attribution) is lifted above the
519
+ toolbar
520
+ - while the right panel covers the bottom right of the map, the controls
521
+ there (not the attribution) move to the left of it, gap-md apart
522
+ - where the attribution's box reaches the scale across, as the two-line
523
+ attribution of a narrow map does, the bottom left corner is lifted
524
+ above it. The attribution is only read; its compact form is
525
+ maplibre-gl's own
526
+
527
+ These are the rules of the style sheet outside the root element: they
528
+ apply to the map's container while the interface is on it.
237
529
 
238
530
  ## Development
239
531
 
@@ -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;
@@ -1,4 +1,5 @@
1
1
  import { type ControlPosition, type IControl } from 'maplibre-gl';
2
+ import type { ShellInset } from './padding.js';
2
3
  import type { MapControlsOptions } from './types.js';
3
4
  /** The members of the map the controls use */
4
5
  export interface ControlsMap {
@@ -20,17 +21,32 @@ export interface MapControls {
20
21
  export declare function mapControls(map: ControlsMap, option: boolean | MapControlsOptions | undefined): MapControls;
21
22
  /** The attribute on the map's container while the interface lifts its bottom corners */
22
23
  export declare const LIFT_ATTRIBUTE = "data-mgd-ui-lift";
23
- /** The bottom corners of the map, lifted above the toolbar while it reaches them */
24
+ /** The bottom corners of the map, kept clear of the interface and of the attribution */
24
25
  export interface CornerLift {
25
- /** Measures again after the toolbar came, went or changed */
26
- update(): void;
26
+ /**
27
+ * Measures again after the toolbar came, went or changed, or the regions of the shell moved;
28
+ * with the inset the shell reported, which is kept until the next one
29
+ */
30
+ update(inset?: ShellInset): void;
27
31
  /** Stops following and puts the corners back */
28
32
  destroy(): void;
29
33
  }
30
34
  /**
31
- * Lifts the bottom corners of maplibre-gl's controls in `container` (the map's container) above
32
- * the toolbar of the interface in `root`, each while the toolbar reaches it across: by the
33
- * distance from the top of the toolbar to the bottom of the map. root.css moves the corners by
34
- * the custom properties this sets on the container (--mgd-ui-lift-left and --mgd-ui-lift-right).
35
+ * Keeps the bottom corners of maplibre-gl's controls in `container` (the map's container) clear
36
+ * of the interface in `root` and of the attribution. root.css moves them by the custom properties
37
+ * this sets on the container:
38
+ *
39
+ * - --mgd-ui-lift-left and --mgd-ui-lift-right lift a corner above the toolbar while the toolbar
40
+ * reaches it across, by the distance from the top of the toolbar to the bottom of the map; the
41
+ * controls of the bottom right moved to the left (below) count as reaching it where they are
42
+ * moved to
43
+ * - --mgd-ui-shift-right moves the controls of the bottom right, not the attribution, to the left
44
+ * while the right region covers the stage (the inset's right) and they are under it up and
45
+ * down: by the inset and gap-md
46
+ * - --mgd-ui-lift-left also lifts the bottom left corner above the attribution's box while that
47
+ * box reaches the corner across
48
+ *
49
+ * The boxes are measured where maplibre-gl places them, without what this moved. The attribution
50
+ * and the right region are followed with a ResizeObserver, as the toolbar and the corners are.
35
51
  */
36
52
  export declare function cornerLift(container: HTMLElement, root: HTMLElement): CornerLift;