@penn-libraries/web 1.2.0-dev.0 → 1.2.0-dev.2

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 (71) hide show
  1. package/dist/cjs/{index-DJo51Q03.js → index-Ag6DtE87.js} +3 -14
  2. package/dist/cjs/index-Ag6DtE87.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/{web/pennlibs-autocomplete.pennlibs-fallback-img.pennlibs-footer.pennlibs-header.pennlibs-iiif-img.entry.esm.js.map → cjs/pennlibs-autocomplete.pennlibs-feedback.pennlibs-footer.pennlibs-header.entry.cjs.js.map} +1 -1
  6. package/dist/cjs/{pennlibs-autocomplete_5.cjs.entry.js → pennlibs-autocomplete_4.cjs.entry.js} +54 -258
  7. package/dist/cjs/pennlibs-banner.cjs.entry.js +1 -1
  8. package/dist/cjs/pennlibs-chat.cjs.entry.js +1 -1
  9. package/dist/cjs/pennlibs-fallback-img.cjs.entry.js +20 -0
  10. package/dist/cjs/pennlibs-fallback-img.entry.cjs.js.map +1 -0
  11. package/dist/cjs/pennlibs-hero.cjs.entry.js +2 -2
  12. package/dist/cjs/pennlibs-iiif-img.cjs.entry.js +249 -0
  13. package/dist/cjs/pennlibs-iiif-img.entry.cjs.js.map +1 -0
  14. package/dist/cjs/web.cjs.js +2 -2
  15. package/dist/collection/components/pennlibs-feedback/pennlibs-feedback.css +24 -15
  16. package/dist/collection/components/pennlibs-feedback/pennlibs-feedback.js +50 -17
  17. package/dist/collection/components/pennlibs-feedback/pennlibs-feedback.js.map +1 -1
  18. package/dist/collection/components/pennlibs-footer/pennlibs-footer.css +23 -2
  19. package/dist/collection/components/pennlibs-footer/pennlibs-footer.js +33 -3
  20. package/dist/collection/components/pennlibs-footer/pennlibs-footer.js.map +1 -1
  21. package/dist/collection/components/pennlibs-header/pennlibs-header.js +1 -1
  22. package/dist/collection/components/pennlibs-hero/pennlibs-hero.js +1 -1
  23. package/dist/components/pennlibs-feedback.js +16 -17
  24. package/dist/components/pennlibs-feedback.js.map +1 -1
  25. package/dist/components/pennlibs-footer.js +9 -4
  26. package/dist/components/pennlibs-footer.js.map +1 -1
  27. package/dist/components/pennlibs-header.js +1 -1
  28. package/dist/components/pennlibs-hero.js +1 -1
  29. package/dist/docs.json +75 -6
  30. package/dist/{web/p-RqnbThKP.js → esm/index-CNQ9-Wwh.js} +4 -15
  31. package/dist/esm/index-CNQ9-Wwh.js.map +1 -0
  32. package/dist/esm/index.js +1 -1
  33. package/dist/esm/loader.js +3 -3
  34. package/dist/esm/{pennlibs-autocomplete.pennlibs-fallback-img.pennlibs-footer.pennlibs-header.pennlibs-iiif-img.entry.js.map → pennlibs-autocomplete.pennlibs-feedback.pennlibs-footer.pennlibs-header.entry.js.map} +1 -1
  35. package/dist/esm/{pennlibs-autocomplete_5.entry.js → pennlibs-autocomplete_4.entry.js} +54 -257
  36. package/dist/esm/pennlibs-banner.entry.js +1 -1
  37. package/dist/esm/pennlibs-chat.entry.js +1 -1
  38. package/dist/esm/pennlibs-fallback-img.entry.js +18 -0
  39. package/dist/esm/pennlibs-fallback-img.entry.js.map +1 -0
  40. package/dist/esm/pennlibs-hero.entry.js +2 -2
  41. package/dist/esm/pennlibs-iiif-img.entry.js +247 -0
  42. package/dist/esm/pennlibs-iiif-img.entry.js.map +1 -0
  43. package/dist/esm/web.js +3 -3
  44. package/dist/types/components/pennlibs-feedback/pennlibs-feedback.d.ts +19 -3
  45. package/dist/types/components/pennlibs-footer/pennlibs-footer.d.ts +4 -0
  46. package/dist/types/components.d.ts +51 -0
  47. package/dist/web/index.esm.js +1 -1
  48. package/dist/web/{p-f37f3865.entry.js → p-3da26275.entry.js} +54 -257
  49. package/dist/web/p-662e4125.entry.js +18 -0
  50. package/dist/{esm/index-RqnbThKP.js → web/p-CNQ9-Wwh.js} +4 -15
  51. package/dist/web/p-CNQ9-Wwh.js.map +1 -0
  52. package/dist/web/{p-8ac5ef70.entry.js → p-a34e1e3e.entry.js} +1 -1
  53. package/dist/web/{p-b7b01d67.entry.js → p-b6e686d1.entry.js} +2 -2
  54. package/dist/web/{p-ce09ae2e.entry.js → p-c96a3643.entry.js} +1 -1
  55. package/dist/web/p-eb633f4d.entry.js +247 -0
  56. package/dist/{cjs/pennlibs-autocomplete.pennlibs-fallback-img.pennlibs-footer.pennlibs-header.pennlibs-iiif-img.entry.cjs.js.map → web/pennlibs-autocomplete.pennlibs-feedback.pennlibs-footer.pennlibs-header.entry.esm.js.map} +1 -1
  57. package/dist/web/pennlibs-fallback-img.entry.esm.js.map +1 -0
  58. package/dist/web/pennlibs-iiif-img.entry.esm.js.map +1 -0
  59. package/dist/web/web.esm.js +3 -3
  60. package/hydrate/index.js +39 -35
  61. package/hydrate/index.mjs +39 -35
  62. package/package.json +1 -1
  63. package/dist/cjs/index-DJo51Q03.js.map +0 -1
  64. package/dist/cjs/pennlibs-feedback.cjs.entry.js +0 -58
  65. package/dist/cjs/pennlibs-feedback.entry.cjs.js.map +0 -1
  66. package/dist/esm/index-RqnbThKP.js.map +0 -1
  67. package/dist/esm/pennlibs-feedback.entry.js +0 -56
  68. package/dist/esm/pennlibs-feedback.entry.js.map +0 -1
  69. package/dist/web/p-780e656e.entry.js +0 -56
  70. package/dist/web/p-RqnbThKP.js.map +0 -1
  71. package/dist/web/pennlibs-feedback.entry.esm.js.map +0 -1
