@penn-libraries/web 1.1.0 → 1.1.1-dev.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 (61) hide show
  1. package/dist/cjs/{index-4ixBJUwu.js → index-C0qvW4Ra.js} +7 -2
  2. package/dist/cjs/index-C0qvW4Ra.js.map +1 -0
  3. package/dist/cjs/index.cjs.js +1 -1
  4. package/dist/cjs/loader.cjs.js +2 -2
  5. package/dist/cjs/pennlibs-autocomplete_3.cjs.entry.js +1 -1
  6. package/dist/cjs/pennlibs-banner.cjs.entry.js +1 -1
  7. package/dist/cjs/pennlibs-chat.cjs.entry.js +1 -1
  8. package/dist/cjs/pennlibs-fallback-img.cjs.entry.js +1 -1
  9. package/dist/cjs/pennlibs-feedback.cjs.entry.js +1 -1
  10. package/dist/cjs/pennlibs-hero.cjs.entry.js +1 -1
  11. package/dist/cjs/pennlibs-iiif-img.cjs.entry.js +132 -0
  12. package/dist/cjs/pennlibs-iiif-img.entry.cjs.js.map +1 -0
  13. package/dist/cjs/web.cjs.js +2 -2
  14. package/dist/collection/collection-manifest.json +2 -1
  15. package/dist/collection/components/pennlibs-iiif-img/pennlibs-iiif-img.css +37 -0
  16. package/dist/collection/components/pennlibs-iiif-img/pennlibs-iiif-img.js +310 -0
  17. package/dist/collection/components/pennlibs-iiif-img/pennlibs-iiif-img.js.map +1 -0
  18. package/dist/components/index.d.ts +2 -0
  19. package/dist/components/index.js +1 -0
  20. package/dist/components/index.js.map +1 -1
  21. package/dist/components/pennlibs-fallback-img.js +1 -32
  22. package/dist/components/pennlibs-fallback-img.js.map +1 -1
  23. package/dist/components/pennlibs-fallback-img2.js +37 -0
  24. package/dist/components/pennlibs-fallback-img2.js.map +1 -0
  25. package/dist/components/pennlibs-iiif-img.d.ts +11 -0
  26. package/dist/components/pennlibs-iiif-img.js +168 -0
  27. package/dist/components/pennlibs-iiif-img.js.map +1 -0
  28. package/dist/docs.json +239 -3
  29. package/dist/{web/p-Di48-Vtw.js → esm/index-D9dYrmUF.js} +7 -2
  30. package/dist/esm/index-D9dYrmUF.js.map +1 -0
  31. package/dist/esm/index.js +1 -1
  32. package/dist/esm/loader.js +3 -3
  33. package/dist/esm/pennlibs-autocomplete_3.entry.js +1 -1
  34. package/dist/esm/pennlibs-banner.entry.js +1 -1
  35. package/dist/esm/pennlibs-chat.entry.js +1 -1
  36. package/dist/esm/pennlibs-fallback-img.entry.js +1 -1
  37. package/dist/esm/pennlibs-feedback.entry.js +1 -1
  38. package/dist/esm/pennlibs-hero.entry.js +1 -1
  39. package/dist/esm/pennlibs-iiif-img.entry.js +130 -0
  40. package/dist/esm/pennlibs-iiif-img.entry.js.map +1 -0
  41. package/dist/esm/web.js +3 -3
  42. package/dist/types/components/pennlibs-iiif-img/pennlibs-iiif-img.d.ts +89 -0
  43. package/dist/types/components.d.ts +119 -0
  44. package/dist/web/index.esm.js +1 -1
  45. package/dist/web/{p-7cbd16c0.entry.js → p-43d9c2d4.entry.js} +1 -1
  46. package/dist/{esm/index-Di48-Vtw.js → web/p-D9dYrmUF.js} +7 -2
  47. package/dist/web/p-D9dYrmUF.js.map +1 -0
  48. package/dist/web/{p-783e2a87.entry.js → p-ad92090a.entry.js} +1 -1
  49. package/dist/web/{p-56b9f10c.entry.js → p-b4b58af0.entry.js} +1 -1
  50. package/dist/web/p-c4074cf1.entry.js +130 -0
  51. package/dist/web/{p-6503bb09.entry.js → p-cb2584da.entry.js} +1 -1
  52. package/dist/web/{p-09e4a02c.entry.js → p-ce97059c.entry.js} +1 -1
  53. package/dist/web/{p-269363bf.entry.js → p-e6188c30.entry.js} +1 -1
  54. package/dist/web/pennlibs-iiif-img.entry.esm.js.map +1 -0
  55. package/dist/web/web.esm.js +3 -3
  56. package/hydrate/index.js +162 -0
  57. package/hydrate/index.mjs +162 -0
  58. package/package.json +1 -1
  59. package/dist/cjs/index-4ixBJUwu.js.map +0 -1
  60. package/dist/esm/index-Di48-Vtw.js.map +0 -1
  61. package/dist/web/p-Di48-Vtw.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { r as registerInstance, h } from './p-Di48-Vtw.js';
1
+ import { r as registerInstance, h } from './p-D9dYrmUF.js';
2
2
 
3
3
  const pennlibsChatCss = ":host {\n font-family: var(--pl-font-family);\n font-size: var(--pl-font-size);\n line-height: normal;\n}\n\n@media print {\n :host {\n display: none;\n }\n}\n\na {\n display: flex;\n align-items: center;\n background: linear-gradient(45deg, #faa755, #fcca99);\n border-radius: 1em;\n position: fixed;\n bottom: 0.75em;\n right: 0.75em;\n box-shadow: rgba(0,0,0,0.3) 0px 2px 16px 0px;\n color: var(--pl-color-penn-blue);\n text-decoration: none;\n font-weight: 500;\n padding: 0.15em;\n font-size: 1em;\n\n @media (min-width: 820px) {\n bottom: 1.5em;\n right: 1.5em;\n width: auto;\n padding: 0.15em 0.75em;\n padding-left: 0.5em;\n }\n}\n\na:hover {\n text-decoration: underline;\n text-underline-offset: var(--pl-link-text-underline-offset);\n text-decoration-thickness: var(--pl-link-hover-text-decoration-thickness);\n}\n\na:hover,\na:focus {\n outline: none;\n box-shadow: 0 0 0 2px var(--pl-color-bg-attention),0 0 0 4px var(--pl-color-penn-blue), rgba(0,0,0,0.3) 0px 2px 16px 0px;;\n}\n\nsvg {\n width: 2.5em;\n height: 2.5em;\n}\n\nspan {\n display: none;\n font-size: 1.1em;\n\n @media (min-width: 820px) {\n display: inline;\n }\n}";
