@mosaicajs/components 0.3.0 → 0.4.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.
Files changed (52) hide show
  1. package/README.md +2 -0
  2. package/dist/aggregate.d.ts +4 -10
  3. package/dist/aggregate.js +7 -13
  4. package/dist/artifact-card.d.ts +21 -7
  5. package/dist/artifact-card.js +136 -79
  6. package/dist/base.d.ts +44 -5
  7. package/dist/base.js +54 -5
  8. package/dist/cluster-filter.d.ts +29 -25
  9. package/dist/cluster-filter.js +123 -106
  10. package/dist/colour-editor.d.ts +15 -4
  11. package/dist/colour-editor.js +107 -34
  12. package/dist/colour-picker.d.ts +12 -5
  13. package/dist/colour-picker.js +119 -32
  14. package/dist/display.js +77 -9
  15. package/dist/events.d.ts +21 -7
  16. package/dist/explorer.d.ts +116 -37
  17. package/dist/explorer.js +476 -210
  18. package/dist/field-card.d.ts +304 -48
  19. package/dist/field-card.js +1364 -244
  20. package/dist/filter-panel.d.ts +54 -10
  21. package/dist/filter-panel.js +242 -42
  22. package/dist/filter.d.ts +22 -4
  23. package/dist/filter.js +113 -34
  24. package/dist/hierarchy.d.ts +68 -45
  25. package/dist/hierarchy.js +320 -243
  26. package/dist/item-card.d.ts +47 -16
  27. package/dist/item-card.js +241 -108
  28. package/dist/key-picker.d.ts +2 -1
  29. package/dist/key-picker.js +4 -3
  30. package/dist/layer-picker.d.ts +4 -2
  31. package/dist/layer-picker.js +16 -7
  32. package/dist/lineage.d.ts +132 -0
  33. package/dist/lineage.js +307 -0
  34. package/dist/map.d.ts +135 -24
  35. package/dist/map.js +387 -117
  36. package/dist/mosaica-components.js +1837 -833
  37. package/dist/mosaica-components.js.sri +1 -1
  38. package/dist/mosaica-components.json +2 -2
  39. package/dist/parts.d.ts +4 -4
  40. package/dist/parts.js +18 -4
  41. package/dist/paths.d.ts +34 -12
  42. package/dist/paths.js +84 -46
  43. package/dist/selection.d.ts +22 -3
  44. package/dist/selection.js +125 -59
  45. package/dist/states.d.ts +72 -0
  46. package/dist/states.js +135 -26
  47. package/dist/status.d.ts +63 -5
  48. package/dist/status.js +215 -36
  49. package/dist/tokens.d.ts +8 -4
  50. package/dist/tokens.js +144 -14
  51. package/dist/view-picker.js +5 -0
  52. package/package.json +4 -3
package/dist/status.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { nothing, type TemplateResult } from 'lit';
1
+ import { nothing, type PropertyValues, type TemplateResult } from 'lit';
2
2
  import { MosaicaElement } from './base.js';
3
3
  import './count.js';