@@ -0,0 +1,249 @@
1
+ 'use strict';
2
+
3
+ var index = require('./index-Ag6DtE87.js');
4
+
5
+ const pennlibsIiifImgCss = ":host{display:block;width:100%;max-width:100%;align-self:flex-start;aspect-ratio:var(--aspect-ratio, auto)}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:100%}.fallback-container pennlibs-fallback-img{width:100%;height:100%}pennlibs-fallback-img{display:block;width:100%;height:100%}";
6
+
7
+ const PIXEL_DENSITIES = [1, 1.5, 2];
8
+ const IIIFImg = class {
9
+ constructor(hostRef) {
10
+ index.registerInstance(this, hostRef);
11
+ /**
12
+ * The IIIF [region](https://iiif.io/api/image/3.0/#41-region) of the image to display.
13
+ * Defines the rectangular portion of the underlying image to return.
14
+ *
15
+ * `full`: The full image is returned, without any cropping.
16
+ *
17
+ * `square`: A square area where width and height equal the shorter dimension.
18
+ *
19
+ * `width:height`: Any aspect ratio format (e.g., `16:9`, `4:3`, `3:2`, `21:9`) applies
20
+ * a centered crop based on the source image dimensions and sets the CSS aspect-ratio
21
+ * property for layout reservation.
22
+ *
23
+ * `x,y,w,h`: Absolute pixel coordinates (x, y position; w, h dimensions).
24
+ *
25
+ * `pct:x,y,w,h`: Percentage-based coordinates of full image dimensions.
26
+ *
27
+ * @default 'full'
28
+ */
29
+ this.region = 'full';
30
+ /**
31
+ * The IIIF [rotation](https://iiif.io/api/image/3.0/#44-rotation) to apply to the image.
32
+ * Specifies mirroring and clockwise rotation in degrees (0-360).
33
+ *
34
+ * `n`: Rotation in degrees only.
35
+ *
36
+ * `!n`: Mirror the image vertically, then rotate by n degrees.
37
+ *
38
+ * @default '0'
39
+ */
40
+ this.rotation = '0';
41
+ /**
42
+ * The IIIF [quality](https://iiif.io/api/image/3.0/#quality) of the image.
43
+ * Controls the color delivery mode.
44
+ *
45
+ * `default`: The server's default quality.
46
+ *
47
+ * `color`: Full color information.
48
+ *
49
+ * `gray`: Grayscale rendering.
50
+ *
51
+ * `bitonal`: Black and white only.
52
+ *
53
+ * @default 'default'
54
+ */
55
+ this.quality = 'default';
56
+ /**
57
+ * Native browser lazy loading behavior. Use "lazy" to defer loading until the image is near the viewport,
58
+ * or "eager" to load immediately.
59
+ * @default 'lazy'
60
+ */
61
+ this.loading = 'lazy';
62
+ /**
63
+ * Whether to display a fallback placeholder image when the IIIF image fails to load.
64
+ * @default true
65
+ */
66
+ this.showFallback = true;
67
+ this.isLoaded = false;
68
+ this.hasError = false;
69
+ this.baseUrl = 'https://iiif-images.library.upenn.edu/iiif/3';
70
+ this.handleLoad = () => {
71
+ this.isLoaded = true;
72
+ };
73
+ this.handleError = () => {
74
+ this.hasError = true;
75
+ };
76
+ }
77
+ /**
78
+ * Fetch IIIF image info to get actual dimensions.
79
+ */
80
+ async fetchImageInfo() {
81
+ try {
82
+ const currentUuid = this.uuid; // Capture current UUID
83
+ const infoUrl = `${this.baseUrl}/${currentUuid}/info.json`;
84
+ const response = await fetch(infoUrl);
85
+ if (!response.ok) {
86
+ console.error(`Failed to fetch IIIF info.json for ${currentUuid}`);
87
+ return;
88
+ }
89
+ const info = await response.json();
90
+ // Only update dimensions if UUID hasn't changed during fetch
91
+ // (protects against race conditions if component updates)
92
+ if (this.uuid === currentUuid) {
93
+ this.imageDimensions = {
94
+ width: info.width,
95
+ height: info.height,
96
+ };
97
+ this.fetchedUuid = currentUuid;
98
+ }
99
+ }
100
+ catch (error) {
101
+ console.error(`Error fetching IIIF image info for ${this.uuid}:`, error);
102
+ }
103
+ }
104
+ /**
105
+ * Check if a region value matches aspect ratio format (e.g., "16:9", "4:3", "3:2").
106
+ */
107
+ isAspectRatio(value) {
108
+ return /^\d+:\d+$/.test(value);
109
+ }
110
+ /**
111
+ * Set CSS custom property for aspect ratio on the host element.
112
+ */
113
+ setAspectRatioCss(aspectRatio) {
114
+ const [widthRatio, heightRatio] = aspectRatio.split(':');
115
+ this.hostElement.style.setProperty('--aspect-ratio', `${widthRatio} / ${heightRatio}`);
116
+ }
117
+ /**
118
+ * Clear CSS custom property for aspect ratio from the host element.
119
+ */
120
+ clearAspectRatioCss() {
121
+ this.hostElement.style.removeProperty('--aspect-ratio');
122
+ }
123
+ /**
124
+ * Format a number for IIIF percentage values (remove trailing zeros).
125
+ * IIIF spec requires no trailing zeros per section 4.7.
126
+ */
127
+ formatPercent(value) {
128
+ return value.toFixed(2).replace(/\.?0+$/, '');
129
+ }
130
+ /**
131
+ * Calculate a centered crop region for a given aspect ratio.
132
+ * Requires actual image dimensions to calculate correctly.
133
+ * @param aspectRatio - The target aspect ratio (e.g., "3:2" or "2:3")
134
+ * @returns A IIIF percentage-based region string (e.g., "pct:0,16.67,100,66.67")
135
+ */
136
+ calculateCenteredCrop(aspectRatio) {
137
+ if (!this.imageDimensions) {
138
+ throw new Error('Image dimensions required for aspect ratio calculation');
139
+ }
140
+ const [widthRatio, heightRatio] = aspectRatio.split(':').map(Number);
141
+ const targetRatio = widthRatio / heightRatio;
142
+ const sourceRatio = this.imageDimensions.width / this.imageDimensions.height;
143
+ // Determine if we need to crop width or height
144
+ if (sourceRatio > targetRatio) {
145
+ // Source is wider than target - crop left and right
146
+ const widthPercent = (targetRatio / sourceRatio) * 100;
147
+ const xOffset = (100 - widthPercent) / 2;
148
+ return `pct:${this.formatPercent(xOffset)},0,${this.formatPercent(widthPercent)},100`;
149
+ }
150
+ else {
151
+ // Source is taller than target - crop top and bottom
152
+ const heightPercent = (sourceRatio / targetRatio) * 100;
153
+ const yOffset = (100 - heightPercent) / 2;
154
+ return `pct:0,${this.formatPercent(yOffset)},100,${this.formatPercent(heightPercent)}`;
155
+ }
156
+ }
157
+ /**
158
+ * Get the region parameter for the IIIF URL.
159
+ */
160
+ getRegionParam() {
161
+ // Handle custom aspect ratios
162
+ let regionParam = this.region || 'full';
163
+ if (this.isAspectRatio(regionParam)) {
164
+ // Only apply crop if we have dimensions for the current UUID
165
+ if (this.imageDimensions && this.fetchedUuid === this.uuid) {
166
+ regionParam = this.calculateCenteredCrop(regionParam);
167
+ }
168
+ else {
169
+ // Use full image until dimensions are loaded
170
+ regionParam = 'full';
171
+ }
172
+ }
173
+ return regionParam;
174
+ }
175
+ buildIIIFUrl(width, format = 'jpg') {
176
+ const sizeParam = width ? `${width},` : 'max';
177
+ const regionParam = this.getRegionParam();
178
+ return `${this.baseUrl}/${this.uuid}/${regionParam}/${sizeParam}/${this.rotation}/${this.quality}.${format}`;
179
+ }
180
+ generateSrcset(baseWidth, format) {
181
+ return PIXEL_DENSITIES.map(density => {
182
+ const width = Math.round(baseWidth * density);
183
+ const url = this.buildIIIFUrl(width, format);
184
+ return `${url} ${density}x`;
185
+ }).join(', ');
186
+ }
187
+ handleRegionChange(newValue, oldValue) {
188
+ // Only process if region actually changed
189
+ if (newValue === oldValue) {
190
+ return;
191
+ }
192
+ // If new region is an aspect ratio, set CSS and fetch info
193
+ if (newValue && this.isAspectRatio(newValue)) {
194
+ this.setAspectRatioCss(newValue);
195
+ this.fetchImageInfo();
196
+ }
197
+ else {
198
+ // Clear aspect ratio CSS if no longer using aspect ratio format
199
+ this.clearAspectRatioCss();
200
+ }
201
+ }
202
+ componentDidLoad() {
203
+ // Set CSS aspect ratio and fetch image info if using aspect ratio format
204
+ if (this.region && this.isAspectRatio(this.region)) {
205
+ this.setAspectRatioCss(this.region);
206
+ this.fetchImageInfo();
207
+ }
208
+ if ('ResizeObserver' in window) {
209
+ this.resizeObserver = new ResizeObserver(entries => {
210
+ for (const entry of entries) {
211
+ this.observedWidth = Math.round(entry.contentRect.width);
212
+ if (this.resizeObserver) {
213
+ this.resizeObserver.disconnect();
214
+ this.resizeObserver = undefined;
215
+ }
216
+ }
217
+ });
218
+ this.resizeObserver.observe(this.hostElement);
219
+ }
220
+ }
221
+ disconnectedCallback() {
222
+ if (this.resizeObserver) {
223
+ this.resizeObserver.disconnect();
224
+ this.resizeObserver = undefined;
225
+ }
226
+ }
227
+ render() {
228
+ // Show placeholder until we've measured the component width
229
+ if (!this.observedWidth) {
230
+ return index.h("pennlibs-fallback-img", null);
231
+ }
232
+ if (this.hasError && this.showFallback) {
233
+ return (index.h("div", { class: "fallback-container" }, index.h("pennlibs-fallback-img", null)));
234
+ }
235
+ const imgClasses = {
236
+ 'iiif-img': true,
237
+ 'loaded': this.isLoaded
238
+ };
239
+ return (index.h("picture", null, index.h("source", { type: "image/webp", srcSet: this.generateSrcset(this.observedWidth, 'webp') }), index.h("img", { class: imgClasses, src: this.buildIIIFUrl(this.observedWidth, 'jpg'), srcSet: this.generateSrcset(this.observedWidth, 'jpg'), alt: this.alt, loading: this.loading, onLoad: this.handleLoad, onError: this.handleError })));
240
+ }
241
+ get hostElement() { return index.getElement(this); }
242
+ static get watchers() { return {
243
+ "region": ["handleRegionChange"]
244
+ }; }
245
+ };
246
+ IIIFImg.style = pennlibsIiifImgCss;
247
+
248
+ exports.pennlibs_iiif_img = IIIFImg;
249
+ //# sourceMappingURL=pennlibs-iiif-img.entry.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pennlibs-iiif-img.entry.cjs.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 align-self: flex-start;\n aspect-ratio: var(--aspect-ratio, auto);\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: 100%;\n}\n\n.fallback-container pennlibs-fallback-img {\n width: 100%;\n height: 100%;\n}\n\npennlibs-fallback-img {\n display: block;\n width: 100%;\n height: 100%;\n}\n","import { Component, h, Prop, State, Element, Watch } from '@stencil/core';\n\ntype ImageFormat = 'jpg' | 'webp';\n\nconst PIXEL_DENSITIES = [1, 1.5, 2] as const;\n\n/**\n * Display responsive, high-quality images from Penn Libraries' IIIF Image API 3.0 server.\n * Measures its own rendered width and automatically generates optimal image sources at\n * multiple pixel densities (1x, 1.5x, 2x) 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 * `width:height`: Any aspect ratio format (e.g., `16:9`, `4:3`, `3:2`, `21:9`) applies\n * a centered crop based on the source image dimensions and sets the CSS aspect-ratio\n * property for layout reservation.\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({ reflect: true }) 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 @State() imageDimensions?: { width: number; height: number };\n @State() observedWidth?: number;\n\n private fetchedUuid?: string; // Track which UUID these dimensions belong to\n\n private readonly baseUrl = 'https://iiif-images.library.upenn.edu/iiif/3';\n\n /**\n * Fetch IIIF image info to get actual dimensions.\n */\n private async fetchImageInfo(): Promise<void> {\n try {\n const currentUuid = this.uuid; // Capture current UUID\n const infoUrl = `${this.baseUrl}/${currentUuid}/info.json`;\n const response = await fetch(infoUrl);\n if (!response.ok) {\n console.error(`Failed to fetch IIIF info.json for ${currentUuid}`);\n return;\n }\n const info = await response.json();\n\n // Only update dimensions if UUID hasn't changed during fetch\n // (protects against race conditions if component updates)\n if (this.uuid === currentUuid) {\n this.imageDimensions = {\n width: info.width,\n height: info.height,\n };\n this.fetchedUuid = currentUuid;\n }\n } catch (error) {\n console.error(`Error fetching IIIF image info for ${this.uuid}:`, error);\n }\n }\n\n /**\n * Check if a region value matches aspect ratio format (e.g., \"16:9\", \"4:3\", \"3:2\").\n */\n private isAspectRatio(value: string): boolean {\n return /^\\d+:\\d+$/.test(value);\n }\n\n /**\n * Set CSS custom property for aspect ratio on the host element.\n */\n private setAspectRatioCss(aspectRatio: string): void {\n const [widthRatio, heightRatio] = aspectRatio.split(':');\n this.hostElement.style.setProperty('--aspect-ratio', `${widthRatio} / ${heightRatio}`);\n }\n\n /**\n * Clear CSS custom property for aspect ratio from the host element.\n */\n private clearAspectRatioCss(): void {\n this.hostElement.style.removeProperty('--aspect-ratio');\n }\n\n /**\n * Format a number for IIIF percentage values (remove trailing zeros).\n * IIIF spec requires no trailing zeros per section 4.7.\n */\n private formatPercent(value: number): string {\n return value.toFixed(2).replace(/\\.?0+$/, '');\n }\n\n /**\n * Calculate a centered crop region for a given aspect ratio.\n * Requires actual image dimensions to calculate correctly.\n * @param aspectRatio - The target aspect ratio (e.g., \"3:2\" or \"2:3\")\n * @returns A IIIF percentage-based region string (e.g., \"pct:0,16.67,100,66.67\")\n */\n private calculateCenteredCrop(aspectRatio: string): string {\n if (!this.imageDimensions) {\n throw new Error('Image dimensions required for aspect ratio calculation');\n }\n\n const [widthRatio, heightRatio] = aspectRatio.split(':').map(Number);\n const targetRatio = widthRatio / heightRatio;\n const sourceRatio = this.imageDimensions.width / this.imageDimensions.height;\n\n // Determine if we need to crop width or height\n if (sourceRatio > targetRatio) {\n // Source is wider than target - crop left and right\n const widthPercent = (targetRatio / sourceRatio) * 100;\n const xOffset = (100 - widthPercent) / 2;\n return `pct:${this.formatPercent(xOffset)},0,${this.formatPercent(widthPercent)},100`;\n } else {\n // Source is taller than target - crop top and bottom\n const heightPercent = (sourceRatio / targetRatio) * 100;\n const yOffset = (100 - heightPercent) / 2;\n return `pct:0,${this.formatPercent(yOffset)},100,${this.formatPercent(heightPercent)}`;\n }\n }\n\n /**\n * Get the region parameter for the IIIF URL.\n */\n private getRegionParam(): string {\n // Handle custom aspect ratios\n let regionParam = this.region || 'full';\n if (this.isAspectRatio(regionParam)) {\n // Only apply crop if we have dimensions for the current UUID\n if (this.imageDimensions && this.fetchedUuid === this.uuid) {\n regionParam = this.calculateCenteredCrop(regionParam);\n } else {\n // Use full image until dimensions are loaded\n regionParam = 'full';\n }\n }\n\n return regionParam;\n }\n\n private buildIIIFUrl(width?: number, format: ImageFormat = 'jpg'): string {\n const sizeParam = width ? `${width},` : 'max';\n const regionParam = this.getRegionParam();\n return `${this.baseUrl}/${this.uuid}/${regionParam}/${sizeParam}/${this.rotation}/${this.quality}.${format}`;\n }\n\n private generateSrcset(baseWidth: number, format: ImageFormat): string {\n return PIXEL_DENSITIES.map(density => {\n const width = Math.round(baseWidth * density);\n const url = this.buildIIIFUrl(width, format);\n return `${url} ${density}x`;\n }).join(', ');\n }\n\n\n private handleLoad = () => {\n this.isLoaded = true;\n };\n\n private handleError = () => {\n this.hasError = true;\n };\n\n @Watch('region')\n handleRegionChange(newValue: string, oldValue: string) {\n // Only process if region actually changed\n if (newValue === oldValue) {\n return;\n }\n\n // If new region is an aspect ratio, set CSS and fetch info\n if (newValue && this.isAspectRatio(newValue)) {\n this.setAspectRatioCss(newValue);\n this.fetchImageInfo();\n } else {\n // Clear aspect ratio CSS if no longer using aspect ratio format\n this.clearAspectRatioCss();\n }\n }\n\n componentDidLoad() {\n // Set CSS aspect ratio and fetch image info if using aspect ratio format\n if (this.region && this.isAspectRatio(this.region)) {\n this.setAspectRatioCss(this.region);\n this.fetchImageInfo();\n }\n\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 });\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 // Show placeholder until we've measured the component width\n if (!this.observedWidth) {\n return <pennlibs-fallback-img />;\n }\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 return (\n <picture>\n <source\n type=\"image/webp\"\n srcSet={this.generateSrcset(this.observedWidth, 'webp')}\n />\n\n <img\n class={imgClasses}\n src={this.buildIIIFUrl(this.observedWidth, 'jpg')}\n srcSet={this.generateSrcset(this.observedWidth, 'jpg')}\n alt={this.alt}\n loading={this.loading}\n onLoad={this.handleLoad}\n onError={this.handleError}\n />\n </picture>\n );\n }\n}\n"],"names":["h"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var index = require('./index-DJo51Q03.js');
3
+ var index = require('./index-Ag6DtE87.js');
4
4
 