4
4
 
@@ -1,4 +1,4 @@
1
- import { r as registerInstance, c as createEvent, h } from './p-Di48-Vtw.js';
1
+ import { r as registerInstance, c as createEvent, h } from './p-D9dYrmUF.js';
2
2
 
3
3
  const pennlibsFeedbackCss = ":host{display:block;background:#eeeff4;padding:1.5em 1em;font-family:var(--pl-font-family);font-size:var(--pl-font-size);color:var(--pl-color-fg-default);line-height:1.4;box-sizing:border-box}@media print{:host{display:none}}.feedback-container{display:flex;align-items:center;flex-wrap:wrap;gap:0.5em;width:100%;justify-content:center}@media (max-width: 26em){.feedback-container{justify-content:center}}h2{display:inline-block;margin:0 0.5em 0 0;font-family:var(--pl-font-sans-serif);font-size:1em;font-weight:600}p{margin:0;padding:calc(0.5em + 1px) 0}strong{font-weight:600}a{color:var(--pl-color-fg-accent);text-decoration:underline;text-underline-offset:var(--pl-link-text-underline-offset);text-decoration-thickness:var(--pl-link-text-decoration-thickness)}button{all:unset;display:flex;align-items:center;gap:0.5em;padding:0.5em 1.5em;font-family:var(--pl-font-family);font-weight:500;line-height:1.4;color:var(--pl-color-fg-default);background:var(--pl-color-bg-default);border:solid 1px var(--pl-color-fg-subtle);border-radius:1em;box-sizing:border-box}button:hover{cursor:pointer}button:hover span{text-decoration:underline}*:focus{outline:0;box-shadow:0 0 0 2px var(--pl-color-bg-attention),\n 0 0 0 3px var(--pl-color-bg-emphasis)}p:focus{outline:none;box-shadow:0 0 0 2px var(--pl-color-bg-attention), 0 0 0 3px var(--pl-color-bg-emphasis)}@media (max-width: 26em){h2{display:block;width:100%;text-align:center}}.help-us{display:flex;flex-direction:column;gap:0.5em}.visually-hidden{clip:rect(0 0 0 0);clip-path:inset(50%);height:1px;overflow:hidden;position:absolute;white-space:nowrap;width:1px}.visually-hidden:focus,.visually-hidden:active{clip:auto;clip-path:none;height:auto;overflow:visible;position:static;white-space:normal;width:auto}";
4
4
 
@@ -0,0 +1,130 @@
1
+ import { r as registerInstance, h, d as getElement } from './p-D9dYrmUF.js';
2
+
3
+ const pennlibsIiifImgCss = ":host{display:block;width:100%;max-width:100%}picture{display:block;line-height:0}.iiif-img{display:block;width:100%;max-width:100%;height:auto;opacity:0;filter:blur(5px);transition:opacity 0.15s ease-in, filter 0.15s ease-in}.iiif-img.loaded{opacity:1;filter:blur(0)}.fallback-container{display:block;width:100%;height:auto;min-height:200px}.fallback-container pennlibs-fallback-img{width:100%;height:100%}";
4
+
5
+ const PIXEL_DENSITY_MULTIPLIERS = [0.5, 0.75, 1, 1.5, 2];
6
+ const DEFAULT_IMAGE_SIZES = [400, 600, 800, 1200, 1600, 2400];
7
+ const IIIFImg = class {
8
+ constructor(hostRef) {
9
+ registerInstance(this, hostRef);
10
+ /**
11
+ * The IIIF [region](https://iiif.io/api/image/3.0/#41-region) of the image to display.
12
+ * Defines the rectangular portion of the underlying image to return.
13
+ *
14
+ * `full`: The full image is returned, without any cropping.
15
+ *
16
+ * `square`: A square area where width and height equal the shorter dimension.
17
+ *
18
+ * `x,y,w,h`: Absolute pixel coordinates (x, y position; w, h dimensions).
19
+ *
20
+ * `pct:x,y,w,h`: Percentage-based coordinates of full image dimensions.
21
+ *
22
+ * @default 'full'
23
+ */
24
+ this.region = 'full';
25
+ /**
26
+ * The IIIF [rotation](https://iiif.io/api/image/3.0/#44-rotation) to apply to the image.
27
+ * Specifies mirroring and clockwise rotation in degrees (0-360).
28
+ *
29
+ * `n`: Rotation in degrees only.
30
+ *
31
+ * `!n`: Mirror the image vertically, then rotate by n degrees.
32
+ *
33
+ * @default '0'
34
+ */
35
+ this.rotation = '0';
36
+ /**
37
+ * The IIIF [quality](https://iiif.io/api/image/3.0/#quality) of the image.
38
+ * Controls the color delivery mode.
39
+ *
40
+ * `default`: The server's default quality.
41
+ *
42
+ * `color`: Full color information.
43
+ *
44
+ * `gray`: Grayscale rendering.
45
+ *
46
+ * `bitonal`: Black and white only.
47
+ *
48
+ * @default 'default'
49
+ */
50
+ this.quality = 'default';
51
+ /**
52
+ * Native browser lazy loading behavior. Use "lazy" to defer loading until the image is near the viewport,
53
+ * or "eager" to load immediately.
54
+ * @default 'lazy'
55
+ */
56
+ this.loading = 'lazy';
57
+ /**
58
+ * Whether to display a fallback placeholder image when the IIIF image fails to load.
59
+ * @default true
60
+ */
61
+ this.showFallback = true;
62
+ this.isLoaded = false;
63
+ this.hasError = false;
64
+ this.baseUrl = 'https://iiif-images.library.upenn.edu/iiif/3';
65
+ this.handleLoad = () => {
66
+ this.isLoaded = true;
67
+ };
68
+ this.handleError = () => {
69
+ this.hasError = true;
70
+ };
71
+ }
72
+ buildIIIFUrl(width, format = 'jpg') {
73
+ const sizeParam = width ? `${width},` : 'max';
74
+ return `${this.baseUrl}/${this.uuid}/${this.region}/${sizeParam}/${this.rotation}/${this.quality}.${format}`;
75
+ }
76
+ generateSrcset(sizes, format) {
77
+ const uniqueSizes = [...new Set(sizes)].sort((a, b) => a - b);
78
+ const srcsetEntries = uniqueSizes.map(size => {
79
+ const url = this.buildIIIFUrl(size, format);
80
+ return `${url} ${size}w`;
81
+ });
82
+ return srcsetEntries.join(', ');
83
+ }
84
+ getOptimalSizes() {
85
+ if (this.observedWidth) {
86
+ const baseSizes = PIXEL_DENSITY_MULTIPLIERS.map(multiplier => Math.round(this.observedWidth * multiplier));
87
+ return baseSizes;
88
+ }
89
+ return [...DEFAULT_IMAGE_SIZES];
90
+ }
91
+ componentDidLoad() {
92
+ if ('ResizeObserver' in window) {
93
+ this.resizeObserver = new ResizeObserver(entries => {
94
+ for (const entry of entries) {
95
+ this.observedWidth = Math.round(entry.contentRect.width);
96
+ if (this.resizeObserver) {
97
+ this.resizeObserver.disconnect();
98
+ this.resizeObserver = undefined;
99
+ }
100
+ // Trigger re-render with optimized sizes
101
+ this.isLoaded = this.isLoaded;
102
+ }
103
+ });
104
+ this.resizeObserver.observe(this.hostElement);
105
+ }
106
+ }
107
+ disconnectedCallback() {
108
+ if (this.resizeObserver) {
109
+ this.resizeObserver.disconnect();
110
+ this.resizeObserver = undefined;
111
+ }
112
+ }
113
+ render() {
114
+ const sizes = this.getOptimalSizes();
115
+ if (this.hasError && this.showFallback) {
116
+ return (h("div", { class: "fallback-container" }, h("pennlibs-fallback-img", null)));
117
+ }
118
+ const imgClasses = {
119
+ 'iiif-img': true,
120
+ 'loaded': this.isLoaded
121
+ };
122
+ const defaultSize = sizes[0];
123
+ return (h("picture", null, h("source", { type: "image/webp", srcSet: this.generateSrcset(sizes, 'webp'), sizes: "100vw" }), h("img", { class: imgClasses, src: this.buildIIIFUrl(defaultSize, 'jpg'), srcSet: this.generateSrcset(sizes, 'jpg'), sizes: "100vw", alt: this.alt, loading: this.loading, onLoad: this.handleLoad, onError: this.handleError })));
124
+ }
125
+ get hostElement() { return getElement(this); }
126
+ };
127
+ IIIFImg.style = pennlibsIiifImgCss;
128
+
129
+ export { IIIFImg as pennlibs_iiif_img };
130
+ //# sourceMappingURL=pennlibs-iiif-img.entry.esm.js.map
@@ -1,4 +1,4 @@
1
- import { r as registerInstance, h, d as getElement } from './p-Di48-Vtw.js';
1
+ import { r as registerInstance, h, d as getElement } from './p-D9dYrmUF.js';
2
2
 