4
4
  /**
@@ -10,25 +10,50 @@ import './count.js';
10
10
  *
11
11
  * While the view is up to date, the first cell is a dot alone. When something goes wrong, the cell
12
12
  * names the state and offers a button if the viewer can act, such as Retry when the server is busy.
13
- * Sign in appears when the session expires only if you have set `reauthorise`.
13
+ * Sign in appears when the session expires only if you have set `reauthorise`. If the viewer may see
14
+ * no item in the view at all, the cell says so.
14
15
  *
15
- * Screen readers announce a refusal or an expiry, because the state cell is an `aria-live` region.
16
- * The counts sit outside it, so they are not read out every time the map pans.
16
+ * Where the strip is short of room, its cells wrap onto further lines.
17
+ *
18
+ * After a change of filters, highlight or selection, the cell reads Running query once the answer
19
+ * has taken more than a quarter of a second, until the map, the layers and the counts have all
20
+ * loaded. Its Cancel button goes back to the filters, highlight and selection that last finished
21
+ * loading.
22
+ *
23
+ * Screen readers announce the states the cell names in words, because the words are a live region.
24
+ * A pan announces nothing: up to date and updating are read but not announced, the counts sit
25
+ * outside the region, and Nothing in view is announced only as the answer to a change of filters,
26
+ * highlight or selection, not where a pan went. After such a change the region announces the counts
27
+ * once they have loaded, such as `3,923 of 50,000 match`, or that the view is up to date where there
28
+ * are no counts to say. After any state it announced, it announces that the view is up to date
29
+ * again. A state's button sits outside the region. When the button goes with its state while it has
30
+ * the focus, the state cell takes the focus. With `quiet`, the strip shows the same and announces
31
+ * nothing.
17
32
  *
18
33
  * @summary A status line showing whether the map is up to date and how many items it shows.
19
34
  * @tagname mosaica-status
20
35
  * @category Elements
21
36
  * @fires {CustomEvent<MosaicaEventDetails['mosaica-statechange']>} mosaica-statechange - The panel
22
37
  * state changed, including on the first render.
38
+ * @fires {CustomEvent<MosaicaEventDetails['mosaica-filterchange']>} mosaica-filterchange - Cancel
39
+ * went back to other filters or another highlight, once for each that changed, with `column` null.
40
+ * @fires {CustomEvent<MosaicaEventDetails['mosaica-clausechange']>} mosaica-clausechange - Cancel
41
+ * put back or took away a `member_of` clause, once for each.
42
+ * @fires {CustomEvent<MosaicaEventDetails['mosaica-selectchange']>} mosaica-selectchange - Cancel
43
+ * went back to another selected region, or to none, and once with `view` for each other view
44
+ * whose held region it put back or took away.
23
45
  * @fires {CustomEvent<MosaicaEventDetails['mosaica-expired']>} mosaica-expired - The session expired.
24
46
  * Fires once each time.
25
- * @csspart strip - The one-line strip.
47
+ * @csspart strip - The strip of cells.
26
48
  * @csspart state - The state's dot and words, with `data-state` set to the panel state.
27
49
  * @csspart refusal - The words "View refused", or "Server busy", with `data-code` set to the
28
50
  * refusal's code.
29
51
  * @csspart reason - The server's reason for a refusal, under "View refused".
30
52
  * @csspart refresh - The Refresh button, shown when the data has changed.
31
53
  * @csspart retry - The Retry button, after a refusal, no answer or a busy server.
54
+ * @csspart cancel - The Cancel button, while a query runs.
55
+ * @csspart spinner - The spinner beside the words Running query. The state part carries
56
+ * `data-querying` while it shows.
32
57
  * @csspart reauthorise - The Sign in button, shown on expiry if `reauthorise` is set.
33
58
  * @csspart count-shown - The `<mosaica-count>` of marks shown.
34
59
  * @csspart count-matched - The `<mosaica-count>` of items the filters match.
@@ -46,11 +71,44 @@ export declare class MosaicaStatus extends MosaicaElement {
46
71
  accessor compact: boolean;
47
72
  /** A function the Sign in button calls when the session has expired. Without it there is no Sign in button. */
48
73
  accessor reauthorise: (() => void) | null;
74
+ /**
75
+ * Announces nothing: every state and result is still shown, and none is put in a live region. Set
76
+ * it when another element on the page says the same things.
77
+ */
78
+ accessor quiet: boolean;
79
+ private readonly query;
80
+ private readonly actionFocus;
81
+ private readonly emptyAnswer;
49
82
  private lastState;
50
83
  private expiryFired;
84
+ /** What the strip shows now, decided before each render: `bare` is a dot alone, and `unsaid` what {@link liveLine} takes. */
85
+ private shown;
86
+ /** What the live region says while the strip shows a dot alone: a query's result, or that the view is back up to date. */
87
+ private heard;
88
+ /** The dot-alone state `heard` was last decided under. */
89
+ private bareState;
90
+ private wasQuerying;
91
+ /** Whether a query has finished and its result is still to be said. */
92
+ private resultDue;
93
+ /** Whether the strip has left a state it announced and has not yet said the view is up to date again. */
94
+ private recoveryDue;
51
95
  private get status();
52
96
  private get view();
97
+ disconnectedCallback(): void;
98
+ protected willUpdate(changed: PropertyValues<this>): void;
99
+ /**
100
+ * Decide what the live region says while the strip shows a dot alone. A pan moves between up to
101
+ * date and updating and says nothing. A query that finishes says its result once the counts are
102
+ * in, or that the view is up to date where there are no counts to say, and a return from a
103
+ * state the strip announced says the view is up to date. Either is taken away at the next
104
+ * change, so that it is said again when it next holds.
105
+ */
106
+ private hear;
107
+ /** The counts as the cells show them, in full figures, or `null` where there are none to show. */
108
+ private result;
53
109
  protected updated(): void;
