ogm-viewer 0.4.0 → 0.5.1

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 (60) hide show
  1. package/dist/cjs/{lerc-CT61sKYw.js → lerc-IU-tWkVO.js} +6 -6
  2. package/dist/cjs/loader.cjs.js +1 -1
  3. package/dist/cjs/{ogm-attributes.ogm-image.ogm-map.ogm-menubar.ogm-metadata.ogm-previews.ogm-settings.ogm-sidebar.ogm-viewer-DKxSOrMP.js → ogm-alerts.ogm-attributes.ogm-image.ogm-map.ogm-menubar.ogm-metadata.ogm-preview.ogm-previews.ogm-settings.ogm-sidebar.ogm-viewer-BAGJpqMB.js} +465 -98
  4. package/dist/cjs/ogm-alerts_11.cjs.entry.js +18 -0
  5. package/dist/cjs/ogm-viewer.cjs.js +1 -1
  6. package/dist/collection/assets/icons/exclamation-triangle-fill.svg +3 -0
  7. package/dist/collection/collection-manifest.json +2 -0
  8. package/dist/collection/components/ogm-alerts/ogm-alerts.css +51 -0
  9. package/dist/collection/components/ogm-alerts/ogm-alerts.js +71 -0
  10. package/dist/collection/components/ogm-alerts/ogm-alerts.test.js +26 -0
  11. package/dist/collection/components/ogm-image/ogm-image.js +57 -5
  12. package/dist/collection/components/ogm-map/ogm-map.css +34 -40
  13. package/dist/collection/components/ogm-map/ogm-map.js +86 -22
  14. package/dist/collection/components/ogm-menubar/ogm-menubar.js +23 -2
  15. package/dist/collection/components/ogm-menubar/ogm-menubar.test.js +55 -0
  16. package/dist/collection/components/ogm-preview/ogm-preview.css +16 -0
  17. package/dist/collection/components/ogm-preview/ogm-preview.js +144 -0
  18. package/dist/collection/components/ogm-preview/ogm-preview.test.js +37 -0
  19. package/dist/collection/components/ogm-previews/ogm-previews.css +3 -2
  20. package/dist/collection/components/ogm-previews/ogm-previews.js +7 -18
  21. package/dist/collection/components/ogm-previews/ogm-previews.test.js +83 -0
  22. package/dist/collection/components/ogm-settings/ogm-settings.js +1 -1
  23. package/dist/collection/components/ogm-sidebar/ogm-sidebar.js +2 -2
  24. package/dist/collection/components/ogm-viewer/ogm-viewer.css +6 -2
  25. package/dist/collection/components/ogm-viewer/ogm-viewer.js +61 -29
  26. package/dist/collection/lib/errors.js +137 -0
  27. package/dist/collection/lib/errors.test.js +96 -0
  28. package/dist/collection/lib/sources/geojson.js +2 -1
  29. package/dist/collection/lib/sources/iiif-manifest.js +2 -3
  30. package/dist/collection/lib/sources/iiif-manifest.test.js +4 -4
  31. package/dist/esm/{lerc-Bz409klg.js → lerc-yC3LNu-h.js} +1 -1
  32. package/dist/esm/loader.js +1 -1
  33. package/dist/esm/{ogm-attributes.ogm-image.ogm-map.ogm-menubar.ogm-metadata.ogm-previews.ogm-settings.ogm-sidebar.ogm-viewer-C9zV3wMe.js → ogm-alerts.ogm-attributes.ogm-image.ogm-map.ogm-menubar.ogm-metadata.ogm-preview.ogm-previews.ogm-settings.ogm-sidebar.ogm-viewer-BpVupgSB.js} +465 -100
  34. package/dist/esm/ogm-alerts_11.entry.js +2 -0
  35. package/dist/esm/ogm-viewer.js +1 -1
  36. package/dist/ogm-viewer/assets/icons/exclamation-triangle-fill.svg +3 -0
  37. package/dist/ogm-viewer/ogm-viewer.esm.js +1 -1
  38. package/dist/ogm-viewer/p-11cf948e.entry.js +1 -0
  39. package/dist/ogm-viewer/p-DPOGJxir.js +2822 -0
  40. package/dist/ogm-viewer/{p-BrovdLv6.js → p-_eTjrSO2.js} +1 -1
  41. package/dist/types/components/ogm-alerts/ogm-alerts.d.ts +6 -0
  42. package/dist/types/components/ogm-alerts/ogm-alerts.test.d.ts +1 -0
  43. package/dist/types/components/ogm-image/ogm-image.d.ts +5 -0
  44. package/dist/types/components/ogm-map/ogm-map.d.ts +7 -0
  45. package/dist/types/components/ogm-menubar/ogm-menubar.d.ts +2 -1
  46. package/dist/types/components/ogm-preview/ogm-preview.d.ts +13 -0
  47. package/dist/types/components/ogm-preview/ogm-preview.test.d.ts +1 -0
  48. package/dist/types/components/ogm-previews/ogm-previews.d.ts +1 -5
  49. package/dist/types/components/ogm-previews/ogm-previews.test.d.ts +1 -0
  50. package/dist/types/components/ogm-viewer/ogm-viewer.d.ts +8 -2
  51. package/dist/types/components.d.ts +70 -0
  52. package/dist/types/lib/errors.d.ts +13 -0
  53. package/dist/types/lib/errors.test.d.ts +1 -0
  54. package/dist/types/lib/sources/source.d.ts +1 -1
  55. package/package.json +1 -1
  56. package/readme.md +1 -0
  57. package/dist/cjs/ogm-attributes_9.cjs.entry.js +0 -16
  58. package/dist/esm/ogm-attributes_9.entry.js +0 -2
  59. package/dist/ogm-viewer/p-86b8fafc.entry.js +0 -1
  60. package/dist/ogm-viewer/p-CpCT3zlD.js +0 -2750