5
5
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
6
6
  /*
@@ -18,7 +18,7 @@ var patchBrowser = () => {
18
18
 
19
19
  patchBrowser().then(async (options) => {
20
20
  await index.globalScripts();
21
- return index.bootstrapLazy([["pennlibs-banner.cjs",[[257,"pennlibs-banner"]]],["pennlibs-chat.cjs",[[257,"pennlibs-chat",{"href":[32]}]]],["pennlibs-feedback.cjs",[[257,"pennlibs-feedback",{"error":[32],"answer":[32]}]]],["pennlibs-hero.cjs",[[257,"pennlibs-hero",{"heroPictureElement":[32],"heroHeadingElement":[32],"heroParagraphElement":[32],"heroSrc":[32]}]]],["pennlibs-autocomplete_5.cjs",[[257,"pennlibs-iiif-img",{"uuid":[1],"alt":[1],"region":[513],"rotation":[1],"quality":[1],"loading":[1],"showFallback":[4,"show-fallback"],"isLoaded":[32],"hasError":[32],"imageDimensions":[32],"observedWidth":[32]},null,{"region":["handleRegionChange"]}],[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]}],[257,"pennlibs-fallback-img"]]]], options);
21
+ return index.bootstrapLazy([["pennlibs-iiif-img.cjs",[[257,"pennlibs-iiif-img",{"uuid":[1],"alt":[1],"region":[513],"rotation":[1],"quality":[1],"loading":[1],"showFallback":[4,"show-fallback"],"isLoaded":[32],"hasError":[32],"imageDimensions":[32],"observedWidth":[32]},null,{"region":["handleRegionChange"]}]]],["pennlibs-autocomplete_4.cjs",[[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-feedback",{"hideQuestion":[4,"hide-question"],"error":[32],"answer":[32]}],[257,"pennlibs-footer",{"showBuiltWith":[4,"show-built-with"],"navigationByChildren":[32]}],[257,"pennlibs-header",{"serviceName":[1,"service-name"],"serviceLede":[1,"service-lede"],"serviceHref":[1,"service-href"],"theme":[1],"isMenuOpen":[32],"navigation":[32]}]]],["pennlibs-banner.cjs",[[257,"pennlibs-banner"]]],["pennlibs-chat.cjs",[[257,"pennlibs-chat",{"href":[32]}]]],["pennlibs-hero.cjs",[[257,"pennlibs-hero",{"heroPictureElement":[32],"heroHeadingElement":[32],"heroParagraphElement":[32],"heroSrc":[32]}]]],["pennlibs-fallback-img.cjs",[[257,"pennlibs-fallback-img"]]]], options);
22
22
  });
23
23
 
24
24
  exports.setNonce = index.setNonce;
@@ -3,12 +3,12 @@
3
3
  :host {
4
4
  display: block;
5
5
  background: #eeeff4;
6
- padding: 1.5em 1em;
7
6
  font-family: var(--pl-font-family);
8
7
  font-size: var(--pl-font-size);
9
8
  color: var(--pl-color-fg-default);
10
9
  line-height: 1.4;
11
10
  box-sizing: border-box;
11
+ padding: 0.75em 0;
12
12
  }
13
13
 
14
14
  @media print {
@@ -17,31 +17,40 @@
17
17
  }
18
18
  }
19
19
 
20
+ *, *::before, *::after {
21
+ box-sizing: border-box;
22
+ }
23
+
24
+ .viewport-margins {
25
+ max-width: var(--pl-viewport-margins-max-width);
26
+ margin: 0 auto;
27
+ padding: 0 var(--pl-viewport-margins-gutter, 1em);
28
+ }
29
+
30
+ .feedback__content {
31
+ display: flex;
32
+ justify-content: space-between;
33
+ gap: var(--pl-space-s);
34
+ flex-wrap: wrap;
35
+ }
36
+
20
37
  /* Container Layout
21
38
  ---------------------------------------- */