3
3
  const pennlibsHeroCss = ":host{--pl-hero-height:clamp(42vh, 32rem, 26rem);--pl-hero-heading-font:var(--pl-font-serif);--pl-hero-color:var(--pl-color-fg-on-emphasis)}*,*:before,*:after{box-sizing:inherit}.visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border:0}.viewport-margins{width:100%;max-width:var(--pl-viewport-margins-max-width);margin:0 auto;padding:0 var(--pl-viewport-margins-gutter, 1em)}.hero{position:relative;min-height:var(--pl-hero-height);height:100%;background-size:cover;background-repeat:no-repeat;background-position:50% 33%;display:flex}.hero::before{content:\"\";display:flex;width:100%;height:100%;top:0;position:absolute;background:linear-gradient(360deg, rgba(0, 0, 0, 0.9) 0%, rgba(0, 0, 0, 0.7) 20%, rgba(0, 0, 0, 0.4) 40%, rgba(0, 0, 0, 0.05) 100%);z-index:0}.hero::after{content:\"\";display:flex;width:100%;height:100%;top:0;position:absolute;background:linear-gradient(180deg, rgba(1, 31, 91, 1) 0%, rgba(1, 31, 91, 0.9) 10%, rgba(1, 31, 91, 0.8) 20%, rgba(1, 31, 91, 0.1) 50%, rgba(1, 31, 91, 0.0) 100%);z-index:0}.hero__content{position:relative;display:flex;flex-direction:column;width:100%;z-index:1}.hero__heading-container{margin-top:auto;padding-top:var(--pl-space-3xl);padding-bottom:var(--pl-space-3xl)}.hero__heading{text-shadow:1px 1px 2px var(--pl-color-fg-default);line-height:1.1;font-size:3em;font-weight:bold;font-family:var(--pl-hero-heading-font);text-wrap:pretty;max-width:30ch;margin:0;color:var(--pl-hero-color)}@media (max-width: 920px){.hero__heading{font-size:2.5em}}.hero__sub-heading{font-size:1.25em;font-family:var(--pl-font-family);font-weight:500;color:var(--pl-hero-color);max-width:52ch;text-wrap:pretty;margin-top:1em;margin-bottom:0}.hero__sub-heading a{text-decoration:underline;text-underline-offset:var(--pl-link-text-underline-offset);text-decoration-thickness:var(--pl-link-text-decoration-thickness);color:var(--pl-hero-color)}.hero__sub-heading a:hover{text-decoration-thickness:var(--pl-link-hover-text-decoration-thickness)}.hero__sub-heading strong{font-weight:bold}@media (max-width: 620px){.hero__heading{font-size:2.75em}.hero__sub-heading{font-size:1em}}";
4
4
 
@@ -1,4 +1,4 @@
1
- import { r as registerInstance, a as getAssetPath, h } from './p-Di48-Vtw.js';
1
+ import { r as registerInstance, a as getAssetPath, h } from './p-D9dYrmUF.js';
2
2
 
3
3
  const pennlibsFallbackImgCss = ":host{font-family:var(--pl-font-family);font-size:var(--pl-font-size);display:flex;align-items:center;justify-content:center;padding:1rem;background:var(--pl-color-bg-subtle);aspect-ratio:3/2}.no-image__img{width:100%;max-width:150px;filter:grayscale(1) opacity(0.3)}";
4
4
 