110
+ /** Cancel the query, and tell the page what it went back to, as the controls that set those do. */
111
+ private cancel;
54
112
  render(): TemplateResult | typeof nothing;
55
113
  /**
56
114
  * The count cells: current in the shown state, greyed out while a new answer is awaited or the
package/dist/status.js CHANGED
@@ -34,10 +34,10 @@ var __runInitializers = (this && this.__runInitializers) || function (thisArg, i
34
34
  };
35
35
  import { css, html, nothing } from 'lit';
36
36
  import { property } from 'lit/decorators.js';
37
- import { NO_COUNT, NO_MASKED } from '@mosaicajs/client';
38
- import { MosaicaElement, emit } from './base.js';
37
+ import { NO_COUNT, NO_MASKED, formatMasked } from '@mosaicajs/client';
38
+ import { MosaicaElement, emit, idString, shapeDetail } from './base.js';
39
39
  import { attachContextRoot, defineOnce } from './define.js';
40
- import { NOTHING_VISIBLE, renderState, seesNothing, showsContent, stateOf } from './states.js';
40
+ import { ActionFocus, EmptyAnswer, NOTHING_VISIBLE, NamedQuery, UNSAID, liveLine, seesNothing, showsContent, stateLine, stateOf, stateWord } from './states.js';
41
41
  import { chrome, tokens } from './tokens.js';
42
42
  import './count.js';
43
43
  /**
@@ -49,25 +49,50 @@ import './count.js';
49
49
  *
50
50
  * While the view is up to date, the first cell is a dot alone. When something goes wrong, the cell
51
51
  * names the state and offers a button if the viewer can act, such as Retry when the server is busy.
52
- * Sign in appears when the session expires only if you have set `reauthorise`.
52
+ * Sign in appears when the session expires only if you have set `reauthorise`. If the viewer may see
53
+ * no item in the view at all, the cell says so.
53
54
  *
54
- * Screen readers announce a refusal or an expiry, because the state cell is an `aria-live` region.
55
- * The counts sit outside it, so they are not read out every time the map pans.
55
+ * Where the strip is short of room, its cells wrap onto further lines.
56
+ *
57
+ * After a change of filters, highlight or selection, the cell reads Running query once the answer
58
+ * has taken more than a quarter of a second, until the map, the layers and the counts have all
59
+ * loaded. Its Cancel button goes back to the filters, highlight and selection that last finished
60
+ * loading.
61
+ *
62
+ * Screen readers announce the states the cell names in words, because the words are a live region.
63
+ * A pan announces nothing: up to date and updating are read but not announced, the counts sit
64
+ * outside the region, and Nothing in view is announced only as the answer to a change of filters,
65
+ * highlight or selection, not where a pan went. After such a change the region announces the counts
66
+ * once they have loaded, such as `3,923 of 50,000 match`, or that the view is up to date where there
67
+ * are no counts to say. After any state it announced, it announces that the view is up to date
68
+ * again. A state's button sits outside the region. When the button goes with its state while it has
69
+ * the focus, the state cell takes the focus. With `quiet`, the strip shows the same and announces
70
+ * nothing.
56
71
  *
57
72
  * @summary A status line showing whether the map is up to date and how many items it shows.
58
73
  * @tagname mosaica-status
59
74
  * @category Elements
60
75
  * @fires {CustomEvent<MosaicaEventDetails['mosaica-statechange']>} mosaica-statechange - The panel
61
76
  * state changed, including on the first render.
77
+ * @fires {CustomEvent<MosaicaEventDetails['mosaica-filterchange']>} mosaica-filterchange - Cancel
78
+ * went back to other filters or another highlight, once for each that changed, with `column` null.
79
+ * @fires {CustomEvent<MosaicaEventDetails['mosaica-clausechange']>} mosaica-clausechange - Cancel
80
+ * put back or took away a `member_of` clause, once for each.
81
+ * @fires {CustomEvent<MosaicaEventDetails['mosaica-selectchange']>} mosaica-selectchange - Cancel
82
+ * went back to another selected region, or to none, and once with `view` for each other view
83
+ * whose held region it put back or took away.
62
84
  * @fires {CustomEvent<MosaicaEventDetails['mosaica-expired']>} mosaica-expired - The session expired.
63
85
  * Fires once each time.
64
- * @csspart strip - The one-line strip.
86
+ * @csspart strip - The strip of cells.
65
87
  * @csspart state - The state's dot and words, with `data-state` set to the panel state.
66
88
  * @csspart refusal - The words "View refused", or "Server busy", with `data-code` set to the
67
89
  * refusal's code.
68
90
  * @csspart reason - The server's reason for a refusal, under "View refused".
69
91
  * @csspart refresh - The Refresh button, shown when the data has changed.
70
92
  * @csspart retry - The Retry button, after a refusal, no answer or a busy server.
93
+ * @csspart cancel - The Cancel button, while a query runs.
94
+ * @csspart spinner - The spinner beside the words Running query. The state part carries
95
+ * `data-querying` while it shows.
71
96
  * @csspart reauthorise - The Sign in button, shown on expiry if `reauthorise` is set.
72
97
  * @csspart count-shown - The `<mosaica-count>` of marks shown.
73
98
  * @csspart count-matched - The `<mosaica-count>` of items the filters match.
@@ -88,15 +113,20 @@ let MosaicaStatus = (() => {
88
113
  let _reauthorise_decorators;
89
114
  let _reauthorise_initializers = [];
90
115
  let _reauthorise_extraInitializers = [];
116
+ let _quiet_decorators;
117
+ let _quiet_initializers = [];
118
+ let _quiet_extraInitializers = [];
91
119
  return class MosaicaStatus extends _classSuper {
92
120
  static {
93
121
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
94
122
  _expanded_decorators = [property({ type: Boolean })];
95
123
  _compact_decorators = [property({ type: Boolean, reflect: true })];
96
124
  _reauthorise_decorators = [property({ attribute: false })];
125
+ _quiet_decorators = [property({ type: Boolean, reflect: true })];
97
126
  __esDecorate(this, null, _expanded_decorators, { kind: "accessor", name: "expanded", static: false, private: false, access: { has: obj => "expanded" in obj, get: obj => obj.expanded, set: (obj, value) => { obj.expanded = value; } }, metadata: _metadata }, _expanded_initializers, _expanded_extraInitializers);
98
127
  __esDecorate(this, null, _compact_decorators, { kind: "accessor", name: "compact", static: false, private: false, access: { has: obj => "compact" in obj, get: obj => obj.compact, set: (obj, value) => { obj.compact = value; } }, metadata: _metadata }, _compact_initializers, _compact_extraInitializers);
99
128
  __esDecorate(this, null, _reauthorise_decorators, { kind: "accessor", name: "reauthorise", static: false, private: false, access: { has: obj => "reauthorise" in obj, get: obj => obj.reauthorise, set: (obj, value) => { obj.reauthorise = value; } }, metadata: _metadata }, _reauthorise_initializers, _reauthorise_extraInitializers);
129
+ __esDecorate(this, null, _quiet_decorators, { kind: "accessor", name: "quiet", static: false, private: false, access: { has: obj => "quiet" in obj, get: obj => obj.quiet, set: (obj, value) => { obj.quiet = value; } }, metadata: _metadata }, _quiet_initializers, _quiet_extraInitializers);
100
130
  if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
101
131
  }
102
132
  static styles = [
@@ -108,8 +138,11 @@ let MosaicaStatus = (() => {
108
138
  max-width: 100%;
109
139
  vertical-align: top;
110
140
  }
141
+ /* The cells wrap onto further lines where the strip is short of room, and a cell wider than
142
+ the strip wraps its own words. */
111
143
  [part='strip'] {
112
144
  display: inline-flex;
145
+ flex-wrap: wrap;
113
146
  align-items: stretch;
114
147
  max-width: 100%;
115
148
  min-height: 32px;
@@ -121,17 +154,19 @@ let MosaicaStatus = (() => {
121
154
  font-size: 12px;
122
155
  color: var(--_mosaica-ink-2);
123
156
  font-variant-numeric: tabular-nums;
124
- white-space: nowrap;
125
157
  }
126
158
  [part='strip'][data-state='detached'] {
127
159
  display: none;
128
160
  }
129
161
  .cell {
130
162
  display: flex;
163
+ flex-wrap: wrap;
131
164
  align-items: center;
132
- gap: 4px;
165
+ gap: 0 4px;
166
+ min-height: 30px;
133
167
  padding: 7px 12px;
134
- flex: none;
168
+ flex: 0 1 auto;
169
+ min-width: 0;
135
170
  }
136
171
  .cell mosaica-count {
137
172
  font-size: inherit;
@@ -139,8 +174,11 @@ let MosaicaStatus = (() => {
139
174
  .cell + .cell {
140
175
  border-left: 1px solid var(--_mosaica-line-2);
141
176
  }
177
+ /* Figures that wait for an answer, or stand from before a refusal, are all drawn in the
178
+ faint ink, which keeps a text's contrast. The state cell says why, by its dot's shape
179
+ or in words. */
142
180
  .cell.dim {
143
- opacity: 0.4;
181
+ color: var(--_mosaica-ink-3);
144
182
  }
145
183
  .cell.state {
146
184
  padding: 0 5px 0 12px;
@@ -148,8 +186,12 @@ let MosaicaStatus = (() => {
148
186
  .cell.state.bare {
149
187
  padding: 0 12px;
150
188
  }
189
+ .cell.state:focus-visible {
190
+ outline-offset: -2px;
191
+ }
151
192
  [part='state'] {
152
193
  gap: 7px;
194
+ min-width: 0;
153
195
  color: inherit;
154
196
  }
155
197
  [part='state'] .skel {
@@ -161,6 +203,7 @@ let MosaicaStatus = (() => {
161
203
  [part='state']:not(:has(.btn)) {
162
204
  padding-right: 7px;
163
205
  }
206
+
164
207
  .cell mosaica-count::part(count) {
165
208
  font-weight: 600;
166
209
  }
@@ -179,6 +222,10 @@ let MosaicaStatus = (() => {
179
222
  .cell mosaica-count.lit::part(count) {
180
223
  color: var(--_mosaica-highlight);
181
224
  }
225
+ .cell.dim mosaica-count::part(count),
226
+ .cell.dim mosaica-count::part(label) {
227
+ color: var(--_mosaica-ink-3);
228
+ }
182
229
  .cell mosaica-count::part(label) {
183
230
  margin-left: 0.3em;
184
231
  }
@@ -198,6 +245,10 @@ let MosaicaStatus = (() => {
198
245
  box-shadow: var(--_mosaica-shadow);
199
246
  font-size: 12px;
200
247
  }
248
+ [part='card'] dl,
249
+ [part='card'] dd {
250
+ margin: 0;
251
+ }
201
252
  `
202
253
  ];
203
254
  #expanded_accessor_storage = __runInitializers(this, _expanded_initializers, false);
@@ -212,15 +263,122 @@ let MosaicaStatus = (() => {
212
263
  /** A function the Sign in button calls when the session has expired. Without it there is no Sign in button. */
213
264
  get reauthorise() { return this.#reauthorise_accessor_storage; }
214
265
  set reauthorise(value) { this.#reauthorise_accessor_storage = value; }
215
- lastState = (__runInitializers(this, _reauthorise_extraInitializers), null);
266
+ #quiet_accessor_storage = (__runInitializers(this, _reauthorise_extraInitializers), __runInitializers(this, _quiet_initializers, false));
267
+ /**
268
+ * Announces nothing: every state and result is still shown, and none is put in a live region. Set
269
+ * it when another element on the page says the same things.
270
+ */
271
+ get quiet() { return this.#quiet_accessor_storage; }
272
+ set quiet(value) { this.#quiet_accessor_storage = value; }
273
+ query = (__runInitializers(this, _quiet_extraInitializers), new NamedQuery(this));
274
+ actionFocus = new ActionFocus();
275
+ emptyAnswer = new EmptyAnswer();
276
+ lastState = null;
216
277
  expiryFired = false;
278
+ /** What the strip shows now, decided before each render: `bare` is a dot alone, and `unsaid` what {@link liveLine} takes. */
279
+ shown = { state: 'detached', running: false, bare: false, line: stateLine('detached', null), unsaid: null };
280
+ /** What the live region says while the strip shows a dot alone: a query's result, or that the view is back up to date. */
281
+ heard = '';
282
+ /** The dot-alone state `heard` was last decided under. */
283
+ bareState = null;
284
+ wasQuerying = false;
285
+ /** Whether a query has finished and its result is still to be said. */
286
+ resultDue = false;
287
+ /** Whether the strip has left a state it announced and has not yet said the view is up to date again. */
288
+ recoveryDue = false;
217
289
  get status() {
218
290
  return this.resolvedStore?.get('status') ?? null;
219
291
  }
220
292
  get view() {
221
293
  return this.resolvedStore?.get('view') ?? null;
222
294
  }
295
+ disconnectedCallback() {
296
+ this.query.stop();
297
+ super.disconnectedCallback();
298
+ }
299
+ willUpdate(changed) {
300
+ super.willUpdate(changed);
301
+ this.actionFocus.note(this.renderRoot);
302
+ const status = this.status;
303
+ const state = stateOf(status);
304
+ const querying = status?.querying === true;
305
+ const refresh = () => this.resolvedStore?.refresh();
306
+ const nothingVisible = seesNothing(this.resolvedStore, state);
307
+ // A query that has run long enough to name. The map may have drawn its answer while the
308
+ // counts still load, and a refusal, an expiry or a lost server is said in its own words.
309
+ const running = querying && this.query.named && (state === 'loading' || state === 'shown' || state === 'empty' || state === 'stale');
310
+ // Up to date and updating, the two states a pan moves between, are each a dot alone, so the
311
+ // strip keeps its width through a gesture; every other state names itself beside its dot.
312
+ const bare = !running && !nothingVisible && (state === 'shown' || (state === 'loading' && status?.sessionWarm === true));
313
+ // Nothing in view is announced as a query's answer. Where a pan went, it is shown and not said.
314
+ const answered = this.emptyAnswer.note(state, querying);
315
+ const panned = state === 'empty' && !running && !nothingVisible && !answered;
316
+ this.hear(state, bare, panned);
317
+ const words = stateWord(state, status);
318
+ const line = running
319
+ ? { dot: null, words: 'Running query', action: html `<button part="cancel" class="btn small" type="button" @click=${() => this.cancel()}>Cancel</button>` }
320
+ : bare
321
+ ? // Read and not drawn, and left out while the live region is saying the same words.
322
+ { dot: state === 'shown' ? 'dot' : 'dot quiet', title: words, words: this.heard === words && !this.quiet ? nothing : html `<span class="sr-only">${words}</span>`, action: nothing }
323
+ : state === 'empty'
324
+ ? { dot: 'dot quiet', words: nothingVisible ? NOTHING_VISIBLE : words, action: nothing }
325
+ : stateLine(state, status, { onRefresh: refresh, onRetry: refresh, onReauthorise: this.reauthorise });
326
+ this.shown = { state, running, bare, line, unsaid: this.quiet || panned ? UNSAID : bare ? { heard: this.heard } : null };
327
+ }
328
+ /**
329
+ * Decide what the live region says while the strip shows a dot alone. A pan moves between up to
330
+ * date and updating and says nothing. A query that finishes says its result once the counts are
331
+ * in, or that the view is up to date where there are no counts to say, and a return from a
332
+ * state the strip announced says the view is up to date. Either is taken away at the next
333
+ * change, so that it is said again when it next holds.
334
+ */
335
+ hear(state, bare, panned) {
336
+ const querying = this.status?.querying === true;
337
+ if (querying)
338
+ this.resultDue = false;
339
+ else if (this.wasQuerying)
340
+ this.resultDue = true;
341
+ this.wasQuerying = querying;
342
+ if (!bare) {
343
+ // The words shown are the answer. Where a pan went is not announced, so leaving it is not.
344
+ this.heard = '';
345
+ this.bareState = null;
346
+ this.resultDue = false;
347
+ if (!panned)
348
+ this.recoveryDue = state !== 'detached';
349
+ return;
350
+ }
351
+ if (state !== this.bareState || querying)
352
+ this.heard = '';
353
+ this.bareState = state;
354
+ if (state !== 'shown' || querying)
355
+ return;
356
+ if (this.resultDue) {
357
+ // Counts still loading are waited for; refused or absent, the state is said in their place.
358
+ if (this.view?.inView?.status === 'loading')
359
+ return;
360
+ this.heard = this.result() ?? stateWord(state, this.status);
361
+ }
362
+ else if (this.recoveryDue)
363
+ this.heard = stateWord(state, this.status);
364
+ this.resultDue = false;
365
+ this.recoveryDue = false;
366
+ }
367
+ /** The counts as the cells show them, in full figures, or `null` where there are none to show. */
368
+ result() {
369
+ const v = this.view;
370
+ const n = v?.inView;
371
+ if (!v || !n || n.status !== 'shown')
372
+ return null;
373
+ const match = `${formatMasked(n.matched)} of ${formatMasked(n.visible)} match`;
374
+ if (!v.highlighting)
375
+ return match;
376
+ const lit = `${formatMasked(n.highlighted)} highlighted of ${formatMasked(n.matched)}`;
377
+ return this.resolvedStore?.requestFilters() != null ? `${match}, ${lit}` : lit;
378
+ }
223
379
  updated() {
380
+ this.actionFocus.keep(this.renderRoot.querySelector('.cell.state'));
381
+ this.query.track(this.status?.querying ?? false);
224
382
  const state = stateOf(this.status);
225
383
  if (state !== this.lastState) {
226
384
  const from = this.lastState;
@@ -235,29 +393,50 @@ let MosaicaStatus = (() => {
235
393
  this.expiryFired = false;
236
394
  }
237
395
  }
396
+ /** Cancel the query, and tell the page what it went back to, as the controls that set those do. */
397
+ cancel() {
398
+ const s = this.resolvedStore;
399
+ if (!s)
400
+ return;
401
+ const was = { ...s.get('filters'), shape: s.get('region')?.shape ?? null };
402
+ s.cancelQuery();
403
+ const now = s.get('filters');
404
+ const differ = (a, b) => JSON.stringify(a) !== JSON.stringify(b);
405
+ if (differ(was.expr, now.expr))
406
+ emit(this, 'mosaica-filterchange', { column: null, verb: 'filter', expr: now.expr });
407
+ if (differ(was.highlight, now.highlight))
408
+ emit(this, 'mosaica-filterchange', { column: null, verb: 'highlight', expr: now.highlight });
409
+ const keyed = (clauses) => new Map(clauses.map((c) => [`${c.verb} ${c.outside} ${c.artifact} ${c.layer}`, c]));
410
+ const [before, after] = [keyed(was.members), keyed(now.members)];
411
+ const told = (c, on) => emit(this, 'mosaica-clausechange', { id: idString(c.artifact), layer: c.layer, outside: c.outside, verb: c.verb, on });
412
+ for (const [key, clause] of before)
413
+ if (!after.has(key))
414
+ told(clause, false);
415
+ for (const [key, clause] of after)
416
+ if (!before.has(key))
417
+ told(clause, true);
418
+ const shape = s.get('region')?.shape ?? null;
419
+ if (shape !== was.shape)
420
+ emit(this, 'mosaica-selectchange', { shape: shapeDetail(shape), status: shape ? 'loading' : 'cleared' });
421
+ const heldIn = (regions) => new Map(regions.map((r) => [r.view, r.shape]));
422
+ const [heldBefore, heldAfter] = [heldIn(was.regions), heldIn(now.regions)];
423
+ for (const view of heldBefore.keys())
424
+ if (!heldAfter.has(view))
425
+ emit(this, 'mosaica-selectchange', { shape: null, view, status: 'cleared' });
426
+ for (const [view, held] of heldAfter)
427
+ if (heldBefore.get(view) !== held)
428
+ emit(this, 'mosaica-selectchange', { shape: shapeDetail(held), view, status: 'loading' });
429
+ }
238
430
  render() {
239
- const status = this.status;
240
- const state = stateOf(status);
241
- const stale = status?.stale ?? false;
431
+ const { state, running, bare, line, unsaid } = this.shown;
432
+ const stale = this.status?.stale ?? false;
242
433
  const v = this.view;
243
- const refresh = () => this.resolvedStore?.refresh();
244
- // Up to date and updating, the two states a pan moves between, are each a dot alone, so the
245
- // strip keeps its width through a gesture; every other state names itself beside its dot.
246
- const bare = (cls, words) => html `<span part="state" data-state=${state}><span class=${cls} role="img" aria-label=${words} title=${words}></span></span>`;
247
- // A viewer who can see nothing is told so once, by the map; the strip keeps its dot.
248
- const nothingVisible = seesNothing(this.resolvedStore, state);
249
- const dotAlone = state === 'shown' || (state === 'loading' && status?.sessionWarm === true) || nothingVisible;
250
- const inner = state === 'shown'
251
- ? bare('dot', 'Up to date')
252
- : nothingVisible
253
- ? bare('dot quiet', NOTHING_VISIBLE)
254
- : dotAlone
255
- ? bare('dot quiet', 'Updating')
256
- : state === 'empty'
257
- ? html `<span part="state" data-state="empty"><span class="dot quiet"></span>Nothing in view</span>`
258
- : renderState(state, status, { onRefresh: refresh, onRetry: refresh, onReauthorise: this.reauthorise });
259
- // One live element across every state, so each change of state is announced from it.
260
- const first = html `<div class=${`cell state${dotAlone ? ' bare' : ''}`} role="status" aria-live="polite">${inner}</div>`;
434
+ // One template for every state, so the live element stays and each change of its words is
435
+ // announced. The state's button sits outside it, and the cell takes the focus when the
436
+ // button goes.
437
+ const first = html `<div class=${`cell state${bare ? ' bare' : ''}`} tabindex="-1">
438
+ <span part="state" data-state=${state} ?data-querying=${running}>${running ? html `<span part="spinner" class="spinner" aria-hidden="true"></span>` : nothing}${liveLine(line, unsaid)}</span>
439
+ </div>`;
261
440
  return html `<div part="strip" data-state=${state}>${first}${this.counts(state, v, stale)}</div>
262
441
  ${this.expanded && showsContent(state) && v ? this.card(v, stale) : nothing}`;
263
442
  }
@@ -293,14 +472,14 @@ let MosaicaStatus = (() => {
293
472
  ${this.compact ? nothing : html `<div class=${cls}><mosaica-count part="count-shown" .count=${{ shown: n.shown, total: n.matched.value, exact: true }} figure="shown" label="shown"></mosaica-count></div>`}`;
294
473
  }
295
474
  card(v, stale) {
296
- const row = (label, value) => html `<div class="k">${label}</div><div class="v">${value}</div>`;
475
+ const row = (label, value) => html `<dt class="k">${label}</dt><dd class="v">${value}</dd>`;
297
476
  const n = v.inView;
298
- return html `<div part="card"><div class="kv">
477
+ return html `<div part="card"><dl class="kv">
299
478
  ${row('Shown', html `<mosaica-count .count=${n ? { shown: n.shown, total: n.matched.value, exact: true } : NO_COUNT} .stale=${stale}></mosaica-count>`)}
300
479
  ${row('Match the filters', html `<mosaica-count .masked=${n?.matched ?? NO_MASKED} .stale=${stale}></mosaica-count>`)}
301
480
  ${v.highlighting ? row('Highlighted', html `<mosaica-count .masked=${n?.highlighted ?? NO_MASKED} .stale=${stale}></mosaica-count>`) : nothing}
302
481
  ${row('In this view', html `<mosaica-count .masked=${n?.visible ?? NO_MASKED} .stale=${stale}></mosaica-count>`)}
303
- </div></div>`;
482
+ </dl></div>`;
304
483
  }
305
484
  };
306
485
  })();
package/dist/tokens.d.ts CHANGED
@@ -19,16 +19,18 @@
19
19
  * @cssprop --mosaica-ink - The colour of body text, values and map tools.
20
20
  * @cssprop --mosaica-ink-2 - The colour of labels, section headings and secondary text.
21
21
  * @cssprop --mosaica-ink-3 - The colour of faint text: placeholders, hints and disabled controls,
22
- * and of the dot in the loading states.
22
+ * and of the ring in the loading states.
23
23
  * @cssprop --mosaica-line - The colour of borders around panels, cards, buttons, chips and
24
24
  * the map toolbar.
25
25
  * @cssprop --mosaica-line-2 - The colour of the rules between sections and between the status
26
26
  * strip's cells.
27
- * @cssprop --mosaica-line-control - The colour of borders around inputs and of a slider's unfilled
28
- * track, at 3:1 against the surface.
27
+ * @cssprop --mosaica-line-control - The colour of borders around inputs, of a slider's unfilled
28
+ * track and of a switch's track when it is off, at 3:1 against the surface.
29
29
  * @cssprop --mosaica-accent - The colour of the active map tool, primary buttons, badges, checked
30
- * boxes and focus rings.
30
+ * boxes, a switch that is on, and the bar on a pressed segment and a chosen row.
31
31
  * @cssprop --mosaica-accent-ink - The text colour on an accent background.
32
+ * @cssprop --mosaica-focus - The colour of the focus ring, at 3:1 or more against each surface and
33
+ * the map's background.
32
34
  * @cssprop --mosaica-highlight - The colour of a highlight chip, a highlighted row's text and its ×.
33
35
  * @cssprop --mosaica-highlight-soft - The background of a highlight chip and a highlighted row.
34
36
  * @cssprop --mosaica-bar - The colour of a field card's bars for the items in view.
@@ -50,6 +52,8 @@
50
52
  * @cssprop --mosaica-space - The gap between the content in the map's corners and the map's edges.
51
53
  * Half of it separates items in one corner.
52
54
  * @cssprop --mosaica-density - A multiplier on `--mosaica-space`'s default.
55
+ * @cssprop --mosaica-long-text - What a title or a name too long for its line does: `nowrap`, the
56
+ * default, cuts it with an ellipsis; `normal` wraps it.
53
57
  * @cssprop --mosaica-map-height - The map's height.
54
58
  */
55
59
  export declare const tokens: import("lit").CSSResult;