22
- .feedback-container {
39
+ .feedback__inline-question {
23
40
  display: flex;
24
- align-items: center;
25
41
  flex-wrap: wrap;
26
42
  gap: 0.5em;
27
- width: 100%;
28
- justify-content: center;
29
- }
30
-
31
- @media (max-width: 26em) {
32
- .feedback-container {
33
- justify-content: center;
34
- }
43
+ align-items: center;
35
44
  }
36
45
 
37
46
  /* Typography
38
47
  ---------------------------------------- */
39
48
  h2 {
40
49
  display: inline-block;
41
- margin: 0 0.5em 0 0;
50
+ margin: 0;
42
51
  font-family: var(--pl-font-sans-serif);
43
52
  font-size: 1em;
44
- font-weight: 600;
53
+ font-weight: 400;
45
54
  }
46
55
 
47
56
  p {
@@ -59,7 +68,7 @@ a {
59
68
  color: var(--pl-color-fg-accent);
60
69
  text-decoration: underline;
61
70
  text-underline-offset: var(--pl-link-text-underline-offset);
62
- text-decoration-thickness: var(--pl-link-text-decoration-thickness);
71
+ text-decoration-thickness: var(--pl-link-hover-text-decoration-thickness);
63
72
  }
64
73
 
65
74
  /* Buttons
@@ -69,7 +78,7 @@ button {
69
78
  display: flex;
70
79
  align-items: center;
71
80
  gap: 0.5em;
72
- padding: 0.5em 1.5em;
81
+ padding: 0.25em 1.25em;
73
82
  font-family: var(--pl-font-family);
74
83
  font-weight: 500;
75
84
  line-height: 1.4;
@@ -1,13 +1,35 @@
1
1
  import { h } from "@stencil/core";
2
2
  const FEEDBACK_STORAGE_KEY = 'pennlibs-feedback-answer';
3
- const FEEDBACK_QUESTION = 'Did you find what you need?';
3
+ const FEEDBACK_TIMESTAMP_KEY = 'pennlibs-feedback-timestamp';
4
+ const FEEDBACK_TTL = 5 * 60 * 1000; // 5 minutes
5
+ const FEEDBACK_QUESTION = 'Is this page useful?';
4
6
  const FEEDBACK_ERROR_STORAGE_KEY = 'pennlibs-feedback-error';
7
+ /**
8
+ * A feedback component that asks visitors "Did you find what you need?" and
9
+ * links to a feedback form. The answer is stored in sessionStorage for 5 minutes
10
+ * to avoid re-prompting during a browsing session.
11
+ *
12
+ * @example
13
+ * <pennlibs-feedback></pennlibs-feedback>
14
+ *
15
+ * @example
16
+ * <pennlibs-feedback hide-question></pennlibs-feedback>
17
+ */
5
18
  export class PennlibsFeedback {
6
19
  constructor() {
20
+ /**
21
+ * When true, hides the yes/no question and only shows the "share your feedback" link.
22
+ */
23
+ this.hideQuestion = false;
7
24
  this.error = false;
8
25
  this.answer = undefined;
9
26
  }
10
27
  componentWillLoad() {
28
+ const timestamp = sessionStorage.getItem(FEEDBACK_TIMESTAMP_KEY);
29
+ if (timestamp && Date.now() - Number(timestamp) > FEEDBACK_TTL) {
30
+ sessionStorage.removeItem(FEEDBACK_STORAGE_KEY);
31
+ sessionStorage.removeItem(FEEDBACK_TIMESTAMP_KEY);
32
+ }
11
33
  if (sessionStorage.getItem(FEEDBACK_STORAGE_KEY)) {
12
34
  this.answer = sessionStorage.getItem(FEEDBACK_STORAGE_KEY);
13
35
  }
@@ -15,18 +37,11 @@ export class PennlibsFeedback {
15
37
  this.error = sessionStorage.getItem(FEEDBACK_ERROR_STORAGE_KEY) === 'true';
16
38
  }
17
39
  }
18
- componentDidRender() {
19
- if (this.answer === 'yes' && this.yesMessageRef) {
20
- this.yesMessageRef.focus();
21
- }
22
- else if (this.answer === 'no' && this.noMessageRef) {
23
- this.noMessageRef.focus();
24
- }
25
- }
26
40
  handleFeedback(answer) {
27
41
  var _a;
28
42
  this.answer = answer;
29
43
  sessionStorage.setItem(FEEDBACK_STORAGE_KEY, answer);
44
+ sessionStorage.setItem(FEEDBACK_TIMESTAMP_KEY, String(Date.now()));
30
45
  this.feedbackEvent.emit({
31
46
  question: FEEDBACK_QUESTION,
32
47
  answer: answer,
@@ -36,13 +51,7 @@ export class PennlibsFeedback {
36
51
  });
37
52
  }
38
53
  render() {
39
- if (this.answer === 'yes') {
40
- return (h("div", { class: "feedback-container" }, h("p", { ref: (el) => this.yesMessageRef = el, tabindex: "-1" }, h("strong", null, "Thank you"), " for your feedback! \uD83D\uDC4D")));
41
- }
42
- if (this.answer === 'no') {
43
- return (h("div", { class: "feedback-container" }, h("p", { ref: (el) => this.noMessageRef = el, tabindex: "-1" }, "To ", h("strong", null, "help us improve"), ", we'd like to know more about your visit. Please ", h("a", { href: "https://www.library.upenn.edu/contact-us" }, "contact us with your feedback"), ".")));
44
- }
45
- return (h("div", { class: "feedback-container" }, h("h2", null, FEEDBACK_QUESTION), h("button", { onClick: () => this.handleFeedback('yes') }, "\uD83D\uDC4D", h("span", null, "Yes", h("span", { class: "visually-hidden" }, ", I found what I need."))), h("button", { onClick: () => this.handleFeedback('no') }, "\uD83D\uDC4E", h("span", null, "No", h("span", { class: "visually-hidden" }, ", I didn't find what I need.")))));
54
+ return (h("div", { key: '04053c96e964ee9f240ca1a77d7944ae0f53248d', class: "feedback__container" }, h("div", { key: 'f5f524b449d3473c1ecc2d454787034c54a811a0', class: "viewport-margins" }, h("div", { key: 'd0c832edc9361dac49d9cbfc143332fea04434ea', class: "feedback__content" }, !this.hideQuestion && (this.answer === 'yes' ? (h("p", null, "Thank you for letting us know.")) : this.answer === 'no' ? (h("p", null, "Thank you for letting us know. ", h("a", { href: "https://www.library.upenn.edu/contact-us" }, "Contact us"), " if you need help.")) : (h("div", { class: "feedback__inline-question" }, h("h2", null, "Did you find what you need?"), h("button", { onClick: () => this.handleFeedback('yes') }, h("span", null, "Yes"), h("span", { class: "visually-hidden" }, ", I found what I needed.")), h("button", { onClick: () => this.handleFeedback('no') }, h("span", null, "No"), h("span", { class: "visually-hidden" }, ", I did not find what I needed."))))), h("div", { key: '9d4de668c9e3696d8983c48fcc20ea89b2163ea1', class: "feedback__share-feedback" }, h("p", { key: '1377153ebba81c48b0a70756b190503b9cd33c61' }, "To help us improve, ", h("a", { key: '26f3f05b0af848ff58d959190a380d5b64db254a', href: "http://library.upenn.edu/feedback" }, "share your feedback"), "."))))));
46
55
  }
47
56
  static get is() { return "pennlibs-feedback"; }
48
57
  static get encapsulation() { return "shadow"; }
@@ -56,6 +65,30 @@ export class PennlibsFeedback {
56
65
  "$": ["pennlibs-feedback.css"]
57
66
  };
58
67
  }
68
+ static get properties() {
69
+ return {
70
+ "hideQuestion": {
71
+ "type": "boolean",
72
+ "mutable": false,
73
+ "complexType": {
74
+ "original": "boolean",
75
+ "resolved": "boolean",
76
+ "references": {}
77
+ },
78
+ "required": false,
79
+ "optional": false,
80
+ "docs": {
81
+ "tags": [],
82
+ "text": "When true, hides the yes/no question and only shows the \"share your feedback\" link."
83
+ },
84
+ "getter": false,
85
+ "setter": false,
86
+ "reflect": false,
87
+ "attribute": "hide-question",
88
+ "defaultValue": "false"
89
+ }
90
+ };
91
+ }
59
92
  static get states() {
60
93
  return {
61
94
  "error": {},
@@ -71,7 +104,7 @@ export class PennlibsFeedback {
71
104
  "composed": true,
72
105
  "docs": {
73
106
  "tags": [],
74
- "text": ""
107
+ "text": "Emitted when a visitor answers the feedback question. Includes the question,\nanswer, current URL, and page heading/title for context."
75
108
  },
76
109
  "complexType": {
77
110
  "original": "{\n question: string;\n answer: string;\n url: string;\n pageHeading?: string;\n pageTitle?: string;\n }",
@@ -1 +1 @@
1
- {"version":3,"file":"pennlibs-feedback.js","sourceRoot":"","sources":["../../../src/components/pennlibs-feedback/pennlibs-feedback.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Component, h, State, Event, EventEmitter } from '@stencil/core';\n\nconst FEEDBACK_STORAGE_KEY = 'pennlibs-feedback-answer';\nconst FEEDBACK_QUESTION = 'Did you find what you need?';\nconst FEEDBACK_ERROR_STORAGE_KEY = 'pennlibs-feedback-error';\n\n@Component({\n tag: 'pennlibs-feedback',\n styleUrl: 'pennlibs-feedback.css',\n shadow: true,\n})\nexport class PennlibsFeedback { \n @State() error: boolean = false;\n @State() answer: string | undefined = undefined;\n\n @Event({\n eventName: 'pennlibs-feedback',\n composed: true,\n cancelable: true,\n bubbles: true,\n }) feedbackEvent!: EventEmitter<{\n question: string;\n answer: string;\n url: string;\n pageHeading?: string;\n pageTitle?: string;\n }>;\n\n private yesMessageRef?: HTMLParagraphElement;\n private noMessageRef?: HTMLParagraphElement;\n\n componentWillLoad() {\n if (sessionStorage.getItem(FEEDBACK_STORAGE_KEY)) {\n this.answer = sessionStorage.getItem(FEEDBACK_STORAGE_KEY) as string;\n }\n\n if (sessionStorage.getItem(FEEDBACK_ERROR_STORAGE_KEY)) {\n this.error = sessionStorage.getItem(FEEDBACK_ERROR_STORAGE_KEY) === 'true';\n }\n }\n\n componentDidRender() {\n if (this.answer === 'yes' && this.yesMessageRef) {\n this.yesMessageRef.focus();\n } else if (this.answer === 'no' && this.noMessageRef) {\n this.noMessageRef.focus();\n }\n }\n\n handleFeedback(answer: string) {\n this.answer = answer;\n sessionStorage.setItem(FEEDBACK_STORAGE_KEY, answer);\n \n this.feedbackEvent.emit({\n question: FEEDBACK_QUESTION,\n answer: answer,\n url: window.location.href,\n pageHeading: document.querySelector('h1')?.textContent || undefined,\n pageTitle: document.title || undefined\n });\n }\n \n render() {\n if (this.answer === 'yes') {\n return (\n <div class=\"feedback-container\">\n <p \n ref={(el) => this.yesMessageRef = el as HTMLParagraphElement}\n tabindex=\"-1\"\n >\n <strong>Thank you</strong> for your feedback! 👍\n </p>\n </div>\n );\n }\n\n if (this.answer === 'no') {\n return (\n <div class=\"feedback-container\">\n <p \n ref={(el) => this.noMessageRef = el as HTMLParagraphElement}\n tabindex=\"-1\"\n >\n To <strong>help us improve</strong>, we'd like to know more about your visit. Please <a href=\"https://www.library.upenn.edu/contact-us\">contact us with your feedback</a>.\n </p>\n </div>\n );\n }\n\n return (\n <div class=\"feedback-container\">\n <h2>{FEEDBACK_QUESTION}</h2>\n <button onClick={() => this.handleFeedback('yes')}>\n 👍\n <span>Yes<span class=\"visually-hidden\">, I found what I need.</span></span>\n </button>\n <button onClick={() => this.handleFeedback('no')}>\n 👎\n <span>No<span class=\"visually-hidden\">, I didn't find what I need.</span></span>\n </button>\n </div>\n )\n }\n}\n\n"]}
1
+ {"version":3,"file":"pennlibs-feedback.js","sourceRoot":"","sources":["../../../src/components/pennlibs-feedback/pennlibs-feedback.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Component, h, Prop, State, Event, EventEmitter } from '@stencil/core';\n\nconst FEEDBACK_STORAGE_KEY = 'pennlibs-feedback-answer';\nconst FEEDBACK_TIMESTAMP_KEY = 'pennlibs-feedback-timestamp';\nconst FEEDBACK_TTL = 5 * 60 * 1000; // 5 minutes\nconst FEEDBACK_QUESTION = 'Is this page useful?';\nconst FEEDBACK_ERROR_STORAGE_KEY = 'pennlibs-feedback-error';\n\n/**\n * A feedback component that asks visitors \"Did you find what you need?\" and\n * links to a feedback form. The answer is stored in sessionStorage for 5 minutes\n * to avoid re-prompting during a browsing session.\n *\n * @example\n * <pennlibs-feedback></pennlibs-feedback>\n *\n * @example\n * <pennlibs-feedback hide-question></pennlibs-feedback>\n */\n@Component({\n tag: 'pennlibs-feedback',\n styleUrl: 'pennlibs-feedback.css',\n shadow: true,\n})\nexport class PennlibsFeedback {\n /**\n * When true, hides the yes/no question and only shows the \"share your feedback\" link.\n */\n @Prop() hideQuestion: boolean = false;\n\n @State() error: boolean = false;\n @State() answer: string | undefined = undefined;\n\n /**\n * Emitted when a visitor answers the feedback question. Includes the question,\n * answer, current URL, and page heading/title for context.\n */\n @Event({\n eventName: 'pennlibs-feedback',\n composed: true,\n cancelable: true,\n bubbles: true,\n }) feedbackEvent!: EventEmitter<{\n question: string;\n answer: string;\n url: string;\n pageHeading?: string;\n pageTitle?: string;\n }>;\n\n componentWillLoad() {\n const timestamp = sessionStorage.getItem(FEEDBACK_TIMESTAMP_KEY);\n if (timestamp && Date.now() - Number(timestamp) > FEEDBACK_TTL) {\n sessionStorage.removeItem(FEEDBACK_STORAGE_KEY);\n sessionStorage.removeItem(FEEDBACK_TIMESTAMP_KEY);\n }\n\n if (sessionStorage.getItem(FEEDBACK_STORAGE_KEY)) {\n this.answer = sessionStorage.getItem(FEEDBACK_STORAGE_KEY) as string;\n }\n\n if (sessionStorage.getItem(FEEDBACK_ERROR_STORAGE_KEY)) {\n this.error = sessionStorage.getItem(FEEDBACK_ERROR_STORAGE_KEY) === 'true';\n }\n }\n\n handleFeedback(answer: string) {\n this.answer = answer;\n sessionStorage.setItem(FEEDBACK_STORAGE_KEY, answer);\n sessionStorage.setItem(FEEDBACK_TIMESTAMP_KEY, String(Date.now()));\n \n this.feedbackEvent.emit({\n question: FEEDBACK_QUESTION,\n answer: answer,\n url: window.location.href,\n pageHeading: document.querySelector('h1')?.textContent || undefined,\n pageTitle: document.title || undefined\n });\n }\n \n render() {\n return (\n <div class=\"feedback__container\">\n <div class=\"viewport-margins\">\n <div class=\"feedback__content\">\n {!this.hideQuestion && (this.answer === 'yes' ? (\n <p>Thank you for letting us know.</p>\n ) : this.answer === 'no' ? (\n <p>Thank you for letting us know. <a href=\"https://www.library.upenn.edu/contact-us\">Contact us</a> if you need help.</p>\n ) : (\n <div class=\"feedback__inline-question\">\n <h2>Did you find what you need?</h2>\n <button onClick={() => this.handleFeedback('yes')}>\n <span>Yes</span><span class=\"visually-hidden\">, I found what I needed.</span>\n </button>\n <button onClick={() => this.handleFeedback('no')}>\n <span>No</span><span class=\"visually-hidden\">, I did not find what I needed.</span>\n </button>\n </div>\n ))}\n <div class=\"feedback__share-feedback\">\n <p>To help us improve, <a href=\"http://library.upenn.edu/feedback\">share your feedback</a>.</p>\n </div>\n </div>\n </div>\n </div>\n )\n }\n}\n\n"]}
@@ -1,7 +1,7 @@
1
1
  :host {
2
2
  font-family: var(--pl-font-family);
3
3
  font-size: var(--pl-font-size);
4
- --padding-bottom: 5.75rem;
4
+ --padding-bottom: 3em;
5
5
  }
6
6
 
7
7
  @media print {
@@ -34,6 +34,27 @@
34
34
  color: var(--pl-color-fg-subtle-on-emphasis);
35
35
  }
36
36
 
37
+ .built-with,
38
+ .built-with a {
39
+ color: var(--pl-color-fg-subtle-on-emphasis);
40
+ }
41
+
42
+ .built-with a {
43
+ font-weight: 500;
44
+ text-underline-offset: var(--pl-link-text-underline-offset);
45
+
46
+ &:hover {
47
+ text-decoration-thickness: var(--pl-link-hover-text-decoration-thickness);
48
+ }
49
+ }
50
+
51
+ .built-with {
52
+ opacity: 0.875;
53
+ margin: 0;
54
+ padding: 1.5em 0;
55
+ font-size: 0.875em;
56
+ }
57
+
37
58
  .website-footer__content {
38
59
  padding: 4em 0;
39
60
  }
@@ -80,7 +101,7 @@
80
101
  flex-wrap: wrap;
81
102
  gap: 0;
82
103
  flex-direction: row;
83
- margin-left: -0.5em;
104
+ margin-left: -0.75em;
84
105
 
85
106
  @media (max-width: 1080px) {
86
107
  display: grid;