@@ -1,4 +1,4 @@
1
- import { r as registerInstance, c as createEvent, h, d as getElement, a as getAssetPath } from './p-Di48-Vtw.js';
1
+ import { r as registerInstance, c as createEvent, h, d as getElement, a as getAssetPath } from './p-D9dYrmUF.js';
2
2
 
3
3
  const pennlibsAutocompleteCss = ":host {\n display: block;\n width: 100%;\n border-radius: 1.25rem;\n padding: 0;\n border-top: 0;\n position: relative;\n}\n\n[role=listbox] {\n position: absolute;\n margin-top: var(--pl-space-xs);\n background: var(--pl-color-bg-default);\n border-radius: 1.25rem;\n box-shadow: rgba(140, 149, 159, 0.3) 0px 8px 24px 0px;\n width: 100%;\n overflow: hidden;\n z-index: 1;\n\n display: flex;\n flex-direction: column;\n}\n\np {\n margin: 0;\n font-size: var(--pl-font-size-s);\n color: var(--pl-color-fg-subtle);\n padding: var(--pl-space-xs) calc(var(--pl-space-m) + var(--pl-space-2xs));\n font-size: var(--pl-font-size-s);\n order: 2;\n font-weight: 500;\n background: var(--pl-color-bg-subtle);\n border-radius: 0 0 1.25rem 1.25rem;\n\n display: flex;\n gap: var(--pl-space-s) var(--pl-space-l);\n flex-wrap: wrap;\n}\n\nol {\n list-style: none;\n margin: 0;\n padding: var(--pl-space-xs) 0;\n order: 1;\n}\n\n[role=option] {\n color: var(--pl-color-fg-default);\n padding: var(--pl-space-s) calc(var(--pl-space-m) + var(--pl-space-2xs));\n text-decoration: none;\n font-weight: 700; \n\n &:hover {\n cursor: pointer;\n }\n\n &:hover,\n &:focus {\n text-decoration-thickness: 2px;\n text-underline-offset: 2px;\n text-decoration: underline;\n }\n}\n\n[aria-selected=true] {\n text-decoration-thickness: 2px;\n text-underline-offset: 2px;\n text-decoration: underline;\n}\n\nmark,\nb {\n background: none;\n font-weight: 400;\n}\n\n.suggestion--border {\n border-bottom: solid 1px rgb(from var(--pl-color-fg-default) r g b / 0.2);\n padding-bottom: calc(var(--pl-space-2xs) + var(--pl-space-s));\n margin-bottom: var(--pl-space-2xs);\n}";
