@mosaicajs/components 0.2.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 +118 -37
  17. package/dist/explorer.js +487 -212
  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 +62 -14
  27. package/dist/item-card.js +310 -110
  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 +1848 -832
  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/README.md CHANGED
@@ -18,4 +18,6 @@ document.body.append(explorer);
18
18
 
19
19
  Each element has its own subpath, such as `@mosaicajs/components/map`. A page with no build step can load `dist/mosaica-components.js` instead: every element and its dependencies in one ES module, with its `sha384` integrity hash in `dist/mosaica-components.js.sri` and the notices of the third-party code inside it in `dist/mosaica-components.js.LICENSE.txt`.
20
20
 
21
+ Give each map an `aria-label`, and set `lang` on your page. [Accessibility](https://github.com/mosaicadb/mosaica/blob/main/docs/system/accessibility.md) lists the keys the elements take, what a screen reader is told and what is not built yet.
22
+
21
23
  Documentation: [Components reference](https://github.com/mosaicadb/mosaica/blob/main/docs/reference/components.md). Licence: Apache-2.0.
@@ -25,18 +25,12 @@ export declare function countsByKey(entry: AggregateEntry | undefined): Map<stri
25
25
  export declare function countedByLevel(layer: Pick<Layer, 'levels'>): boolean;
26
26
  /** Whether `layer` is a tree, `nested` or `dag`, whose clusters are ranked at the cut the map draws. */
27
27
  export declare function isTree(layer: Pick<Layer, 'hierarchy'>): boolean;
28
- /**
29
- * The level of a `stacked` or `tiered` layer that is counted and coloured: `chosen` where the layer
30
- * declares it, else its top (coarsest) level among those `served` at the camera's zoom, as the
31
- * map draws it. `undefined` on a layer with no levels to choose.
32
- */
33
- export declare function levelOf(layer: Pick<Layer, 'name' | 'hierarchy' | 'levels'>, chosen: number | null, served?: readonly {
34
- layer: string;
35
- rung: number;
36
- }[]): number | undefined;
28
+ /** Whether `layer` declares parent edges, `tiered`, `nested` or `dag`, so its artifacts can be walked by their parents and children. */
29
+ export declare function isLinked(layer: Pick<Layer, 'hierarchy'>): boolean;
37
30
  /**
38
31
  * The grouping ranking a layer's `top` clusters: at the cut the map draws on a tree, else at
39
- * {@link levelOf} `level`. Each row carries its slot in the palette the map colours clusters from.
32
+ * `level`, the level already worked out for the layer (`levelOfLayer`), or at no level where it is
33
+ * `null`. Each row carries its slot in the palette the map colours clusters from.
40
34
  */
41
35
  export declare function rankedGrouping(layer: Pick<Layer, 'name' | 'hierarchy' | 'levels'>, top: number, level: number | null): AggregateSpecGrouping;
42
36
  /**
package/dist/aggregate.js CHANGED
@@ -1,4 +1,4 @@
1
- import { jsonBody, topLevelServed } from '@mosaicajs/client/internal';
1
+ import { jsonBody } from '@mosaicajs/client/internal';
2
2
  /**
3
3
  * The listed groups of `table`, in the server's order: largest count first for a `top` grouping,
4
4
  * the order named otherwise. The `rest` and `none` rows are left out.
@@ -49,25 +49,19 @@ export function countedByLevel(layer) {
49
49
  export function isTree(layer) {
50
50
  return layer.hierarchy.kind === 'nested' || layer.hierarchy.kind === 'dag';
51
51
  }
52
- /**
53
- * The level of a `stacked` or `tiered` layer that is counted and coloured: `chosen` where the layer
54
- * declares it, else its top (coarsest) level among those `served` at the camera's zoom, as the
55
- * map draws it. `undefined` on a layer with no levels to choose.
56
- */
57
- export function levelOf(layer, chosen, served = []) {
58
- if ((layer.hierarchy.kind !== 'stacked' && layer.hierarchy.kind !== 'tiered') || layer.levels.length === 0)
59
- return undefined;
60
- return chosen !== null && layer.levels.some((l) => l.level === chosen) ? chosen : topLevelServed(layer, served);
52
+ /** Whether `layer` declares parent edges, `tiered`, `nested` or `dag`, so its artifacts can be walked by their parents and children. */
53
+ export function isLinked(layer) {
54
+ return layer.hierarchy.kind === 'tiered' || isTree(layer);
61
55
  }
62
56
  /**
63
57
  * The grouping ranking a layer's `top` clusters: at the cut the map draws on a tree, else at
64
- * {@link levelOf} `level`. Each row carries its slot in the palette the map colours clusters from.
58
+ * `level`, the level already worked out for the layer (`levelOfLayer`), or at no level where it is
59
+ * `null`. Each row carries its slot in the palette the map colours clusters from.
65
60
  */
66
61
  export function rankedGrouping(layer, top, level) {
67
62
  if (isTree(layer))
68
63
  return { count: true, by: { layer: layer.name, top, cut: 'drawn', paletteSize: 'drawn' } };
69
- const at = levelOf(layer, level);
70
- return { count: true, by: { layer: layer.name, ...(at === undefined || !countedByLevel(layer) ? {} : { level: at }), top, paletteSize: 'drawn' } };
64
+ return { count: true, by: { layer: layer.name, ...(level === null || !countedByLevel(layer) ? {} : { level }), top, paletteSize: 'drawn' } };
71
65
  }
72
66
  /**
73
67
  * The groupings that count `artifacts` of `layer` by name: one per level where the route addresses
@@ -6,7 +6,7 @@ import './count.js';
6
6
  * Use the artifact card to show an artifact the viewer has opened on the map, such as a cluster. An
7
7
  * artifact is a named group of items in an annotation layer. The card's count covers every member
8
8
  * the viewer may see, so it does not change as the map moves. Pressing one of the artifact's
9
- * parents or children opens that one instead.
9
+ * parents or children opens that one instead, and moves focus to its name.
10
10
  *
11
11
  * The card reads the store's opened artifact, or the `artifact` you set.
12
12
  *
@@ -25,22 +25,22 @@ import './count.js';
25
25
  * or highlight button added or removed a `member_of` clause.
26
26
  * @fires {CustomEvent<MosaicaEventDetails['mosaica-close']>} mosaica-close - The close button was
27
27
  * pressed, with `what` set to `artifact`.
28
- * @csspart title - The heading, with the level's or layer's title and the close button.
28
+ * @csspart title - The heading: the level's or layer's title.
29
29
  * @csspart close - The close button.
30
- * @csspart state - The state line, with `data-state`.
30
+ * @csspart state - The state line, with `data-state`, inside a `role="status"` region.
31
31
  * @csspart refusal - The words "Not available", with `data-code` set to the refusal's code.
32
32
  * @csspart reason - The server's reason for the refusal, under "Not available".
33
- * @csspart headline - The artifact's name, with `data-unnamed` if it has none.
33
+ * @csspart headline - The artifact's name, an `<h3>`, with `data-unnamed` if it has none.
34
34
  * @csspart count - The count of items the viewer may see.
35
35
  * @csspart content - The artifact's description, or its attached topic.
36
36
  * @csspart value - The layer and the key.
37
37
  * @csspart shape - How the layer's shapes are made, with `data-kind` (`derived`, `predicate` or
38
38
  * `authored`).
39
- * @csspart label - The headings above the parents and the children.
39
+ * @csspart label - The headings above the parents and the children, each an `<h3>`.
40
40
  * @csspart parents - The list of parents.
41
- * @csspart parent - One parent, with `data-id`.
41
+ * @csspart parent - One parent, a button, with `data-id`.
42
42
  * @csspart children - The list of children in this view.
43
- * @csspart child - One child, with `data-id`.
43
+ * @csspart child - One child, a button, with `data-id`.
44
44
  * @csspart name - A parent's or child's name, with `data-unnamed` if it has none.
45
45
  * @csspart fit - The Fit to cluster button.
46
46
  * @csspart verbs - The clause buttons.
@@ -65,7 +65,21 @@ export declare class MosaicaArtifactCard extends MosaicaElement {
65
65
  * Outside this replace each other, and a highlight is kept beside either.
66
66
  */
67
67
  private verb;
68
+ /** The parent or child pressed, its button, and the artifact it was pressed on, until the card shows another. */
69
+ private opening;
70
+ /** Open a parent or child in the card's place. */
71
+ private openRow;
72
+ /**
73
+ * Once the card shows the parent or child that was pressed, focus goes to its name, since the
74
+ * row that had it is gone; where it was refused, to the close button. Focus that has gone
75
+ * anywhere else meanwhile stays there.
76
+ */
77
+ protected updated(): void;
78
+ /** The parents or the children, each a button that opens it, under a heading that names the list. */
79
+ private rows;
68
80
  render(): TemplateResult | typeof nothing;
81
+ /** What the card shows of an artifact, under its heading. */
82
+ private body;
69
83
  }
70
84
  declare global {
71
85
  interface HTMLElementTagNameMap {
@@ -35,7 +35,7 @@ var __runInitializers = (this && this.__runInitializers) || function (thisArg, i
35
35
  import { css, html, nothing } from 'lit';
36
36
  import { property } from 'lit/decorators.js';
37
37
  import { artifactName, isFilterLayer, withMember, withoutMember } from '@mosaicajs/client';
38
- import { MosaicaElement, UNNAMED, emit, idString } from './base.js';
38
+ import { MosaicaElement, UNNAMED, emit, focusLost, idString } from './base.js';
39
39
  import { attachContextRoot, defineOnce } from './define.js';
40
40
  import { icon } from './icons.js';
41
41
  import { reasonOf, refusedState, renderState } from './states.js';
@@ -43,13 +43,6 @@ import { chrome, tokens } from './tokens.js';
43
43
  import './count.js';
44
44
  /** A parent's or child's name, or {@link UNNAMED}. */
45
45
  const nameSpan = (name) => html `<span part="name" class="name" ?data-unnamed=${name === null}>${name ?? UNNAMED}</span>`;
46
- /** A `role="button"` row's keys: Enter and Space press it. */
47
- const onKeys = (press) => (e) => {
48
- if (e.key !== 'Enter' && e.key !== ' ')
49
- return;
50
- e.preventDefault();
51
- press();
52
- };
53
46
  /** What each kind of drawn shape says on the card. */
54
47
  const SHAPE_TEXT = {
55
48
  derived: 'Drawn around the members you can see',
@@ -60,7 +53,7 @@ const SHAPE_TEXT = {
60
53
  * Use the artifact card to show an artifact the viewer has opened on the map, such as a cluster. An
61
54
  * artifact is a named group of items in an annotation layer. The card's count covers every member
62
55
  * the viewer may see, so it does not change as the map moves. Pressing one of the artifact's
63
- * parents or children opens that one instead.
56
+ * parents or children opens that one instead, and moves focus to its name.
64
57
  *
65
58
  * The card reads the store's opened artifact, or the `artifact` you set.
66
59
  *
@@ -79,22 +72,22 @@ const SHAPE_TEXT = {
79
72
  * or highlight button added or removed a `member_of` clause.
80
73
  * @fires {CustomEvent<MosaicaEventDetails['mosaica-close']>} mosaica-close - The close button was
81
74
  * pressed, with `what` set to `artifact`.
82
- * @csspart title - The heading, with the level's or layer's title and the close button.
75
+ * @csspart title - The heading: the level's or layer's title.
83
76
  * @csspart close - The close button.
84
- * @csspart state - The state line, with `data-state`.
77
+ * @csspart state - The state line, with `data-state`, inside a `role="status"` region.
85
78
  * @csspart refusal - The words "Not available", with `data-code` set to the refusal's code.
86
79
  * @csspart reason - The server's reason for the refusal, under "Not available".
87
- * @csspart headline - The artifact's name, with `data-unnamed` if it has none.
80
+ * @csspart headline - The artifact's name, an `<h3>`, with `data-unnamed` if it has none.
88
81
  * @csspart count - The count of items the viewer may see.
89
82
  * @csspart content - The artifact's description, or its attached topic.
90
83
  * @csspart value - The layer and the key.
91
84
  * @csspart shape - How the layer's shapes are made, with `data-kind` (`derived`, `predicate` or
92
85
  * `authored`).
93
- * @csspart label - The headings above the parents and the children.
86
+ * @csspart label - The headings above the parents and the children, each an `<h3>`.
94
87
  * @csspart parents - The list of parents.
95
- * @csspart parent - One parent, with `data-id`.
88
+ * @csspart parent - One parent, a button, with `data-id`.
96
89
  * @csspart children - The list of children in this view.
97
- * @csspart child - One child, with `data-id`.
90
+ * @csspart child - One child, a button, with `data-id`.
98
91
  * @csspart name - A parent's or child's name, with `data-unnamed` if it has none.
99
92
  * @csspart fit - The Fit to cluster button.
100
93
  * @csspart verbs - The clause buttons.
@@ -126,12 +119,27 @@ let MosaicaArtifactCard = (() => {
126
119
  :host {
127
120
  display: block;
128
121
  }
129
- [part='title'] .what {
122
+ .head {
123
+ display: flex;
124
+ align-items: center;
125
+ gap: 8px;
126
+ margin: 0 0 10px;
127
+ }
128
+ [part='title'] {
130
129
  flex: 1;
131
130
  min-width: 0;
131
+ margin: 0;
132
132
  }
133
- [part='headline'] {
134
- margin-bottom: 6px;
133
+ h3[part='headline'] {
134
+ display: block;
135
+ margin: 0 0 6px;
136
+ letter-spacing: 0;
137
+ text-transform: none;
138
+ color: var(--_mosaica-ink);
139
+ }
140
+ /* The name takes focus when the card opens a parent or child in its place; it is no control, so it shows no ring. */
141
+ [part='headline']:focus {
142
+ outline: none;
135
143
  }
136
144
  [part='count'] {
137
145
  display: flex;
@@ -151,16 +159,49 @@ let MosaicaArtifactCard = (() => {
151
159
  font-size: 12px;
152
160
  color: var(--_mosaica-ink-2);
153
161
  }
154
- .children-label {
162
+ dl,
163
+ dd {
164
+ margin: 0;
165
+ }
166
+ h3[part='label'] {
167
+ display: block;
155
168
  margin: 12px 0 4px;
169
+ font-weight: 400;
170
+ letter-spacing: 0;
171
+ text-transform: none;
156
172
  }
173
+ [part='parents'],
157
174
  [part='children'] {
158
175
  list-style: none;
159
176
  margin: 0;
160
177
  padding: 0;
161
178
  }
179
+ /* A parent or child: its name on one line, and its count at the right. */
180
+ [part='parent'],
181
+ [part='child'] {
182
+ display: flex;
183
+ align-items: center;
184
+ gap: 8px;
185
+ width: 100%;
186
+ min-height: 30px;
187
+ padding: 0 6px 0 22px;
188
+ border-radius: var(--_mosaica-radius-control);
189
+ text-align: left;
190
+ }
191
+ [part='parent']:hover,
192
+ [part='child']:hover {
193
+ background: var(--_mosaica-surface-2);
194
+ }
195
+ [part='name'] {
196
+ flex: 1 1 0;
197
+ min-width: 0;
198
+ overflow: hidden;
199
+ text-overflow: ellipsis;
200
+ white-space: var(--_mosaica-long-text);
201
+ overflow-wrap: anywhere;
202
+ }
203
+ [part='parent'] mosaica-count::part(count),
162
204
  [part='child'] mosaica-count::part(count) {
163
- margin-left: auto;
164
205
  color: var(--_mosaica-ink-2);
165
206
  font-size: 12px;
166
207
  font-weight: 400;
@@ -177,26 +218,14 @@ let MosaicaArtifactCard = (() => {
177
218
  [part='verbs'] .btn[aria-pressed='true'] {
178
219
  border-color: currentColor;
179
220
  background: var(--_mosaica-surface-2);
221
+ box-shadow: inset 0 -2px 0 currentColor;
180
222
  }
181
223
  [part='verbs'] [data-verb='highlight'][aria-pressed='true'] {
182
224
  background: var(--_mosaica-highlight-soft);
183
225
  color: var(--_mosaica-highlight);
184
226
  }
185
- .parents-label {
186
- margin: 12px 0 4px;
187
- }
188
- [part='parents'] {
189
- list-style: none;
190
- margin: 0;
191
- padding: 0;
192
- }
193
- [part='parent'] mosaica-count::part(count) {
194
- margin-left: auto;
195
- color: var(--_mosaica-ink-2);
196
- font-size: 12px;
197
- font-weight: 400;
198
- }
199
227
  [part='close'] {
228
+ flex: none;
200
229
  width: 24px;
201
230
  height: 24px;
202
231
  margin: -4px -4px -4px 0;
@@ -258,6 +287,46 @@ let MosaicaArtifactCard = (() => {
258
287
  >
259
288
  ${icon(outside ? 'outside' : verb === 'filter' ? 'filter' : 'highlight', 14)}${label}
260
289
  </button>`;
290
+ }
291
+ /** The parent or child pressed, its button, and the artifact it was pressed on, until the card shows another. */
292
+ opening = (__runInitializers(this, _refusal_extraInitializers), null);
293
+ /** Open a parent or child in the card's place. */
294
+ openRow(id, button) {
295
+ this.opening = { id, from: this.shown.artifact?.id ?? null, button };
296
+ void this.resolvedStore?.openArtifact(id);
297
+ }
298
+ /**
299
+ * Once the card shows the parent or child that was pressed, focus goes to its name, since the
300
+ * row that had it is gone; where it was refused, to the close button. Focus that has gone
301
+ * anywhere else meanwhile stays there.
302
+ */
303
+ updated() {
304
+ const opening = this.opening;
305
+ if (!opening)
306
+ return;
307
+ const { artifact, refusal } = this.shown;
308
+ if (!refusal && artifact?.id === opening.from)
309
+ return;
310
+ this.opening = null;
311
+ if (this.shadowRoot?.activeElement !== opening.button && !(focusLost() && !opening.button.isConnected))
312
+ return;
313
+ if (refusal)
314
+ this.renderRoot.querySelector('[part="close"]')?.focus();
315
+ else if (artifact?.id === opening.id)
316
+ this.renderRoot.querySelector('[part="headline"]')?.focus();
317
+ }
318
+ /** The parents or the children, each a button that opens it, under a heading that names the list. */
319
+ rows(children, rows, attached, stale) {
320
+ if (rows.length === 0)
321
+ return nothing;
322
+ const id = children ? 'children' : 'parents';
323
+ return html `<h3 part="label" class="xs muted" id=${id}>${children ? 'Children in this view' : 'Parents'}</h3>
324
+ <ul part=${children ? 'children' : 'parents'} aria-labelledby=${id}>
325
+ ${rows.map((a) => html `<li><button part=${children ? 'child' : 'parent'} type="button" data-id=${idString(a.mosaicaId)} @click=${(e) => this.openRow(a.mosaicaId, e.currentTarget)}>
326
+ ${nameSpan(artifactName(a, attached))}
327
+ <mosaica-count .masked=${{ value: Number(a.maskedCount), exact: true }} .stale=${stale}></mosaica-count>
328
+ </button></li>`)}
329
+ </ul>`;
261
330
  }
262
331
  render() {
263
332
  const { artifact, refusal } = this.shown;
@@ -269,29 +338,40 @@ let MosaicaArtifactCard = (() => {
269
338
  const declaredLayer = row?.layer ?? artifact?.detail.layer;
270
339
  const decl = declaredLayer === undefined ? undefined : metaLayers.find((l) => l.name === declaredLayer);
271
340
  const what = (row && decl?.levels.find((lv) => lv.level === row.rung)?.title) || decl?.title || decl?.name || 'Selected';
272
- // The close button appears only while the card shows something to close.
273
- const heading = (closable) => html `<h2 part="title"><span class="what">${what}</span>${closable
341
+ const state = refusal
342
+ ? refusedState('Not available', refusal.code, null, reasonOf(refusal))
343
+ : artifact
344
+ ? html `<span part="state" data-state="shown"></span>`
345
+ : this.resolvedStore
346
+ ? html `<span part="state" data-state="empty">Nothing selected</span>`
347
+ : renderState('detached', null);
348
+ // One template whatever the card shows, so the state's region is in the page before its words
349
+ // change. The close button appears only while the card shows something to close.
350
+ return html `<div class="panel">
351
+ <div class="head">
352
+ <h2 part="title">${what}</h2>
353
+ ${refusal || artifact
274
354
  ? html `<slot name="actions"></slot><button part="close" type="button" aria-label="Close" @click=${() => emit(this, 'mosaica-close', { what: 'artifact' })}>${icon('close', 14)}</button>`
275
- : nothing}</h2>`;
276
- if (refusal) {
277
- return html `<div class="panel">${heading(true)}${refusedState('Not available', refusal.code, null, reasonOf(refusal))}</div>`;
278
- }
279
- if (!artifact) {
280
- if (!this.resolvedStore)
281
- return html `<div class="panel">${heading(false)}${renderState('detached', null)}</div>`;
282
- return html `<div class="panel">${heading(false)}<span part="state" data-state="empty">Nothing selected</span></div>`;
283
- }
355
+ : nothing}
356
+ </div>
357
+ <div role="status">${state}</div>
358
+ ${artifact && !refusal ? this.body(artifact, decl) : nothing}
359
+ </div>`;
360
+ }
361
+ /** What the card shows of an artifact, under its heading. */
362
+ body(artifact, decl) {
284
363
  const s = this.resolvedStore;
285
364
  const artifacts = s?.get('artifacts');
286
365
  const served = artifacts?.served ?? [];
287
366
  const attached = artifacts?.attached ?? new Map();
367
+ const bySize = (a, b) => (a.maskedCount < b.maskedCount ? 1 : a.maskedCount > b.maskedCount ? -1 : 0);
288
368
  // The artifact's row and children are read from the served set now, on every render.
289
369
  const here = served.find((a) => a.mosaicaId === artifact.id);
290
370
  // On a `dag` layer a child appears on the card of each served parent.
291
- const children = served.filter((a) => a.parentIds.includes(artifact.id)).sort((a, b) => (a.maskedCount < b.maskedCount ? 1 : a.maskedCount > b.maskedCount ? -1 : 0));
371
+ const children = served.filter((a) => a.parentIds.includes(artifact.id)).sort(bySize);
292
372
  // Parents from the served set, which holds every parent the map draws. The hierarchy panel
293
373
  // asks `POST /v1/artifacts/browse` for the rest.
294
- const parents = here ? served.filter((a) => here.parentIds.includes(a.mosaicaId)).sort((a, b) => (a.maskedCount < b.maskedCount ? 1 : a.maskedCount > b.maskedCount ? -1 : 0)) : [];
374
+ const parents = here ? served.filter((a) => here.parentIds.includes(a.mosaicaId)).sort(bySize) : [];
295
375
  const stale = s?.get('status').stale ?? false;
296
376
  const count = { value: Number(artifact.detail.maskedCount), exact: true };
297
377
  const id = idString(artifact.id);
@@ -300,34 +380,16 @@ let MosaicaArtifactCard = (() => {
300
380
  const shape = decl?.shape ? { kind: decl.shape, text: SHAPE_TEXT[decl.shape] } : null;
301
381
  // A clause made here carries the drawn name as its label, since nothing downstream can resolve it.
302
382
  const clauseName = here ? artifactName(here, attached) : null;
303
- return html `<div class="panel">${heading(true)}
304
- <span part="state" data-state="shown"></span>
305
- <div part="headline" class="card-title" ?data-unnamed=${clauseName === null}>${clauseName ?? UNNAMED}</div>
383
+ return html `<h3 part="headline" class="card-title" tabindex="-1" ?data-unnamed=${clauseName === null}>${clauseName ?? UNNAMED}</h3>
306
384
  <div part="count"><mosaica-count .masked=${count} .stale=${stale} label="members"></mosaica-count></div>
307
385
  ${here && here.content.length > 0 && attached.has(here.mosaicaId) ? html `<p part="content">${attached.get(here.mosaicaId)}</p>` : here && here.content.length > 1 ? html `<p part="content">${here.content.slice(1).join(' · ')}</p>` : nothing}
308
- <div class="field">
309
- <div class="k">Layer</div><div part="value" class="v">${decl?.title || artifact.detail.layer}</div>
310
- ${artifact.detail.key ? html `<div class="k">Key</div><div part="value" class="v mono">${artifact.detail.key}</div>` : nothing}
311
- ${shape ? html `<div class="k">Shape</div><div part="shape" class="v" data-kind=${shape.kind}>${shape.text}</div>` : nothing}
312
- </div>
313
- ${parents.length > 0
314
- ? html `<div part="label" class="xs muted parents-label">Parents</div>
315
- <ul part="parents" class="list">
316
- ${parents.map((pnt) => html `<li part="parent" class="item child" role="button" tabindex="0" data-id=${idString(pnt.mosaicaId)} @click=${() => void s?.openArtifact(pnt.mosaicaId)} @keydown=${onKeys(() => void s?.openArtifact(pnt.mosaicaId))}>
317
- ${nameSpan(artifactName(pnt, attached))}
318
- <mosaica-count .masked=${{ value: Number(pnt.maskedCount), exact: true }} .stale=${stale}></mosaica-count>
319
- </li>`)}
320
- </ul>`
321
- : nothing}
322
- ${children.length > 0
323
- ? html `<div part="label" class="xs muted children-label">Children in this view</div>
324
- <ul part="children" class="list">
325
- ${children.map((c) => html `<li part="child" class="item child" role="button" tabindex="0" data-id=${idString(c.mosaicaId)} @click=${() => void s?.openArtifact(c.mosaicaId)} @keydown=${onKeys(() => void s?.openArtifact(c.mosaicaId))}>
326
- ${nameSpan(artifactName(c, attached))}
327
- <mosaica-count .masked=${{ value: Number(c.maskedCount), exact: true }} .stale=${stale}></mosaica-count>
328
- </li>`)}
329
- </ul>`
330
- : nothing}
386
+ <dl class="field">
387
+ <dt class="k">Layer</dt><dd part="value" class="v">${decl?.title || artifact.detail.layer}</dd>
388
+ ${artifact.detail.key ? html `<dt class="k">Key</dt><dd part="value" class="v mono">${artifact.detail.key}</dd>` : nothing}
389
+ ${shape ? html `<dt class="k">Shape</dt><dd part="shape" class="v" data-kind=${shape.kind}>${shape.text}</dd>` : nothing}
390
+ </dl>
391
+ ${this.rows(false, parents, attached, stale)}
392
+ ${this.rows(true, children, attached, stale)}
331
393
  ${
332
394
  // A filter layer draws nothing, so there is nothing to fit to.
333
395
  decl && isFilterLayer(decl)
@@ -337,12 +399,7 @@ let MosaicaArtifactCard = (() => {
337
399
  ${this.verb(artifact.detail.layer, artifact.id, false, 'filter', 'Filter to this', clauseName)}
338
400
  ${this.verb(artifact.detail.layer, artifact.id, false, 'highlight', 'Highlight this', clauseName)}
339
401
  ${this.verb(artifact.detail.layer, artifact.id, true, 'filter', 'Outside this', clauseName)}
340
- </div>
341
- </div>`;
342
- }
343
- constructor() {
344
- super(...arguments);
345
- __runInitializers(this, _refusal_extraInitializers);
402
+ </div>`;
346
403
  }
347
404
  };
348
405
  })();
package/dist/base.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { LitElement, type PropertyValues } from 'lit';
2
- import { type SelectionShape, type Store, type TokenSupplier } from '@mosaicajs/client';
1
+ import { LitElement, type PropertyValues, type TemplateResult } from 'lit';
2
+ import { type ColumnDraft, type Meta, type SelectionShape, type Store, type TokenSupplier } from '@mosaicajs/client';
3
3
  import type { SelectionShapeDetail, MosaicaEventDetails } from './events.js';
4
4
  /**
5
5
  * Where an element's store came from: `property` is its `store` property, `context` a
@@ -99,13 +99,38 @@ export declare abstract class MosaicaElement extends LitElement {
99
99
  dispose(): void;
100
100
  }
101
101
  /**
102
- * What stands where an artifact has no name, on an element carrying `data-unnamed`, which draws it
103
- * faint. A key such as `hdb-2422486` is an identifier and would read as a name, so it shows only in
104
- * the card's key field.
102
+ * What stands where an artifact has neither a name nor a key, on an element carrying
103
+ * `data-unnamed`, which draws it faint.
105
104
  */
106
105
  export declare const UNNAMED = "Unnamed";
106
+ /**
107
+ * What a list calls an artifact. `from` is `name` for its name, `key` for its publisher's key where
108
+ * it has no name, and `none` for {@link UNNAMED} where it has neither. A key is drawn on an element
109
+ * carrying `data-key-name`, in the secondary ink and the identifiers' font, so that it does not read
110
+ * as a name.
111
+ */
112
+ export type Called = {
113
+ text: string;
114
+ from: 'name' | 'key' | 'none';
115
+ };
116
+ /** What a list calls an artifact with this name and key, either of which may be missing or empty. */
117
+ export declare function called(name: string | null | undefined, key: string | null | undefined): Called;
118
+ /** What a list calls an artifact, as text marked `data-key-name` for a key and `data-unnamed` for {@link UNNAMED}. */
119
+ export declare function calledText(c: Called): TemplateResult;
120
+ /** What a list calls several artifacts, as {@link calledText} marks each, separated by commas. */
121
+ export declare function calledList(cs: readonly Called[]): TemplateResult;
122
+ /** The `label` a `member_of` clause on an artifact carries: what it is called, unless that is {@link UNNAMED}. */
123
+ export declare function labelOf(c: Called): {
124
+ label?: string;
125
+ };
107
126
  /** Emit one of the elements' events, bubbling and composed so it crosses shadow roots. */
108
127
  export declare function emit<K extends keyof MosaicaEventDetails>(from: HTMLElement, name: K, detail: MosaicaEventDetails[K]): void;
128
+ /**
129
+ * Whether focus is on nothing in the page, as it is once the control that had it has left: an
130
+ * element that removed that control may then place focus. Focus anywhere else is the viewer's to
131
+ * keep.
132
+ */
133
+ export declare function focusLost(): boolean;
109
134
  /**
110
135
  * A count shortened to its first figures and a letter, `250K`, `25.2M`, `3.5B`, where room is
111
136
  * short; below a thousand, whole.
@@ -125,6 +150,20 @@ export declare function columnCaption(name: string): string;
125
150
  * else the key.
126
151
  */
127
152
  export declare function keyTitle(store: Store | null, column: string, key: string): string;
153
+ /** A bool's two values, as the keys a bool card's rows carry. */
154
+ export type BoolKey = 'true' | 'false';
155
+ /** A bool's values in the order a card lists them. */
156
+ export declare const BOOL_KEYS: readonly BoolKey[];
157
+ /** A bool's values as the server stores, bins and filters them. */
158
+ export declare const BOOL_NUMBERS: Record<BoolKey, 0 | 1>;
159
+ /** A bool's values as a card, a chip and an item card name them. */
160
+ export declare const BOOL_WORDS: Record<BoolKey, string>;
161
+ /** Whether `column` is declared `bool`. */
162
+ export declare function isBoolColumn(meta: Meta | null | undefined, column: string): boolean;
163
+ /** The values of a bool, true then false, that a clause on it keeps: those whose 1 or 0 lies within its range. */
164
+ export declare function boolKeys(draft: ColumnDraft | undefined): BoolKey[];
165
+ /** The clause on a bool that keeps `keys`: a range over their 1 and 0, or no clause for none. */
166
+ export declare function boolClause(keys: readonly BoolKey[]): ColumnDraft;
128
167
  /**
129
168
  * A `timestamp_us` value in full, readably and in UTC: `14 March 2024, 12:00 UTC`, with seconds
130
169
  * where they are not zero. Fractions of a second are left out.
package/dist/base.js CHANGED
@@ -33,9 +33,9 @@ var __runInitializers = (this && this.__runInitializers) || function (thisArg, i
33
33
  return useValue ? value : void 0;
34
34
  };
35
35
  import { ContextConsumer } from '@lit/context';
36
- import { LitElement } from 'lit';
36
+ import { LitElement, html } from 'lit';
37
37
  import { property } from 'lit/decorators.js';
38
- import { createStore } from '@mosaicajs/client';
38
+ import { createStore, isPopulated } from '@mosaicajs/client';
39
39
  import { storeContext } from './context.js';
40
40
  /**
41
41
  * What every element shares: how it finds its store, and how it follows it.
@@ -292,15 +292,42 @@ let MosaicaElement = (() => {
292
292
  })();
293
293
  export { MosaicaElement };
294
294
  /**
295
- * What stands where an artifact has no name, on an element carrying `data-unnamed`, which draws it
296
- * faint. A key such as `hdb-2422486` is an identifier and would read as a name, so it shows only in
297
- * the card's key field.
295
+ * What stands where an artifact has neither a name nor a key, on an element carrying
296
+ * `data-unnamed`, which draws it faint.
298
297
  */
299
298
  export const UNNAMED = 'Unnamed';
299
+ /** What a list calls an artifact with this name and key, either of which may be missing or empty. */
300
+ export function called(name, key) {
301
+ if (name)
302
+ return { text: name, from: 'name' };
303
+ if (key)
304
+ return { text: key, from: 'key' };
305
+ return { text: UNNAMED, from: 'none' };
306
+ }
307
+ /** What a list calls an artifact, as text marked `data-key-name` for a key and `data-unnamed` for {@link UNNAMED}. */
308
+ export function calledText(c) {
309
+ return html `<span ?data-key-name=${c.from === 'key'} ?data-unnamed=${c.from === 'none'}>${c.text}</span>`;
310
+ }
311
+ /** What a list calls several artifacts, as {@link calledText} marks each, separated by commas. */
312
+ export function calledList(cs) {
313
+ return html `${cs.map((c, i) => html `${i > 0 ? ', ' : ''}${calledText(c)}`)}`;
314
+ }
315
+ /** The `label` a `member_of` clause on an artifact carries: what it is called, unless that is {@link UNNAMED}. */
316
+ export function labelOf(c) {
317
+ return c.from === 'none' ? {} : { label: c.text };
318
+ }
300
319
  /** Emit one of the elements' events, bubbling and composed so it crosses shadow roots. */
301
320
  export function emit(from, name, detail) {
302
321
  from.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }));
303
322
  }
323
+ /**
324
+ * Whether focus is on nothing in the page, as it is once the control that had it has left: an
325
+ * element that removed that control may then place focus. Focus anywhere else is the viewer's to
326
+ * keep.
327
+ */
328
+ export function focusLost() {
329
+ return document.activeElement === null || document.activeElement === document.body;
330
+ }
304
331
  /**
305
332
  * A count shortened to its first figures and a letter, `250K`, `25.2M`, `3.5B`, where room is
306
333
  * short; below a thousand, whole.
@@ -337,6 +364,28 @@ export function keyTitle(store, column, key) {
337
364
  const named = store.get('legend').categories[column]?.find((v) => v.key === key) ?? store.get('filters').suggestions[column]?.values.find((v) => v.key === key);
338
365
  return named?.title ?? key;
339
366
  }
367
+ /** A bool's values in the order a card lists them. */
368
+ export const BOOL_KEYS = ['true', 'false'];
369
+ /** A bool's values as the server stores, bins and filters them. */
370
+ export const BOOL_NUMBERS = { true: 1, false: 0 };
371
+ /** A bool's values as a card, a chip and an item card name them. */
372
+ export const BOOL_WORDS = { true: 'yes', false: 'no' };
373
+ /** Whether `column` is declared `bool`. */
374
+ export function isBoolColumn(meta, column) {
375
+ return meta?.declaredScalars.find((c) => c.name === column)?.arrowType === 'bool';
376
+ }
377
+ /** The values of a bool, true then false, that a clause on it keeps: those whose 1 or 0 lies within its range. */
378
+ export function boolKeys(draft) {
379
+ if (draft?.family !== 'numeric' || !isPopulated(draft))
380
+ return [];
381
+ const { gte, lte } = draft;
382
+ return BOOL_KEYS.filter((k) => (gte === null || BOOL_NUMBERS[k] >= gte) && (lte === null || BOOL_NUMBERS[k] <= lte));
383
+ }
384
+ /** The clause on a bool that keeps `keys`: a range over their 1 and 0, or no clause for none. */
385
+ export function boolClause(keys) {
386
+ const values = keys.map((k) => BOOL_NUMBERS[k]);
387
+ return values.length === 0 ? { family: 'numeric', gte: null, lte: null } : { family: 'numeric', gte: Math.min(...values), lte: Math.max(...values) };
388
+ }
340
389
  const DATE = new Intl.DateTimeFormat('en-GB', { day: 'numeric', month: 'long', year: 'numeric', timeZone: 'UTC' });
341
390
  const pad = (n) => String(n).padStart(2, '0');
342
391
  /**