@@ -46,4 +46,59 @@ describe('ogm-menubar', () => {
46
46
  `);
47
47
  });
48
48
  });
49
+ describe('when hideTitle is true', () => {
50
+ it('does not render the title', async () => {
51
+ const record = new OgmRecord({
52
+ id: 'stanford-ff359cr8805',
53
+ dct_title_s: 'Coho Salmon Watersheds: San Francisco Bay Area, California, 2011',
54
+ dct_description_sm: [],
55
+ gbl_resourceType_sm: ['Polygon data'],
56
+ gbl_resourceClass_sm: ['Datasets'],
57
+ dct_accessRights_s: 'Public',
58
+ gbl_mdVersion_s: 'Aardvark',
59
+ });
60
+ const { root } = await render(h("ogm-menubar", { record: record, hideTitle: true }));
61
+ const shadowRoot = root.shadowRoot;
62
+ expect(shadowRoot.querySelector('.title')).toBeNull();
63
+ });
64
+ it('toggles the title when the hideTitle prop changes', async () => {
65
+ const record = new OgmRecord({
66
+ id: 'stanford-ff359cr8805',
67
+ dct_title_s: 'Coho Salmon Watersheds: San Francisco Bay Area, California, 2011',
68
+ dct_description_sm: [],
69
+ gbl_resourceType_sm: ['Polygon data'],
70
+ gbl_resourceClass_sm: ['Datasets'],
71
+ dct_accessRights_s: 'Public',
72
+ gbl_mdVersion_s: 'Aardvark',
73
+ });
74
+ const { root, setProps } = await render(h("ogm-menubar", { record: record, hideTitle: false }));
75
+ const shadowRoot = root.shadowRoot;
76
+ expect(shadowRoot.querySelector('.title')?.textContent?.trim()).toBe('Coho Salmon Watersheds: San Francisco Bay Area, California, 2011');
77
+ await setProps({ hideTitle: true });
78
+ expect(shadowRoot.querySelector('.title')).toBeNull();
79
+ await setProps({ hideTitle: false });
80
+ expect(shadowRoot.querySelector('.title')?.textContent?.trim()).toBe('Coho Salmon Watersheds: San Francisco Bay Area, California, 2011');
81
+ });
82
+ });
83
+ describe('loading indicator', () => {
84
+ it('shows the spinner while loading', async () => {
85
+ const { root } = await render(h("ogm-menubar", { loading: true }));
86
+ const shadowRoot = root.shadowRoot;
87
+ expect(shadowRoot.querySelector('.loading-spinner')).not.toBeNull();
88
+ });
89
+ it('hides the spinner when not loading', async () => {
90
+ const { root } = await render(h("ogm-menubar", { loading: false }));
91
+ const shadowRoot = root.shadowRoot;
92
+ expect(shadowRoot.querySelector('.loading-spinner')).toBeNull();
93
+ });
94
+ it('toggles the spinner when the loading prop changes', async () => {
95
+ const { root, setProps } = await render(h("ogm-menubar", { loading: false }));
96
+ const shadowRoot = root.shadowRoot;
97
+ expect(shadowRoot.querySelector('.loading-spinner')).toBeNull();
98
+ await setProps({ loading: true });
99
+ expect(shadowRoot.querySelector('.loading-spinner')).not.toBeNull();
100
+ await setProps({ loading: false });
101
+ expect(shadowRoot.querySelector('.loading-spinner')).toBeNull();
102
+ });
103
+ });
49
104
  });
@@ -0,0 +1,16 @@
1
+ :host {
2
+ display: block;
3
+ height: 100%;
4
+ position: relative;
5
+ }
6
+
7
+ ogm-map,
8
+ ogm-image {
9
+ display: block;
10
+ height: 100%;
11
+ }
12
+
13
+ ogm-alerts {
14
+ position: absolute;
15
+ inset: 0;
16
+ }
@@ -0,0 +1,144 @@
1
+ import { h, Host } from "@stencil/core";
2
+ import IIIFSource from "../../lib/sources/iiif";
3
+ // Wraps a single source's preview and surfaces error(s) during the preview.
4
+ export class OgmPreview {
5
+ theme;
6
+ source;
7
+ previewOpacity;
8
+ sidebarPadding;
9
+ error;
10
+ // A new source is a fresh load attempt, so clear any error left over from the previous one.
11
+ resetError() {
12
+ this.error = undefined;
13
+ }
14
+ // Catch the load error emitted by the child map/image and show it in place of the preview.
15
+ handlePreviewError(event) {
16
+ event.stopPropagation();
17
+ this.error = event.detail;
18
+ }
19
+ // IIIF sources use the image viewer; every other source type is previewed on the map.
20
+ renderPreview() {
21
+ if (this.source instanceof IIIFSource) {
22
+ return h("ogm-image", { theme: this.theme, source: this.source, padding: this.sidebarPadding });
23
+ }
24
+ return h("ogm-map", { "preview-opacity": this.previewOpacity, theme: this.theme, source: this.source, padding: this.sidebarPadding });
25
+ }
26
+ render() {
27
+ return (h(Host, { key: 'c962bdcbebdebb1abde8900b0d0811be218fca46' }, this.renderPreview(), this.error && h("ogm-alerts", { key: '87b8217bbb7d5101d9f306f2492326d3f9f92429', theme: this.theme, error: this.error })));
28
+ }
29
+ static get is() { return "ogm-preview"; }
30
+ static get encapsulation() { return "shadow"; }
31
+ static get originalStyleUrls() {
32
+ return {
33
+ "$": ["ogm-preview.css"]
34
+ };
35
+ }
36
+ static get styleUrls() {
37
+ return {
38
+ "$": ["ogm-preview.css"]
39
+ };
40
+ }
41
+ static get properties() {
42
+ return {
43
+ "theme": {
44
+ "type": "string",
45
+ "mutable": false,
46
+ "complexType": {
47
+ "original": "'light' | 'dark'",
48
+ "resolved": "\"dark\" | \"light\"",
49
+ "references": {}
50
+ },
51
+ "required": false,
52
+ "optional": false,
53
+ "docs": {
54
+ "tags": [],
55
+ "text": ""
56
+ },
57
+ "getter": false,
58
+ "setter": false,
59
+ "reflect": false,
60
+ "attribute": "theme"
61
+ },
62
+ "source": {
63
+ "type": "unknown",
64
+ "mutable": false,
65
+ "complexType": {
66
+ "original": "Source",
67
+ "resolved": "Source",
68
+ "references": {
69
+ "Source": {
70
+ "location": "global",
71
+ "id": "global::Source"
72
+ }
73
+ }
74
+ },
75
+ "required": false,
76
+ "optional": false,
77
+ "docs": {
78
+ "tags": [],
79
+ "text": ""
80
+ },
81
+ "getter": false,
82
+ "setter": false
83
+ },
84
+ "previewOpacity": {
85
+ "type": "number",
86
+ "mutable": false,
87
+ "complexType": {
88
+ "original": "number",
89
+ "resolved": "number",
90
+ "references": {}
91
+ },
92
+ "required": false,
93
+ "optional": false,
94
+ "docs": {
95
+ "tags": [],
96
+ "text": ""
97
+ },
98
+ "getter": false,
99
+ "setter": false,
100
+ "reflect": false,
101
+ "attribute": "preview-opacity"
102
+ },
103
+ "sidebarPadding": {
104
+ "type": "number",
105
+ "mutable": false,
106
+ "complexType": {
107
+ "original": "number",
108
+ "resolved": "number",
109
+ "references": {}
110
+ },
111
+ "required": false,
112
+ "optional": false,
113
+ "docs": {
114
+ "tags": [],
115
+ "text": ""
116
+ },
117
+ "getter": false,
118
+ "setter": false,
119
+ "reflect": false,
120
+ "attribute": "sidebar-padding"
121
+ }
122
+ };
123
+ }
124
+ static get states() {
125
+ return {
126
+ "error": {}
127
+ };
128
+ }
129
+ static get watchers() {
130
+ return [{
131
+ "propName": "source",
132
+ "methodName": "resetError"
133
+ }];
134
+ }
135
+ static get listeners() {
136
+ return [{
137
+ "name": "previewError",
138
+ "method": "handlePreviewError",
139
+ "target": undefined,
140
+ "capture": false,
141
+ "passive": false
142
+ }];
143
+ }
144
+ }
@@ -0,0 +1,37 @@
1
+ import { describe, it, expect, h, vi } from "@stencil/vitest";
2
+ // Render with Stencil's low-level render directly rather than @stencil/vitest's `render` wrapper: the
3
+ // wrapper re-throws lifecycle errors, and the <ogm-map>/<ogm-image> mounted inside a preview throw
4
+ // when they try to initialize WebGL/OpenSeadragon (unavailable in the test DOM).
5
+ import { render as stencilRender } from "@stencil/core";
6
+ import GeoJSONSource from "../../lib/sources/geojson";
7
+ import { referenceError } from "../../lib/errors";
8
+ // Let Stencil's RAF-based update cycle flush after a state change (mirrors the vitest waitForChanges).
9
+ const flush = () => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
10
+ const renderPreview = async (source) => {
11
+ const container = document.createElement('div');
12
+ document.body.appendChild(container);
13
+ const consoleError = vi.spyOn(console, 'error').mockImplementation(() => { });
14
+ await stencilRender(h("ogm-preview", { source: source }), container);
15
+ const el = container.firstElementChild;
16
+ await el.componentOnReady?.();
17
+ consoleError.mockRestore();
18
+ return el;
19
+ };
20
+ describe('ogm-preview', () => {
21
+ it('renders a preview for a source', async () => {
22
+ const el = await renderPreview(new GeoJSONSource('id', 'http://example.com/data.json'));
23
+ const shadowRoot = el.shadowRoot;
24
+ expect(shadowRoot.querySelector('ogm-map')).not.toBeNull();
25
+ expect(shadowRoot.querySelector('ogm-alerts')).toBeNull();
26
+ });
27
+ it('shows the error over the preview when a previewError is reported', async () => {
28
+ const el = await renderPreview(new GeoJSONSource('id', 'http://example.com/data.json'));
29
+ const error = referenceError(new TypeError('Failed to fetch'), 'GeoJSON', 'http://example.com/data.json');
30
+ el.dispatchEvent(new CustomEvent('previewError', { detail: error, bubbles: true }));
31
+ await flush();
32
+ const shadowRoot = el.shadowRoot;
33
+ expect(shadowRoot.querySelector('ogm-alerts')).not.toBeNull();
34
+ // The preview stays mounted underneath so its in-flight load can finish and not leak loading state
35
+ expect(shadowRoot.querySelector('ogm-map')).not.toBeNull();
36
+ });
37
+ });
@@ -37,8 +37,9 @@ wa-tab::part(base) {
37
37
  font-size: var(--wa-font-size-s);
38
38
  }
39
39
 
40
- ogm-map,
41
- ogm-image {
40
+ /* ogm-preview wraps each tab's map/image; ogm-map is also rendered bare as the no-record fallback. */
41
+ ogm-preview,
42
+ ogm-map {
42
43
  display: block;
43
44
  height: 100%;
44
45
  }
@@ -10,25 +10,15 @@ import XYZSource from "../../lib/sources/xyz";
10
10
  import IIIFSource from "../../lib/sources/iiif";
11
11
  import IIIFManifestSource from "../../lib/sources/iiif-manifest";
12
12
  export class OgmPreviews {
13
- el;
14
13
  theme;
15
14
  record;
16
15
  previewOpacity;
17
16
  sidebarPadding;
18
17
  sources = [];
19
- // Pick the appropriate preview to render based on the source type
20
- renderPreview(source) {
21
- if (source instanceof IIIFSource)
22
- return this.renderImagePreview(source);
23
- return this.renderMapPreview(source);
24
- }
25
- // Render an image preview
26
- renderImagePreview(source) {
27
- return h("ogm-image", { theme: this.theme, source: source, padding: this.sidebarPadding });
28
- }
29
- // Render a map preview
30
- renderMapPreview(source) {
31
- return h("ogm-map", { "preview-opacity": this.previewOpacity, theme: this.theme, source: source, padding: this.sidebarPadding });
18
+ // @Watch only fires on changes; handle the initial load here
19
+ componentWillLoad() {
20
+ if (this.record)
21
+ this.getSources(this.record);
32
22
  }
33
23
  // Given a record, get all of the valid sources that can be used to preview it on a map
34
24
  getSources(record) {
@@ -58,9 +48,9 @@ export class OgmPreviews {
58
48
  }
59
49
  // Render as tabs for switching between sources
60
50
  render() {
61
- if (!this.record)
62
- return h("ogm-map", { theme: this.theme, padding: this.sidebarPadding });
63
- return (h(Host, { class: this.theme && `wa-${this.theme}` }, h("wa-tab-group", null, this.sources.map((source, idx) => (h("wa-tab", { key: idx, panel: `${source.constructor.name}-${source.id}-${idx}` }, source.label()))), this.sources.map((source, idx) => (h("wa-tab-panel", { key: idx, name: `${source.constructor.name}-${source.id}-${idx}`, active: idx === 0 }, this.renderPreview(source)))))));
51
+ if (!this.record || !this.sources.length)
52
+ return;
53
+ return (h(Host, { class: this.theme && `wa-${this.theme}` }, h("wa-tab-group", null, this.sources.map((source, idx) => (h("wa-tab", { key: idx, panel: `${source.constructor.name}-${source.id}-${idx}` }, source.label()))), this.sources.map((source, idx) => (h("wa-tab-panel", { key: idx, name: `${source.constructor.name}-${source.id}-${idx}`, active: idx === 0 }, h("ogm-preview", { theme: this.theme, source: source, "preview-opacity": this.previewOpacity, "sidebar-padding": this.sidebarPadding })))))));
64
54
  }
65
55
  static get is() { return "ogm-previews"; }
66
56
  static get encapsulation() { return "shadow"; }
@@ -162,7 +152,6 @@ export class OgmPreviews {
162
152
  "sources": {}
163
153
  };
164
154
  }
165
- static get elementRef() { return "el"; }
166
155
  static get watchers() {
167
156
  return [{
168
157
  "propName": "record",
@@ -0,0 +1,83 @@
1
+ import { describe, it, expect, h, vi } from "@stencil/vitest";
2
+ // Render with Stencil's low-level render directly rather than @stencil/vitest's `render` wrapper: the
3
+ // wrapper re-throws lifecycle errors, and the <ogm-map> mounted inside a preview panel throws when it
4
+ // tries to initialize WebGL (unavailable in the test DOM). Stencil's own safeCall only routes that to
5
+ // console.error, so stencilRender still produces the ogm-previews shadow DOM we want to assert on.
6
+ import { render as stencilRender } from "@stencil/core";
7
+ import OgmRecord from "../../lib/record";
8
+ // Build a minimal Aardvark record, optionally with a WMS reference that yields one previewable source
9
+ const buildRecord = (previewable) => new OgmRecord({
10
+ id: 'berkeley-s7sq63',
11
+ dct_title_s: 'Calaveras County Contours',
12
+ gbl_resourceClass_sm: ['Datasets'],
13
+ dct_accessRights_s: 'Public',
14
+ gbl_mdVersion_s: 'Aardvark',
15
+ ...(previewable
16
+ ? {
17
+ gbl_wxsIdentifier_s: 's7sq63',
18
+ dct_references_s: JSON.stringify({ 'http://www.opengis.net/def/serviceType/ogc/wms': 'https://example.com/geoserver/wms' }),
19
+ }
20
+ : {}),
21
+ });
22
+ const renderPreviews = async (record) => {
23
+ const container = document.createElement('div');
24
+ document.body.appendChild(container);
25
+ // The mounted <ogm-map> logs a swallowed WebGL init error; keep test output clean
26
+ const consoleError = vi.spyOn(console, 'error').mockImplementation(() => { });
27
+ await stencilRender(h("ogm-previews", { record: record }), container);
28
+ const el = container.firstElementChild;
29
+ await el.componentOnReady?.();
30
+ consoleError.mockRestore();
31
+ return el.shadowRoot;
32
+ };
33
+ // Let Stencil's RAF-based update cycle flush so the nested <ogm-preview> renders its own shadow DOM.
34
+ const flush = () => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
35
+ // Render previews and drill into the single <ogm-preview> to see which preview (map or image) it
36
+ // chose. Going through <ogm-previews> means the source is constructed in the built component's module
37
+ // realm, so <ogm-preview>'s `instanceof IIIFSource` check holds (unlike a source built in the test).
38
+ const renderPreviewChild = async (record) => {
39
+ const container = document.createElement('div');
40
+ document.body.appendChild(container);
41
+ const consoleError = vi.spyOn(console, 'error').mockImplementation(() => { });
42
+ await stencilRender(h("ogm-previews", { record: record }), container);
43
+ const el = container.firstElementChild;
44
+ await el.componentOnReady?.();
45
+ const preview = el.shadowRoot.querySelector('ogm-preview');
46
+ await preview?.componentOnReady?.();
47
+ await flush();
48
+ consoleError.mockRestore();
49
+ return preview.shadowRoot;
50
+ };
51
+ // A minimal Aardvark record whose only reference is the given previewable one
52
+ const buildRecordWith = (references) => new OgmRecord({
53
+ id: 'test-record',
54
+ dct_title_s: 'Test Record',
55
+ gbl_resourceClass_sm: ['Datasets'],
56
+ dct_accessRights_s: 'Public',
57
+ gbl_mdVersion_s: 'Aardvark',
58
+ dct_references_s: JSON.stringify(references),
59
+ });
60
+ describe('ogm-previews', () => {
61
+ it('renders a tab for a record supplied at initial render', async () => {
62
+ const shadowRoot = await renderPreviews(buildRecord(true));
63
+ const tabs = shadowRoot.querySelectorAll('wa-tab');
64
+ expect(shadowRoot.querySelector('wa-tab-group')).not.toBeNull();
65
+ expect(tabs).toHaveLength(1);
66
+ expect(tabs[0].textContent).toContain('WMS');
67
+ });
68
+ it('renders nothing for a record with no previewable sources', async () => {
69
+ const shadowRoot = await renderPreviews(buildRecord(false));
70
+ expect(shadowRoot.querySelector('wa-tab-group')).toBeNull();
71
+ expect(shadowRoot.querySelectorAll('wa-tab')).toHaveLength(0);
72
+ });
73
+ it('wraps a map source in an ogm-preview that renders the map', async () => {
74
+ const previewShadow = await renderPreviewChild(buildRecordWith({ 'http://geojson.org/geojson-spec.html': 'https://example.com/data.json' }));
75
+ expect(previewShadow.querySelector('ogm-map')).not.toBeNull();
76
+ expect(previewShadow.querySelector('ogm-image')).toBeNull();
77
+ });
78
+ it('wraps a IIIF image source in an ogm-preview that renders the image viewer', async () => {
79
+ const previewShadow = await renderPreviewChild(buildRecordWith({ 'http://iiif.io/api/image': 'https://example.com/iiif/info.json' }));
80
+ expect(previewShadow.querySelector('ogm-image')).not.toBeNull();
81
+ expect(previewShadow.querySelector('ogm-map')).toBeNull();
82
+ });
83
+ });
@@ -12,7 +12,7 @@ export class OgmSettings {
12
12
  this.opacityChange.emit(this.waSlider.value);
13
13
  }
14
14
  render() {
15
- return (h("div", { key: '706e4027a015a9904b6a0ce792a35482b7b3e7ef', class: "settings" }, h("wa-slider", { key: '70129a4d1aab6fef042d818009814c8ffe5129f6', disabled: !this.record || this.record.references.iiifOnly, label: "Layer opacity", min: "0", max: "100", step: "1", value: "100" })));
15
+ return (h("div", { key: 'e2b21549c2bb82aa80780e99bcbed78497683690', class: "settings" }, h("wa-slider", { key: 'bc45061a08522c2e227be5b422cac15fb6da04c3', disabled: !this.record || this.record.references.iiifOnly, label: "Layer opacity", min: "0", max: "100", step: "1", value: "100" })));
16
16
  }
17
17
  static get is() { return "ogm-settings"; }
18
18
  static get encapsulation() { return "shadow"; }
@@ -20,7 +20,7 @@ export class OgmSidebar {
20
20
  this.tabs.active = 'information';
21
21
  }
22
22
  render() {
23
- return (h(Host, { key: 'f509e573871fdc8b54f33577f1f036401c087ea9', class: this.theme && `wa-${this.theme}` }, h("div", { key: '111ac328e9ba5d3bbc4d0783117c89e071e71b92', class: `sidebar ${this.open ? 'open' : ''}`, role: "region", "aria-label": "Sidebar", "aria-hidden": this.open ? 'false' : 'true' }, h("wa-tab-group", { key: '67d4ed7726ec166956c3ac27eb055938c9dc90c2', placement: "start" }, h("wa-tab", { key: '040c71d6ea4f023b1b37f24c0a12faa027d8873b', slot: "nav", panel: "information" }, h("wa-icon", { key: '84faaf111905ab96fdc2103a93ca17c50eadb03c', name: "info-circle-fill", label: "Information", canvas: "auto" })), h("wa-tab", { key: '59331d6334e81951fb2602accbe4a941cb0f38e2', slot: "nav", panel: "rights" }, h("wa-icon", { key: 'b86d3a0e2b4eac72727f23d531298943cba592a4', name: "c-circle", label: "Rights", canvas: "auto" })), h("wa-tab", { key: '829cb148639415680a29c3f274649a82935bb2ed', slot: "nav", panel: "links" }, h("wa-icon", { key: 'f9a083c20ab0073a3826eca7ffdf270292d0c521', name: "box-arrow-up-right", label: "Links", canvas: "auto" })), h("wa-tab", { key: 'e8eeaea243304648eb68d5b14ba9b7ed661c515c', slot: "nav", panel: "record" }, h("wa-icon", { key: '58b892bd76b00627c39b1d657d1728d221358a13', name: "braces", label: "Record", canvas: "auto" })), h("wa-tab", { key: '7f1363299fccd4e9f8ff56d5fda23c8bed277506', slot: "nav", panel: "settings" }, h("wa-icon", { key: '362f697d663fed741277d58c6e553baf1cca3a22', name: "sliders", label: "Settings", canvas: "auto" })), h("wa-tab-panel", { key: '09941411a1f7410b732ba2b319e5cdc886e2d390', name: "information" }, h("div", { key: '679de2935761f12cfbfd43e2330b02864333dcf1', class: "panel-header" }, "About this item"), h("div", { key: '6572b357d462d3e808d1f0b3dcf6fab7f693cdb0', class: "panel-content" }, h("ogm-metadata", { key: '9964e3d0f392837cdeca4deb4b6c8066d8848cb7', record: this.record, fieldNames: [
23
+ return (h(Host, { key: 'f2b189bf52bcb7deaec53be53c79083423ac2cc7', class: this.theme && `wa-${this.theme}` }, h("div", { key: '42ecd1fd9391fde0dd9cd8c0f2f2ca78d9cedea1', class: `sidebar ${this.open ? 'open' : ''}`, role: "region", "aria-label": "Sidebar", "aria-hidden": this.open ? 'false' : 'true' }, h("wa-tab-group", { key: '61d629c83e125027857e2e634131254f6f621b71', placement: "start" }, h("wa-tab", { key: '35b9b6998f79be73d7e30ed9ee87045b95a06130', slot: "nav", panel: "information" }, h("wa-icon", { key: '7335b6c1e7024b64d5183057eb39f5c3daacc0e1', name: "info-circle-fill", label: "Information", canvas: "auto" })), h("wa-tab", { key: 'cf39ed59867f70ebfb58eddea1a916d4a9b839be', slot: "nav", panel: "rights" }, h("wa-icon", { key: 'b664695aeaedf956d0881a1916ad341841a23c1f', name: "c-circle", label: "Rights", canvas: "auto" })), h("wa-tab", { key: 'aa6172e06b0349fedb1c35cbc6ed03cb4cfa17a5', slot: "nav", panel: "links" }, h("wa-icon", { key: 'a47c7cb356f776f042a41fe610810119bc9ed6ca', name: "box-arrow-up-right", label: "Links", canvas: "auto" })), h("wa-tab", { key: '9befb56e552c93464ac2e1cc5979de92ada1832b', slot: "nav", panel: "record" }, h("wa-icon", { key: 'fd2d86ba6459eecce6303668e91a71723090829d', name: "braces", label: "Record", canvas: "auto" })), h("wa-tab", { key: '54ad126ec6eef8b89e5748528d6e30ef3f7dcd6f', slot: "nav", panel: "settings" }, h("wa-icon", { key: 'abf83b541d84ccca8caff08fa505c262b8338157', name: "sliders", label: "Settings", canvas: "auto" })), h("wa-tab-panel", { key: '933d76a9e75741b9b17e0924888963e8777d512d', name: "information" }, h("div", { key: 'd0639a93e8cf8d79cbc0486c699db57a6b46f207', class: "panel-header" }, "About this item"), h("div", { key: 'b4415b06fd5e7fbce31107b41a527b6e524bf056', class: "panel-content" }, h("ogm-metadata", { key: '1eb858dd9f4fd676e4fb1f4ec7ca5f8210e21f5e', record: this.record, fieldNames: [
24
24
  'title',
25
25
  'description',
26
26
  'creators',
@@ -34,7 +34,7 @@ export class OgmSidebar {
34
34
  'temporal',
35
35
  'issued',
36
36
  'mdModified',
37
- ] }))), h("wa-tab-panel", { key: 'e57bbf559dd6587f4ac315a7fae175e3882bf245', name: "rights" }, h("div", { key: '4d207bd556a1c9975c1fe2ba7273898c7cb277d5', class: "panel-header" }, "Access conditions"), h("div", { key: '4cddf05f500b47b374f8abcf97aafe7f5778043a', class: "panel-content" }, h("ogm-metadata", { key: 'c5a8ab75b87ef32d3ccddd6eaaac67ffe161643d', record: this.record, fieldNames: ['provider', 'license', 'rights', 'rightsHolder'] }))), h("wa-tab-panel", { key: '3c61e94ea9387ec5574288ac02f0c5018698d340', name: "links" }, h("div", { key: '96866a01e6cfa16cfd7d01d5333b9d815bece444', class: "panel-header" }, "Links"), h("div", { key: 'd6a80e54d2484a50731fd80d210ab8608ff3b2b5', class: "panel-content" }, h("ogm-metadata", { key: '96ff29be57f6595f7a3e0fc3153f62415f0d004a', record: this.record, fieldNames: ['references'] }))), h("wa-tab-panel", { key: '4cdf9eee5be8b6bfa5d244a7a6d0835e1401e5ee', name: "record" }, h("div", { key: '70dc344f36664a6f2ae3de51fcfe6e4dada6ccf4', class: "panel-header" }, "Record view"), h("div", { key: '9de08526fa39cf08f6ae98b08fc6967d21c920d1', class: "panel-content" }, h("div", { key: '794e50ba83554943042102a1b2ef177a8a503e52', class: "record-json" }, JSON.stringify(this.record?.json, null, 2)))), h("wa-tab-panel", { key: '17afdc4e66bb2cc1f479cbd89c8c2b12c75b4c02', name: "settings" }, h("div", { key: 'a488b05c7ccd9e8778b0beb885b12ddcd67b77d0', class: "panel-header" }, "Settings"), h("div", { key: 'c33204c002758a55420c71cc1f60165e0f45d7ba', class: "panel-content" }, h("ogm-settings", { key: '0023f3f43cacc9b41c1fe6548b96002340d975ad', record: this.record })))))));
37
+ ] }))), h("wa-tab-panel", { key: '8bf2a486e918a0b172ec68261d43ab01d1e5927c', name: "rights" }, h("div", { key: '1ec5efad4edb737994982bc0e371174963437cb9', class: "panel-header" }, "Access conditions"), h("div", { key: '88e876ae699f82eec351217f731effcbd571036a', class: "panel-content" }, h("ogm-metadata", { key: '939c58b4fa0d96b3e042d951fac4199b448a757d', record: this.record, fieldNames: ['provider', 'license', 'rights', 'rightsHolder'] }))), h("wa-tab-panel", { key: '47563dce1a1d067f9ef2186e9ab423fe2f2725d9', name: "links" }, h("div", { key: '9021c78ef541235486f2d7a7d3fbeb88b02e3ec4', class: "panel-header" }, "Links"), h("div", { key: '868438dd07bfa1bdbfae1c8d9680c17aee026c31', class: "panel-content" }, h("ogm-metadata", { key: '868ac799f80c71e3730e80c9ee47afd887e4f22d', record: this.record, fieldNames: ['references'] }))), h("wa-tab-panel", { key: '1ca6a74b3b1a1b606a8a873ffae74ec6521a5fd6', name: "record" }, h("div", { key: '63f3998c6b8ba4cfed63e4b62d52b9ca7199ddcf', class: "panel-header" }, "Record view"), h("div", { key: '484f7b7ccd92f29ddfc1b9710b7a30868f39d63a', class: "panel-content" }, h("div", { key: '581a35264c287d71a77a897731c8a2d4098dcc4e', class: "record-json" }, JSON.stringify(this.record?.json, null, 2)))), h("wa-tab-panel", { key: '45f76acfae788d0e1fd5e158e54295c31b2b47ce', name: "settings" }, h("div", { key: '08e8c15c9e079d84a8444cb32bef6443feb303ce', class: "panel-header" }, "Settings"), h("div", { key: '9681b37b6e0c83cd5c2c953bbe90efbdfbbf6cc0', class: "panel-content" }, h("ogm-settings", { key: '00638f2ebf0a7a47f6df47aaf688492f42e86915', record: this.record })))))));
38
38
  }
39
39
  static get is() { return "ogm-sidebar"; }
40
40
  static get encapsulation() { return "shadow"; }
@@ -1,10 +1,14 @@
1
1
  :host {
2
2
  --stanford-cardinal-red: #8f1414;
3
- --accent-color: var(--wa-color-brand-50);
4
- /* --accent-color: var(--stanford-cardinal-red); */
5
3
  }
6
4
 
7
5
  .container {
6
+ /* Defined here rather than on :host because it resolves a Web Awesome token, which is only
7
+ available on .container (where the palette/theme stylesheet is activated by the
8
+ wa-palette-default / wa-light|wa-dark classes) and its descendants. */
9
+ --accent-color: var(--wa-color-brand-50);
10
+ /* --accent-color: var(--stanford-cardinal-red); */
11
+
8
12
  display: flex;
9
13
  flex-direction: column;
10
14
  position: relative;
@@ -1,31 +1,16 @@
1
1
  import { setBasePath, getBasePath, registerIconLibrary } from "@awesome.me/webawesome";
2
2
  import { Host, getAssetPath, h } from "@stencil/core";
3
3
  import OgmRecord from "../../lib/record";
4
+ import { fetchOrThrow, recordError } from "../../lib/errors";
4
5
  // Only need to call this once, at the top level
5
6
  setBasePath(getAssetPath(''));
6
7
  // Serve icons from our self-hosted bootstrap-icons subset instead of the default Font Awesome library
7
8
  registerIconLibrary('default', {
8
9
  resolver: name => getBasePath(`assets/icons/${name}.svg`),
9
10
  });
10
- // Web Awesome's raw color palette is scoped to `:root`, which can only ever match the actual
11
- // document root - never an element inside a shadow tree, so a component-scoped @import can't
12
- // activate it. Loading it here, at the document level, lets it match `:root` for real, and lets
13
- // its `.wa-light`/`.wa-dark` theme rules match the <ogm-viewer> host itself (see render() below),
14
- // from which every token inherits down through the rest of the shadow tree. Injecting it here
15
- // means consumers don't have to remember to add it to their own page.
16
- function loadWebAwesomeStylesheet() {
17
- const id = 'ogm-viewer-webawesome-styles';
18
- if (document.getElementById(id))
19
- return;
20
- const link = document.createElement('link');
21
- link.id = id;
22
- link.rel = 'stylesheet';
23
- link.href = getBasePath('assets/webawesome/styles/webawesome.css');
24
- document.head.appendChild(link);
25
- }
26
- loadWebAwesomeStylesheet();
27
11
  // Import all required Web Awesome components
28
12
  import "@awesome.me/webawesome/dist/components/button/button.js";
13
+ import "@awesome.me/webawesome/dist/components/callout/callout.js";
29
14
  import "@awesome.me/webawesome/dist/components/icon/icon.js";
30
15
  import "@awesome.me/webawesome/dist/components/scroller/scroller.js";
31
16
  import "@awesome.me/webawesome/dist/components/slider/slider.js";
@@ -34,14 +19,22 @@ import "@awesome.me/webawesome/dist/components/tab-group/tab-group.js";
34
19
  import "@awesome.me/webawesome/dist/components/tab-panel/tab-panel.js";
35
20
  import "@awesome.me/webawesome/dist/components/tab/tab.js";
36
21
  import "@awesome.me/webawesome/dist/components/tooltip/tooltip.js";
22
+ // Web Awesome activates its palette and semantic color variants on `:root` by default (with
23
+ // `.wa-*` classes only needed to override the defaults). `:root` never matches inside a shadow
24
+ // tree, so we opt into each default explicitly on our container to reproduce what a document-level
25
+ // load would give us: the default palette plus the default hue for every color variant.
26
+ const WA_SCOPE = 'wa-palette-default wa-brand-blue wa-neutral-gray wa-success-green wa-warning-yellow wa-danger-red';
37
27
  export class OgmViewer {
38
28
  el;
39
29
  recordUrl;
40
30
  theme = this.getThemePreference();
31
+ hideTitle = false;
41
32
  record;
33
+ error;
42
34
  previewOpacity = 100;
43
35
  sidebarOpen = false;
44
36
  loading = false;
37
+ loadingCount = 0;
45
38
  sidebarPadding = 0;
46
39
  // Prior to rendering, fetch the record if a URL is provided
47
40
  async componentWillLoad() {
@@ -63,32 +56,46 @@ export class OgmViewer {
63
56
  }
64
57
  // Can be called externally to set the record directly
65
58
  async loadRecord(record) {
59
+ this.error = undefined;
66
60
  this.record = record;
67
61
  }
68
62
  // Listen for opacity changes from the sidebar and adjust the preview layer
69
63
  adjustPreviewOpacity(event) {
70
64
  this.previewOpacity = event.detail;
71
65
  }
72
- // Listen for map to report loading started
66
+ // Listen for a preview to report loading started
73
67
  setLoadingStarted() {
68
+ this.loadingCount++;
74
69
  this.loading = true;
75
70
  }
76
- // Listen for map to report loading finished
71
+ // When all in-flight previews have loaded, clear loading state
77
72
  setLoadingFinished() {
73
+ this.loadingCount = Math.max(0, this.loadingCount - 1);
74
+ this.loading = this.loadingCount > 0;
75
+ }
76
+ // When a new record loads, reset the loading count and loading state
77
+ resetLoading() {
78
+ this.loadingCount = 0;
78
79
  this.loading = false;
79
80
  }
80
- // Fetch a record by URL and parse it into an OgmRecord instance
81
+ // Fetch a record by URL and parse it into an OgmRecord instance.
81
82
  async fetchRecord(recordUrl) {
82
- const response = await fetch(recordUrl);
83
- const data = await response.json();
84
- return new OgmRecord(data);
83
+ this.error = undefined;
84
+ try {
85
+ const response = await fetchOrThrow(recordUrl);
86
+ const data = await response.json();
87
+ return new OgmRecord(data);
88
+ }
89
+ catch (error) {
90
+ console.error(`Error loading record ${recordUrl}:`, error);
91
+ this.error = recordError(error, recordUrl);
92
+ return undefined;
93
+ }
85
94
  }
95
+ // Link Web Awesome's stylesheet inside the shadow tree so it doesn't
96
+ // leak into the host page when rendering.
86
97
  render() {
87
- return (
88
- // Applying the theme class to the host (rather than an internal div) lets the document-level
89
- // Web Awesome stylesheet's `.wa-light`/`.wa-dark` rules match it directly, since the host sits
90
- // in the consuming page's own light DOM while everything inside this shadow root does not.
91
- h(Host, { key: 'db64abdf2b961976aa7359d089a57dcaf37fb175', class: `wa-${this.theme}` }, h("div", { key: '8fc67a48693890f017f481b91d0b870a79a5dc58', class: "container" }, h("ogm-menubar", { key: '9818fefb02eec280b73319c87e0a11b93bd69626', theme: this.theme, record: this.record, loading: this.loading }), h("div", { key: '5653ffea28fe78c8aa2d254db29e5c5d0ece96eb', class: "main-container" }, h("ogm-sidebar", { key: '97bda51d3b84e9d535e25bc1abb7615703485450', theme: this.theme, record: this.record, open: this.sidebarOpen }), h("ogm-previews", { key: '97f8dab53279ac2a9ce5f0a1da2e9f0dcb4ec5fd', theme: this.theme, record: this.record, "preview-opacity": this.previewOpacity, "sidebar-padding": this.sidebarPadding })))));
98
+ return (h(Host, { key: '042eb882fe5979453b3ea32c11dd9a95dfff6cc1', class: `wa-${this.theme}` }, h("link", { key: '510b4ccabdff543fd3724d9f00fcce2fc0da59e4', rel: "stylesheet", href: getBasePath('assets/webawesome/styles/themes/default.css') }), h("div", { key: 'ca0916da0a4e14fc6dbda29bc0ed7fcbab4389fa', class: `container ${WA_SCOPE} wa-${this.theme}` }, h("ogm-menubar", { key: 'e153abb59c5aa5858e723f1496916517c69a6f79', theme: this.theme, record: this.record, loading: this.loading, hideTitle: this.hideTitle }), h("div", { key: 'e83ad788ed7a7da1cbe6771184ba821fedebf868', class: "main-container" }, h("ogm-sidebar", { key: '664f9ad8f2b64f998e6fe2a67116413a6d1ac377', theme: this.theme, record: this.record, open: this.sidebarOpen }), this.error ? (h("ogm-alerts", { theme: this.theme, error: this.error })) : (h("ogm-previews", { theme: this.theme, record: this.record, "preview-opacity": this.previewOpacity, "sidebar-padding": this.sidebarPadding }))))));
92
99
  }
93
100
  static get is() { return "ogm-viewer"; }
94
101
  static get encapsulation() { return "shadow"; }
@@ -142,14 +149,36 @@ export class OgmViewer {
142
149
  "reflect": false,
143
150
  "attribute": "theme",
144
151
  "defaultValue": "this.getThemePreference()"
152
+ },
153
+ "hideTitle": {
154
+ "type": "boolean",
155
+ "mutable": false,
156
+ "complexType": {
157
+ "original": "boolean",
158
+ "resolved": "boolean",
159
+ "references": {}
160
+ },
161
+ "required": false,
162
+ "optional": false,
163
+ "docs": {
164
+ "tags": [],
165
+ "text": ""
166
+ },
167
+ "getter": false,
168
+ "setter": false,
169
+ "reflect": false,
170
+ "attribute": "hide-title",
171
+ "defaultValue": "false"
145
172
  }
146
173
  };
147
174
  }
148
175
  static get states() {
149
176
  return {
150
177
  "record": {},
178
+ "error": {},
151
179
  "previewOpacity": {},
152
- "sidebarOpen": {}
180
+ "sidebarOpen": {},
181
+ "loading": {}
153
182
  };
154
183
  }
155
184
  static get methods() {
@@ -186,6 +215,9 @@ export class OgmViewer {
186
215
  return [{
187
216
  "propName": "recordUrl",
188
217
  "methodName": "updateRecord"
218
+ }, {
219
+ "propName": "record",
220
+ "methodName": "resetLoading"
189
221
  }];
190
222
  }
191
223
  static get listeners() {