4
4
 
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pennlibs-iiif-img.entry.esm.js","sources":["src/components/pennlibs-iiif-img/pennlibs-iiif-img.css?tag=pennlibs-iiif-img&encapsulation=shadow","src/components/pennlibs-iiif-img/pennlibs-iiif-img.tsx"],"sourcesContent":[":host {\n display: block;\n width: 100%;\n max-width: 100%;\n}\n\npicture {\n display: block;\n line-height: 0;\n}\n\n.iiif-img {\n display: block;\n width: 100%;\n max-width: 100%;\n height: auto;\n opacity: 0;\n filter: blur(5px);\n transition: opacity 0.15s ease-in, filter 0.15s ease-in;\n}\n\n.iiif-img.loaded {\n opacity: 1;\n filter: blur(0);\n}\n\n.fallback-container {\n display: block;\n width: 100%;\n height: auto;\n min-height: 200px;\n}\n\n.fallback-container pennlibs-fallback-img {\n width: 100%;\n height: 100%;\n}\n","import { Component, h, Prop, State, Element } from '@stencil/core';\n\ntype ImageFormat = 'jpg' | 'webp';\n\nconst PIXEL_DENSITY_MULTIPLIERS = [0.5, 0.75, 1, 1.5, 2] as const;\nconst DEFAULT_IMAGE_SIZES = [400, 600, 800, 1200, 1600, 2400] as const;\n\n/**\n * Display responsive, high-quality images from Penn Libraries' IIIF Image API 3.0 server.\n * Automatically generates optimal image sources for different viewport sizes and pixel densities,\n * with modern WebP format and JPG fallback support.\n *\n * @component\n * @example\n * <pennlibs-iiif-img\n * uuid=\"063ff35c-bbdd-4b63-bbdd-6206590e20d5\"\n * alt=\"\">\n * </pennlibs-iiif-img>\n */\n@Component({\n tag: 'pennlibs-iiif-img',\n styleUrl: 'pennlibs-iiif-img.css',\n shadow: true,\n})\nexport class IIIFImg {\n private resizeObserver?: ResizeObserver;\n\n @Element() hostElement!: HTMLElement;\n\n /**\n * The IIIF [UUID identifier](https://iiif.io/api/image/3.0/#3-identifier) of the image resource on the Penn Libraries IIIF server.\n */\n @Prop() uuid!: string;\n\n /**\n * Alternative text that describes the image content for screen readers and when images fail to load.\n * Use an empty string for purely decorative images.\n */\n @Prop() alt!: string;\n\n /**\n * The IIIF [region](https://iiif.io/api/image/3.0/#41-region) of the image to display.\n * Defines the rectangular portion of the underlying image to return.\n * \n * `full`: The full image is returned, without any cropping.\n * \n * `square`: A square area where width and height equal the shorter dimension.\n * \n * `x,y,w,h`: Absolute pixel coordinates (x, y position; w, h dimensions).\n * \n * `pct:x,y,w,h`: Percentage-based coordinates of full image dimensions.\n * \n * @default 'full'\n */\n @Prop() region?: string = 'full';\n\n /**\n * The IIIF [rotation](https://iiif.io/api/image/3.0/#44-rotation) to apply to the image.\n * Specifies mirroring and clockwise rotation in degrees (0-360).\n * \n * `n`: Rotation in degrees only.\n * \n * `!n`: Mirror the image vertically, then rotate by n degrees.\n * \n * @default '0'\n */\n @Prop() rotation?: string = '0';\n\n /**\n * The IIIF [quality](https://iiif.io/api/image/3.0/#quality) of the image.\n * Controls the color delivery mode.\n * \n * `default`: The server's default quality.\n * \n * `color`: Full color information.\n * \n * `gray`: Grayscale rendering.\n * \n * `bitonal`: Black and white only.\n * \n * @default 'default'\n */\n @Prop() quality?: string = 'default';\n\n /**\n * Native browser lazy loading behavior. Use \"lazy\" to defer loading until the image is near the viewport,\n * or \"eager\" to load immediately.\n * @default 'lazy'\n */\n @Prop() loading?: 'lazy' | 'eager' = 'lazy';\n\n /**\n * Whether to display a fallback placeholder image when the IIIF image fails to load.\n * @default true\n */\n @Prop() showFallback?: boolean = true;\n\n @State() isLoaded: boolean = false;\n @State() hasError: boolean = false;\n\n private observedWidth?: number;\n\n private readonly baseUrl = 'https://iiif-images.library.upenn.edu/iiif/3';\n\n private buildIIIFUrl(width?: number, format: ImageFormat = 'jpg'): string {\n const sizeParam = width ? `${width},` : 'max';\n return `${this.baseUrl}/${this.uuid}/${this.region}/${sizeParam}/${this.rotation}/${this.quality}.${format}`;\n }\n\n private generateSrcset(sizes: number[], format: ImageFormat): string {\n const uniqueSizes = [...new Set(sizes)].sort((a, b) => a - b);\n\n const srcsetEntries = uniqueSizes.map(size => {\n const url = this.buildIIIFUrl(size, format);\n return `${url} ${size}w`;\n });\n\n return srcsetEntries.join(', ');\n }\n\n private getOptimalSizes(): number[] {\n if (this.observedWidth) {\n const baseSizes = PIXEL_DENSITY_MULTIPLIERS.map(multiplier =>\n Math.round(this.observedWidth! * multiplier)\n );\n\n return baseSizes;\n }\n\n return [...DEFAULT_IMAGE_SIZES];\n }\n\n private handleLoad = () => {\n this.isLoaded = true;\n };\n\n private handleError = () => {\n this.hasError = true;\n };\n\n componentDidLoad() {\n if ('ResizeObserver' in window) {\n this.resizeObserver = new ResizeObserver(entries => {\n for (const entry of entries) {\n this.observedWidth = Math.round(entry.contentRect.width);\n\n if (this.resizeObserver) {\n this.resizeObserver.disconnect();\n this.resizeObserver = undefined;\n }\n\n // Trigger re-render with optimized sizes\n this.isLoaded = this.isLoaded;\n }\n });\n\n this.resizeObserver.observe(this.hostElement);\n }\n }\n\n disconnectedCallback() {\n if (this.resizeObserver) {\n this.resizeObserver.disconnect();\n this.resizeObserver = undefined;\n }\n }\n\n render() {\n const sizes = this.getOptimalSizes();\n\n if (this.hasError && this.showFallback) {\n return (\n <div class=\"fallback-container\">\n <pennlibs-fallback-img />\n </div>\n );\n }\n\n const imgClasses = {\n 'iiif-img': true,\n 'loaded': this.isLoaded\n };\n\n const defaultSize = sizes[0];\n\n return (\n <picture>\n <source\n type=\"image/webp\"\n srcSet={this.generateSrcset(sizes, 'webp')}\n sizes=\"100vw\"\n />\n\n <img\n class={imgClasses}\n src={this.buildIIIFUrl(defaultSize, 'jpg')}\n srcSet={this.generateSrcset(sizes, 'jpg')}\n sizes=\"100vw\"\n alt={this.alt}\n loading={this.loading}\n onLoad={this.handleLoad}\n onError={this.handleError}\n />\n </picture>\n );\n }\n}\n"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
- import { p as promiseResolve, g as globalScripts, b as bootstrapLazy } from './p-Di48-Vtw.js';
2
- export { s as setNonce } from './p-Di48-Vtw.js';
1
+ import { p as promiseResolve, g as globalScripts, b as bootstrapLazy } from './p-D9dYrmUF.js';
2
+ export { s as setNonce } from './p-D9dYrmUF.js';
3
3
 
4
4
  /*
5
5
  Stencil Client Patch Browser v4.38.1 | MIT Licensed | https://stenciljs.com
@@ -16,6 +16,6 @@ var patchBrowser = () => {
16
16
 
17
17
  patchBrowser().then(async (options) => {
18
18
  await globalScripts();
19
- return bootstrapLazy([["p-269363bf",[[257,"pennlibs-autocomplete",{"for":[1],"showSuggestions":[32],"currentIndex":[32],"originalValue":[32],"options":[32]},[[0,"slotchange","handleSlotChange"],[16,"input","handleInputEvent"],[16,"focus","handleFocusEvent"],[16,"blur","handleBlurEvent"],[0,"focusout","handleFocusOut"],[4,"keydown","handleKeyDown"]]],[257,"pennlibs-footer",{"navigationByChildren":[32]}],[257,"pennlibs-header",{"serviceName":[1,"service-name"],"serviceLede":[1,"service-lede"],"serviceHref":[1,"service-href"],"theme":[1],"isMenuOpen":[32],"navigation":[32]}]]],["p-7cbd16c0",[[257,"pennlibs-banner"]]],["p-783e2a87",[[257,"pennlibs-chat",{"href":[32]}]]],["p-09e4a02c",[[257,"pennlibs-fallback-img"]]],["p-56b9f10c",[[257,"pennlibs-feedback",{"error":[32],"answer":[32]}]]],["p-6503bb09",[[257,"pennlibs-hero",{"heroPictureElement":[32],"heroHeadingElement":[32],"heroParagraphElement":[32],"heroSrc":[32]}]]]], options);
19
+ return bootstrapLazy([["p-c4074cf1",[[257,"pennlibs-iiif-img",{"uuid":[1],"alt":[1],"region":[1],"rotation":[1],"quality":[1],"loading":[1],"showFallback":[4,"show-fallback"],"isLoaded":[32],"hasError":[32]}]]],["p-e6188c30",[[257,"pennlibs-autocomplete",{"for":[1],"showSuggestions":[32],"currentIndex":[32],"originalValue":[32],"options":[32]},[[0,"slotchange","handleSlotChange"],[16,"input","handleInputEvent"],[16,"focus","handleFocusEvent"],[16,"blur","handleBlurEvent"],[0,"focusout","handleFocusOut"],[4,"keydown","handleKeyDown"]]],[257,"pennlibs-footer",{"navigationByChildren":[32]}],[257,"pennlibs-header",{"serviceName":[1,"service-name"],"serviceLede":[1,"service-lede"],"serviceHref":[1,"service-href"],"theme":[1],"isMenuOpen":[32],"navigation":[32]}]]],["p-43d9c2d4",[[257,"pennlibs-banner"]]],["p-ad92090a",[[257,"pennlibs-chat",{"href":[32]}]]],["p-b4b58af0",[[257,"pennlibs-feedback",{"error":[32],"answer":[32]}]]],["p-cb2584da",[[257,"pennlibs-hero",{"heroPictureElement":[32],"heroHeadingElement":[32],"heroParagraphElement":[32],"heroSrc":[32]}]]],["p-ce97059c",[[257,"pennlibs-fallback-img"]]]], options);
20
20
  });
21
21
  //# sourceMappingURL=web.esm.js.map
package/hydrate/index.js CHANGED
@@ -1530,6 +1530,11 @@ var parsePropertyValue = (propValue, propType, isFormAssociated) => {
1530
1530
  return propValue;
1531
1531
  }
1532
1532
  if (propValue != null && !isComplexType(propValue)) {
1533
+ if (propType & 4 /* Boolean */) {
1534
+ {
1535
+ return propValue === "false" ? false : propValue === "" || !!propValue;
1536
+ }
1537
+ }
1533
1538
  if (propType & 1 /* String */) {
1534
1539
  return String(propValue);
1535
1540
  }
@@ -3870,6 +3875,162 @@ class Hero {
3870
3875
  }; }
3871
3876
  }
3872
3877
 
3878
+ const pennlibsIiifImgCss = ":host{display:block;width:100%;max-width:100%}picture{display:block;line-height:0}.iiif-img{display:block;width:100%;max-width:100%;height:auto;opacity:0;filter:blur(5px);transition:opacity 0.15s ease-in, filter 0.15s ease-in}.iiif-img.loaded{opacity:1;filter:blur(0)}.fallback-container{display:block;width:100%;height:auto;min-height:200px}.fallback-container pennlibs-fallback-img{width:100%;height:100%}";
3879
+
3880
+ const PIXEL_DENSITY_MULTIPLIERS = [0.5, 0.75, 1, 1.5, 2];
3881
+ const DEFAULT_IMAGE_SIZES = [400, 600, 800, 1200, 1600, 2400];
3882
+ /**
3883
+ * Display responsive, high-quality images from Penn Libraries' IIIF Image API 3.0 server.
3884
+ * Automatically generates optimal image sources for different viewport sizes and pixel densities,
3885
+ * with modern WebP format and JPG fallback support.
3886
+ *
3887
+ * @component
3888
+ * @example
3889
+ * <pennlibs-iiif-img
3890
+ * uuid="063ff35c-bbdd-4b63-bbdd-6206590e20d5"
3891
+ * alt="">
3892
+ * </pennlibs-iiif-img>
3893
+ */
3894
+ class IIIFImg {
3895
+ constructor(hostRef) {
3896
+ registerInstance(this, hostRef);
3897
+ /**
3898
+ * The IIIF [region](https://iiif.io/api/image/3.0/#41-region) of the image to display.
3899
+ * Defines the rectangular portion of the underlying image to return.
3900
+ *
3901
+ * `full`: The full image is returned, without any cropping.
3902
+ *
3903
+ * `square`: A square area where width and height equal the shorter dimension.
3904
+ *
3905
+ * `x,y,w,h`: Absolute pixel coordinates (x, y position; w, h dimensions).
3906
+ *
3907
+ * `pct:x,y,w,h`: Percentage-based coordinates of full image dimensions.
3908
+ *
3909
+ * @default 'full'
3910
+ */
3911
+ this.region = 'full';
3912
+ /**
3913
+ * The IIIF [rotation](https://iiif.io/api/image/3.0/#44-rotation) to apply to the image.
3914
+ * Specifies mirroring and clockwise rotation in degrees (0-360).
3915
+ *
3916
+ * `n`: Rotation in degrees only.
3917
+ *
3918
+ * `!n`: Mirror the image vertically, then rotate by n degrees.
3919
+ *
3920
+ * @default '0'
3921
+ */
3922
+ this.rotation = '0';
3923
+ /**
3924
+ * The IIIF [quality](https://iiif.io/api/image/3.0/#quality) of the image.
3925
+ * Controls the color delivery mode.
3926
+ *
3927
+ * `default`: The server's default quality.
3928
+ *
3929
+ * `color`: Full color information.
3930
+ *
3931
+ * `gray`: Grayscale rendering.
3932
+ *
3933
+ * `bitonal`: Black and white only.
3934
+ *
3935
+ * @default 'default'
3936
+ */
3937
+ this.quality = 'default';
3938
+ /**
3939
+ * Native browser lazy loading behavior. Use "lazy" to defer loading until the image is near the viewport,
3940
+ * or "eager" to load immediately.
3941
+ * @default 'lazy'
3942
+ */
3943
+ this.loading = 'lazy';
3944
+ /**
3945
+ * Whether to display a fallback placeholder image when the IIIF image fails to load.
3946
+ * @default true
3947
+ */
3948
+ this.showFallback = true;
3949
+ this.isLoaded = false;
3950
+ this.hasError = false;
3951
+ this.baseUrl = 'https://iiif-images.library.upenn.edu/iiif/3';
3952
+ this.handleLoad = () => {
3953
+ this.isLoaded = true;
3954
+ };
3955
+ this.handleError = () => {
3956
+ this.hasError = true;
3957
+ };
3958
+ }
3959
+ buildIIIFUrl(width, format = 'jpg') {
3960
+ const sizeParam = width ? `${width},` : 'max';
3961
+ return `${this.baseUrl}/${this.uuid}/${this.region}/${sizeParam}/${this.rotation}/${this.quality}.${format}`;
3962
+ }
3963
+ generateSrcset(sizes, format) {
3964
+ const uniqueSizes = [...new Set(sizes)].sort((a, b) => a - b);
3965
+ const srcsetEntries = uniqueSizes.map(size => {
3966
+ const url = this.buildIIIFUrl(size, format);
3967
+ return `${url} ${size}w`;
3968
+ });
3969
+ return srcsetEntries.join(', ');
3970
+ }
3971
+ getOptimalSizes() {
3972
+ if (this.observedWidth) {
3973
+ const baseSizes = PIXEL_DENSITY_MULTIPLIERS.map(multiplier => Math.round(this.observedWidth * multiplier));
3974
+ return baseSizes;
3975
+ }
3976
+ return [...DEFAULT_IMAGE_SIZES];
3977
+ }
3978
+ componentDidLoad() {
3979
+ if ('ResizeObserver' in window) {
3980
+ this.resizeObserver = new ResizeObserver(entries => {
3981
+ for (const entry of entries) {
3982
+ this.observedWidth = Math.round(entry.contentRect.width);
3983
+ if (this.resizeObserver) {
3984
+ this.resizeObserver.disconnect();
3985
+ this.resizeObserver = undefined;
3986
+ }
3987
+ // Trigger re-render with optimized sizes
3988
+ this.isLoaded = this.isLoaded;
3989
+ }
3990
+ });
3991
+ this.resizeObserver.observe(this.hostElement);
3992
+ }
3993
+ }
3994
+ disconnectedCallback() {
3995
+ if (this.resizeObserver) {
3996
+ this.resizeObserver.disconnect();
3997
+ this.resizeObserver = undefined;
3998
+ }
3999
+ }
4000
+ render() {
4001
+ const sizes = this.getOptimalSizes();
4002
+ if (this.hasError && this.showFallback) {
4003
+ return (hAsync("div", { class: "fallback-container" }, hAsync("pennlibs-fallback-img", null)));
4004
+ }
4005
+ const imgClasses = {
4006
+ 'iiif-img': true,
4007
+ 'loaded': this.isLoaded
4008
+ };
4009
+ const defaultSize = sizes[0];
4010
+ return (hAsync("picture", null, hAsync("source", { type: "image/webp", srcSet: this.generateSrcset(sizes, 'webp'), sizes: "100vw" }), hAsync("img", { class: imgClasses, src: this.buildIIIFUrl(defaultSize, 'jpg'), srcSet: this.generateSrcset(sizes, 'jpg'), sizes: "100vw", alt: this.alt, loading: this.loading, onLoad: this.handleLoad, onError: this.handleError })));
4011
+ }
4012
+ get hostElement() { return getElement(this); }
4013
+ static get style() { return pennlibsIiifImgCss; }
4014
+ static get cmpMeta() { return {
4015
+ "$flags$": 265,
4016
+ "$tagName$": "pennlibs-iiif-img",
4017
+ "$members$": {
4018
+ "uuid": [1],
4019
+ "alt": [1],
4020
+ "region": [1],
4021
+ "rotation": [1],
4022
+ "quality": [1],
4023
+ "loading": [1],
4024
+ "showFallback": [4, "show-fallback"],
4025
+ "isLoaded": [32],
4026
+ "hasError": [32]
4027
+ },
4028
+ "$listeners$": undefined,
4029
+ "$lazyBundleId$": "-",
4030
+ "$attrsToReflect$": []
4031
+ }; }
4032
+ }
4033
+
3873
4034
  const pennlibsFallbackImgCss = ":host{font-family:var(--pl-font-family);font-size:var(--pl-font-size);display:flex;align-items:center;justify-content:center;padding:1rem;background:var(--pl-color-bg-subtle);aspect-ratio:3/2}.no-image__img{width:100%;max-width:150px;filter:grayscale(1) opacity(0.3)}";
3874
4035
 
3875
4036
  class NoImage {
@@ -3962,6 +4123,7 @@ registerComponents([
3962
4123
  Footer,
3963
4124
  Header,
3964
4125
  Hero,
4126
+ IIIFImg,
3965
4127
  NoImage,
3966
4128
  PennlibsFeedback,
3967
4129
  ]);
package/hydrate/index.mjs CHANGED
@@ -1528,6 +1528,11 @@ var parsePropertyValue = (propValue, propType, isFormAssociated) => {
1528
1528
  return propValue;
1529
1529
  }
1530
1530
  if (propValue != null && !isComplexType(propValue)) {
1531
+ if (propType & 4 /* Boolean */) {
1532
+ {
1533
+ return propValue === "false" ? false : propValue === "" || !!propValue;
1534
+ }
1535
+ }
1531
1536
  if (propType & 1 /* String */) {
1532
1537
  return String(propValue);
1533
1538
  }
@@ -3868,6 +3873,162 @@ class Hero {
3868
3873
  }; }
3869
3874
  }
3870
3875
 
3876
+ const pennlibsIiifImgCss = ":host{display:block;width:100%;max-width:100%}picture{display:block;line-height:0}.iiif-img{display:block;width:100%;max-width:100%;height:auto;opacity:0;filter:blur(5px);transition:opacity 0.15s ease-in, filter 0.15s ease-in}.iiif-img.loaded{opacity:1;filter:blur(0)}.fallback-container{display:block;width:100%;height:auto;min-height:200px}.fallback-container pennlibs-fallback-img{width:100%;height:100%}";
3877
+
3878
+ const PIXEL_DENSITY_MULTIPLIERS = [0.5, 0.75, 1, 1.5, 2];
3879
+ const DEFAULT_IMAGE_SIZES = [400, 600, 800, 1200, 1600, 2400];
3880
+ /**
3881
+ * Display responsive, high-quality images from Penn Libraries' IIIF Image API 3.0 server.
3882
+ * Automatically generates optimal image sources for different viewport sizes and pixel densities,
3883
+ * with modern WebP format and JPG fallback support.
3884
+ *
3885
+ * @component
3886
+ * @example
3887
+ * <pennlibs-iiif-img
3888
+ * uuid="063ff35c-bbdd-4b63-bbdd-6206590e20d5"
3889
+ * alt="">
3890
+ * </pennlibs-iiif-img>
3891
+ */
3892
+ class IIIFImg {
3893
+ constructor(hostRef) {
3894
+ registerInstance(this, hostRef);
3895
+ /**
3896
+ * The IIIF [region](https://iiif.io/api/image/3.0/#41-region) of the image to display.
3897
+ * Defines the rectangular portion of the underlying image to return.
3898
+ *
3899
+ * `full`: The full image is returned, without any cropping.
3900
+ *
3901
+ * `square`: A square area where width and height equal the shorter dimension.
3902
+ *
3903
+ * `x,y,w,h`: Absolute pixel coordinates (x, y position; w, h dimensions).
3904
+ *
3905
+ * `pct:x,y,w,h`: Percentage-based coordinates of full image dimensions.
3906
+ *
3907
+ * @default 'full'
3908
+ */
3909
+ this.region = 'full';
3910
+ /**
3911
+ * The IIIF [rotation](https://iiif.io/api/image/3.0/#44-rotation) to apply to the image.
3912
+ * Specifies mirroring and clockwise rotation in degrees (0-360).
3913
+ *
3914
+ * `n`: Rotation in degrees only.
3915
+ *
3916
+ * `!n`: Mirror the image vertically, then rotate by n degrees.
3917
+ *
3918
+ * @default '0'
3919
+ */
3920
+ this.rotation = '0';
3921
+ /**
3922
+ * The IIIF [quality](https://iiif.io/api/image/3.0/#quality) of the image.
3923
+ * Controls the color delivery mode.
3924
+ *
3925
+ * `default`: The server's default quality.
3926
+ *
3927
+ * `color`: Full color information.
3928
+ *
3929
+ * `gray`: Grayscale rendering.
3930
+ *
3931
+ * `bitonal`: Black and white only.
3932
+ *
3933
+ * @default 'default'
3934
+ */
3935
+ this.quality = 'default';
3936
+ /**
3937
+ * Native browser lazy loading behavior. Use "lazy" to defer loading until the image is near the viewport,
3938
+ * or "eager" to load immediately.
3939
+ * @default 'lazy'
3940
+ */
3941
+ this.loading = 'lazy';
3942
+ /**
3943
+ * Whether to display a fallback placeholder image when the IIIF image fails to load.
3944
+ * @default true
3945
+ */
3946
+ this.showFallback = true;
3947
+ this.isLoaded = false;
3948
+ this.hasError = false;
3949
+ this.baseUrl = 'https://iiif-images.library.upenn.edu/iiif/3';
3950
+ this.handleLoad = () => {
3951
+ this.isLoaded = true;
3952
+ };
3953
+ this.handleError = () => {
3954
+ this.hasError = true;
3955
+ };
3956
+ }
3957
+ buildIIIFUrl(width, format = 'jpg') {
3958
+ const sizeParam = width ? `${width},` : 'max';
3959
+ return `${this.baseUrl}/${this.uuid}/${this.region}/${sizeParam}/${this.rotation}/${this.quality}.${format}`;
3960
+ }
3961
+ generateSrcset(sizes, format) {
3962
+ const uniqueSizes = [...new Set(sizes)].sort((a, b) => a - b);
3963
+ const srcsetEntries = uniqueSizes.map(size => {
3964
+ const url = this.buildIIIFUrl(size, format);
3965
+ return `${url} ${size}w`;
3966
+ });
3967
+ return srcsetEntries.join(', ');
3968
+ }
3969
+ getOptimalSizes() {
3970
+ if (this.observedWidth) {
3971
+ const baseSizes = PIXEL_DENSITY_MULTIPLIERS.map(multiplier => Math.round(this.observedWidth * multiplier));
3972
+ return baseSizes;
3973
+ }
3974
+ return [...DEFAULT_IMAGE_SIZES];
3975
+ }
3976
+ componentDidLoad() {
3977
+ if ('ResizeObserver' in window) {
3978
+ this.resizeObserver = new ResizeObserver(entries => {
3979
+ for (const entry of entries) {
3980
+ this.observedWidth = Math.round(entry.contentRect.width);
3981
+ if (this.resizeObserver) {
3982
+ this.resizeObserver.disconnect();
3983
+ this.resizeObserver = undefined;
3984
+ }
3985
+ // Trigger re-render with optimized sizes
3986
+ this.isLoaded = this.isLoaded;
3987
+ }
3988
+ });
3989
+ this.resizeObserver.observe(this.hostElement);
3990
+ }
3991
+ }
3992
+ disconnectedCallback() {
3993
+ if (this.resizeObserver) {
3994
+ this.resizeObserver.disconnect();
3995
+ this.resizeObserver = undefined;
3996
+ }
3997
+ }
3998
+ render() {
3999
+ const sizes = this.getOptimalSizes();
4000
+ if (this.hasError && this.showFallback) {
4001
+ return (hAsync("div", { class: "fallback-container" }, hAsync("pennlibs-fallback-img", null)));
4002
+ }
4003
+ const imgClasses = {
4004
+ 'iiif-img': true,
4005
+ 'loaded': this.isLoaded
4006
+ };
4007
+ const defaultSize = sizes[0];
4008
+ return (hAsync("picture", null, hAsync("source", { type: "image/webp", srcSet: this.generateSrcset(sizes, 'webp'), sizes: "100vw" }), hAsync("img", { class: imgClasses, src: this.buildIIIFUrl(defaultSize, 'jpg'), srcSet: this.generateSrcset(sizes, 'jpg'), sizes: "100vw", alt: this.alt, loading: this.loading, onLoad: this.handleLoad, onError: this.handleError })));
4009
+ }
4010
+ get hostElement() { return getElement(this); }
4011
+ static get style() { return pennlibsIiifImgCss; }
4012
+ static get cmpMeta() { return {
4013
+ "$flags$": 265,
4014
+ "$tagName$": "pennlibs-iiif-img",
4015
+ "$members$": {
4016
+ "uuid": [1],
4017
+ "alt": [1],
4018
+ "region": [1],
4019
+ "rotation": [1],
4020
+ "quality": [1],
4021
+ "loading": [1],
4022
+ "showFallback": [4, "show-fallback"],
4023
+ "isLoaded": [32],
4024
+ "hasError": [32]
4025
+ },
4026
+ "$listeners$": undefined,
4027
+ "$lazyBundleId$": "-",
4028
+ "$attrsToReflect$": []
4029
+ }; }
4030
+ }
4031
+
3871
4032
  const pennlibsFallbackImgCss = ":host{font-family:var(--pl-font-family);font-size:var(--pl-font-size);display:flex;align-items:center;justify-content:center;padding:1rem;background:var(--pl-color-bg-subtle);aspect-ratio:3/2}.no-image__img{width:100%;max-width:150px;filter:grayscale(1) opacity(0.3)}";
3872
4033
 
3873
4034
  class NoImage {
@@ -3960,6 +4121,7 @@ registerComponents([
3960
4121
  Footer,
3961
4122
  Header,
3962
4123
  Hero,
4124
+ IIIFImg,
3963
4125
  NoImage,
3964
4126
  PennlibsFeedback,
3965
4127
  ]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@penn-libraries/web",
3
- "version": "1.1.0",
3
+ "version": "1.1.1-dev.1",
4
4
  "description": "Reusable web designs for University of Pennsylvania Libraries websites. This package contains our code for shared Web Components and Cascading Style Sheets (CSS).",
5
5
  "main": "dist/index.cjs.js",
6
6
  "module": "dist/index.js",