@penn-libraries/web 1.1.1-dev.1 → 1.1.1-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 (61) hide show
  1. package/dist/cjs/{index-C0qvW4Ra.js → index-DVr0pLZy.js} +57 -3
  2. package/dist/cjs/index-DVr0pLZy.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.pennlibs-fallback-img.pennlibs-footer.pennlibs-header.pennlibs-iiif-img.entry.cjs.js.map +1 -0
  6. package/dist/cjs/{pennlibs-autocomplete_3.cjs.entry.js → pennlibs-autocomplete_5.cjs.entry.js} +261 -2
  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-feedback.cjs.entry.js +1 -1
  10. package/dist/cjs/pennlibs-hero.cjs.entry.js +1 -1
  11. package/dist/cjs/web.cjs.js +2 -2
  12. package/dist/collection/components/pennlibs-iiif-img/pennlibs-iiif-img.css +9 -2
  13. package/dist/collection/components/pennlibs-iiif-img/pennlibs-iiif-img.js +147 -25
  14. package/dist/collection/components/pennlibs-iiif-img/pennlibs-iiif-img.js.map +1 -1
  15. package/dist/components/pennlibs-iiif-img.js +144 -23
  16. package/dist/components/pennlibs-iiif-img.js.map +1 -1
  17. package/dist/docs.json +4 -4
  18. package/dist/{web/p-D9dYrmUF.js → esm/index-Cst_89-s.js} +57 -3
  19. package/dist/esm/index-Cst_89-s.js.map +1 -0
  20. package/dist/esm/index.js +1 -1
  21. package/dist/esm/loader.js +3 -3
  22. package/dist/esm/pennlibs-autocomplete.pennlibs-fallback-img.pennlibs-footer.pennlibs-header.pennlibs-iiif-img.entry.js.map +1 -0
  23. package/dist/esm/{pennlibs-autocomplete_3.entry.js → pennlibs-autocomplete_5.entry.js} +260 -3
  24. package/dist/esm/pennlibs-banner.entry.js +1 -1
  25. package/dist/esm/pennlibs-chat.entry.js +1 -1
  26. package/dist/esm/pennlibs-feedback.entry.js +1 -1
  27. package/dist/esm/pennlibs-hero.entry.js +1 -1
  28. package/dist/esm/web.js +3 -3
  29. package/dist/types/components/pennlibs-iiif-img/pennlibs-iiif-img.d.ts +45 -4
  30. package/dist/types/components.d.ts +10 -10
  31. package/dist/web/index.esm.js +1 -1
  32. package/dist/web/{p-cb2584da.entry.js → p-4ffdbc93.entry.js} +1 -1
  33. package/dist/web/{p-b4b58af0.entry.js → p-621f166e.entry.js} +1 -1
  34. package/dist/web/{p-e6188c30.entry.js → p-6e0c2de9.entry.js} +260 -3
  35. package/dist/web/{p-ad92090a.entry.js → p-848d9acc.entry.js} +1 -1
  36. package/dist/{esm/index-D9dYrmUF.js → web/p-Cst_89-s.js} +57 -3
  37. package/dist/web/p-Cst_89-s.js.map +1 -0
  38. package/dist/web/{p-43d9c2d4.entry.js → p-a9c79310.entry.js} +1 -1
  39. package/dist/web/pennlibs-autocomplete.pennlibs-fallback-img.pennlibs-footer.pennlibs-header.pennlibs-iiif-img.entry.esm.js.map +1 -0
  40. package/dist/web/web.esm.js +3 -3
  41. package/hydrate/index.js +189 -30
  42. package/hydrate/index.mjs +189 -30
  43. package/package.json +1 -1
  44. package/dist/cjs/index-C0qvW4Ra.js.map +0 -1
  45. package/dist/cjs/pennlibs-autocomplete.pennlibs-footer.pennlibs-header.entry.cjs.js.map +0 -1
  46. package/dist/cjs/pennlibs-fallback-img.cjs.entry.js +0 -20
  47. package/dist/cjs/pennlibs-fallback-img.entry.cjs.js.map +0 -1
  48. package/dist/cjs/pennlibs-iiif-img.cjs.entry.js +0 -132
  49. package/dist/cjs/pennlibs-iiif-img.entry.cjs.js.map +0 -1
  50. package/dist/esm/index-D9dYrmUF.js.map +0 -1
  51. package/dist/esm/pennlibs-autocomplete.pennlibs-footer.pennlibs-header.entry.js.map +0 -1
  52. package/dist/esm/pennlibs-fallback-img.entry.js +0 -18
  53. package/dist/esm/pennlibs-fallback-img.entry.js.map +0 -1
  54. package/dist/esm/pennlibs-iiif-img.entry.js +0 -130
  55. package/dist/esm/pennlibs-iiif-img.entry.js.map +0 -1
  56. package/dist/web/p-D9dYrmUF.js.map +0 -1
  57. package/dist/web/p-c4074cf1.entry.js +0 -130
  58. package/dist/web/p-ce97059c.entry.js +0 -18
  59. package/dist/web/pennlibs-autocomplete.pennlibs-footer.pennlibs-header.entry.esm.js.map +0 -1
  60. package/dist/web/pennlibs-fallback-img.entry.esm.js.map +0 -1
  61. package/dist/web/pennlibs-iiif-img.entry.esm.js.map +0 -1
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var index = require('./index-C0qvW4Ra.js');
3
+ var index = require('./index-DVr0pLZy.js');
4
4
 
5
5
  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}";
6
6
 
@@ -277,6 +277,20 @@ const Autocomplete = class {
277
277
  };
278
278
  Autocomplete.style = pennlibsAutocompleteCss;
279
279
 
280
+ 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)}";
281
+
282
+ const NoImage = class {
283
+ constructor(hostRef) {
284
+ index.registerInstance(this, hostRef);
285
+ }
286
+ render() {
287
+ const shieldImg = index.getAssetPath('./assets/simplified-shield.webp');
288
+ return (index.h("img", { key: 'ba6cc227e90aac2e22b9997ad5c794fd4fd6d5ef', src: shieldImg, alt: "", class: "no-image__img" }));
289
+ }
290
+ static get assetsDirs() { return ["assets"]; }
291
+ };
292
+ NoImage.style = pennlibsFallbackImgCss;
293
+
280
294
  const pennlibsFooterCss = ":host {\n font-family: var(--pl-font-family);\n font-size: var(--pl-font-size);\n --padding-bottom: 5.75rem;\n}\n\n@media print {\n :host {\n display: none;\n }\n}\n\n*, *::before, *::after {\n box-sizing: border-box;\n}\n\n.website-footer-wrapper {\n background: var(--pl-color-penn-blue);\n padding-bottom: var(--padding-bottom);\n}\n\n.viewport-margins {\n max-width: var(--pl-viewport-margins-max-width);\n margin: 0 auto;\n padding: 0 var(--pl-viewport-margins-gutter, 1em);\n}\n\n.website-footer {\n background-size: cover;\n color: var(--pl-color-fg-subtle-on-emphasis);\n}\n\n.website-footer a {\n color: var(--pl-color-fg-subtle-on-emphasis);\n}\n\n.website-footer__content {\n padding: 4em 0;\n}\n\n.website-footer__links-container {\n gap: 4em 2em;\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(12em, 1fr));\n}\n\n.website-footer__heading {\n font-size: 0.8em;\n text-transform: uppercase;\n letter-spacing: 0.075em;\n margin-bottom: 1em;\n opacity: 0.75;\n}\n\n.website-footer__links {\n margin: 0;\n padding: 0;\n list-style: none;\n display: flex;\n flex-direction: column;\n gap: 0.75em;\n}\n\n.website-footer__links a {\n text-decoration: none;\n}\n\n.website-footer__links a:hover {\n text-decoration: underline;\n text-underline-offset: var(--pl-link-text-underline-offset);\n}\n\n.website-footer__footer {\n padding: 1em 0;\n background: rgba(1, 31, 91, 0.65);\n}\n\n.website-footer__links--footer {\n display: flex;\n flex-wrap: wrap;\n gap: 0;\n flex-direction: row;\n margin-left: -0.5em;\n\n @media (max-width: 1080px) {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(16em, 1fr));\n }\n}\n\n.website-footer__links--footer a {\n display: inline-block;\n padding: 0.5em 0.75em;\n font-size: 0.875em;\n opacity: 0.875;\n font-weight: 500;\n}\n\n.website-footer__links--footer a:hover {\n text-decoration: underline;\n text-underline-offset: var(--pl-link-text-underline-offset);\n}";
281
295
 
282
296
  const Footer = class {
@@ -376,7 +390,252 @@ const Header = class {
376
390
  };
377
391
  Header.style = pennlibsHeaderCss;
378
392
 
393
+ 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%}";
394
+
395
+ const PIXEL_DENSITIES = [1, 1.5, 2];
396
+ const IIIFImg = class {
397
+ constructor(hostRef) {
398
+ index.registerInstance(this, hostRef);
399
+ /**
400
+ * The IIIF [region](https://iiif.io/api/image/3.0/#41-region) of the image to display.
401
+ * Defines the rectangular portion of the underlying image to return.
402
+ *
403
+ * `full`: The full image is returned, without any cropping.
404
+ *
405
+ * `square`: A square area where width and height equal the shorter dimension.
406
+ *
407
+ * `width:height`: Any aspect ratio format (e.g., `16:9`, `4:3`, `3:2`, `21:9`) applies
408
+ * a centered crop based on the source image dimensions and sets the CSS aspect-ratio
409
+ * property for layout reservation.
410
+ *
411
+ * `x,y,w,h`: Absolute pixel coordinates (x, y position; w, h dimensions).
412
+ *
413
+ * `pct:x,y,w,h`: Percentage-based coordinates of full image dimensions.
414
+ *
415
+ * @default 'full'
416
+ */
417
+ this.region = 'full';
418
+ /**
419
+ * The IIIF [rotation](https://iiif.io/api/image/3.0/#44-rotation) to apply to the image.
420
+ * Specifies mirroring and clockwise rotation in degrees (0-360).
421
+ *
422
+ * `n`: Rotation in degrees only.
423
+ *
424
+ * `!n`: Mirror the image vertically, then rotate by n degrees.
425
+ *
426
+ * @default '0'
427
+ */
428
+ this.rotation = '0';
429
+ /**
430
+ * The IIIF [quality](https://iiif.io/api/image/3.0/#quality) of the image.
431
+ * Controls the color delivery mode.
432
+ *
433
+ * `default`: The server's default quality.
434
+ *
435
+ * `color`: Full color information.
436
+ *
437
+ * `gray`: Grayscale rendering.
438
+ *
439
+ * `bitonal`: Black and white only.
440
+ *
441
+ * @default 'default'
442
+ */
443
+ this.quality = 'default';
444
+ /**
445
+ * Native browser lazy loading behavior. Use "lazy" to defer loading until the image is near the viewport,
446
+ * or "eager" to load immediately.
447
+ * @default 'lazy'
448
+ */
449
+ this.loading = 'lazy';
450
+ /**
451
+ * Whether to display a fallback placeholder image when the IIIF image fails to load.
452
+ * @default true
453
+ */
454
+ this.showFallback = true;
455
+ this.isLoaded = false;
456
+ this.hasError = false;
457
+ this.baseUrl = 'https://iiif-images.library.upenn.edu/iiif/3';
458
+ this.handleLoad = () => {
459
+ this.isLoaded = true;
460
+ };
461
+ this.handleError = () => {
462
+ this.hasError = true;
463
+ };
464
+ }
465
+ /**
466
+ * Fetch IIIF image info to get actual dimensions.
467
+ */
468
+ async fetchImageInfo() {
469
+ try {
470
+ const currentUuid = this.uuid; // Capture current UUID
471
+ const infoUrl = `${this.baseUrl}/${currentUuid}/info.json`;
472
+ const response = await fetch(infoUrl);
473
+ if (!response.ok) {
474
+ console.error(`Failed to fetch IIIF info.json for ${currentUuid}`);
475
+ return;
476
+ }
477
+ const info = await response.json();
478
+ // Only update dimensions if UUID hasn't changed during fetch
479
+ // (protects against race conditions if component updates)
480
+ if (this.uuid === currentUuid) {
481
+ this.imageDimensions = {
482
+ width: info.width,
483
+ height: info.height,
484
+ };
485
+ this.fetchedUuid = currentUuid;
486
+ }
487
+ }
488
+ catch (error) {
489
+ console.error(`Error fetching IIIF image info for ${this.uuid}:`, error);
490
+ }
491
+ }
492
+ /**
493
+ * Check if a region value matches aspect ratio format (e.g., "16:9", "4:3", "3:2").
494
+ */
495
+ isAspectRatio(value) {
496
+ return /^\d+:\d+$/.test(value);
497
+ }
498
+ /**
499
+ * Set CSS custom property for aspect ratio on the host element.
500
+ */
501
+ setAspectRatioCss(aspectRatio) {
502
+ const [widthRatio, heightRatio] = aspectRatio.split(':');
503
+ this.hostElement.style.setProperty('--aspect-ratio', `${widthRatio} / ${heightRatio}`);
504
+ }
505
+ /**
506
+ * Clear CSS custom property for aspect ratio from the host element.
507
+ */
508
+ clearAspectRatioCss() {
509
+ this.hostElement.style.removeProperty('--aspect-ratio');
510
+ }
511
+ /**
512
+ * Format a number for IIIF percentage values (remove trailing zeros).
513
+ * IIIF spec requires no trailing zeros per section 4.7.
514
+ */
515
+ formatPercent(value) {
516
+ return value.toFixed(2).replace(/\.?0+$/, '');
517
+ }
518
+ /**
519
+ * Calculate a centered crop region for a given aspect ratio.
520
+ * Requires actual image dimensions to calculate correctly.
521
+ * @param aspectRatio - The target aspect ratio (e.g., "3:2" or "2:3")
522
+ * @returns A IIIF percentage-based region string (e.g., "pct:0,16.67,100,66.67")
523
+ */
524
+ calculateCenteredCrop(aspectRatio) {
525
+ if (!this.imageDimensions) {
526
+ throw new Error('Image dimensions required for aspect ratio calculation');
527
+ }
528
+ const [widthRatio, heightRatio] = aspectRatio.split(':').map(Number);
529
+ const targetRatio = widthRatio / heightRatio;
530
+ const sourceRatio = this.imageDimensions.width / this.imageDimensions.height;
531
+ // Determine if we need to crop width or height
532
+ if (sourceRatio > targetRatio) {
533
+ // Source is wider than target - crop left and right
534
+ const widthPercent = (targetRatio / sourceRatio) * 100;
535
+ const xOffset = (100 - widthPercent) / 2;
536
+ return `pct:${this.formatPercent(xOffset)},0,${this.formatPercent(widthPercent)},100`;
537
+ }
538
+ else {
539
+ // Source is taller than target - crop top and bottom
540
+ const heightPercent = (sourceRatio / targetRatio) * 100;
541
+ const yOffset = (100 - heightPercent) / 2;
542
+ return `pct:0,${this.formatPercent(yOffset)},100,${this.formatPercent(heightPercent)}`;
543
+ }
544
+ }
545
+ /**
546
+ * Get the region parameter for the IIIF URL.
547
+ */
548
+ getRegionParam() {
549
+ // Handle custom aspect ratios
550
+ let regionParam = this.region || 'full';
551
+ if (this.isAspectRatio(regionParam)) {
552
+ // Only apply crop if we have dimensions for the current UUID
553
+ if (this.imageDimensions && this.fetchedUuid === this.uuid) {
554
+ regionParam = this.calculateCenteredCrop(regionParam);
555
+ }
556
+ else {
557
+ // Use full image until dimensions are loaded
558
+ regionParam = 'full';
559
+ }
560
+ }
561
+ return regionParam;
562
+ }
563
+ buildIIIFUrl(width, format = 'jpg') {
564
+ const sizeParam = width ? `${width},` : 'max';
565
+ const regionParam = this.getRegionParam();
566
+ return `${this.baseUrl}/${this.uuid}/${regionParam}/${sizeParam}/${this.rotation}/${this.quality}.${format}`;
567
+ }
568
+ generateSrcset(baseWidth, format) {
569
+ return PIXEL_DENSITIES.map(density => {
570
+ const width = Math.round(baseWidth * density);
571
+ const url = this.buildIIIFUrl(width, format);
572
+ return `${url} ${density}x`;
573
+ }).join(', ');
574
+ }
575
+ handleRegionChange(newValue, oldValue) {
576
+ // Only process if region actually changed
577
+ if (newValue === oldValue) {
578
+ return;
579
+ }
580
+ // If new region is an aspect ratio, set CSS and fetch info
581
+ if (newValue && this.isAspectRatio(newValue)) {
582
+ this.setAspectRatioCss(newValue);
583
+ this.fetchImageInfo();
584
+ }
585
+ else {
586
+ // Clear aspect ratio CSS if no longer using aspect ratio format
587
+ this.clearAspectRatioCss();
588
+ }
589
+ }
590
+ componentDidLoad() {
591
+ // Set CSS aspect ratio and fetch image info if using aspect ratio format
592
+ if (this.region && this.isAspectRatio(this.region)) {
593
+ this.setAspectRatioCss(this.region);
594
+ this.fetchImageInfo();
595
+ }
596
+ if ('ResizeObserver' in window) {
597
+ this.resizeObserver = new ResizeObserver(entries => {
598
+ for (const entry of entries) {
599
+ this.observedWidth = Math.round(entry.contentRect.width);
600
+ if (this.resizeObserver) {
601
+ this.resizeObserver.disconnect();
602
+ this.resizeObserver = undefined;
603
+ }
604
+ }
605
+ });
606
+ this.resizeObserver.observe(this.hostElement);
607
+ }
608
+ }
609
+ disconnectedCallback() {
610
+ if (this.resizeObserver) {
611
+ this.resizeObserver.disconnect();
612
+ this.resizeObserver = undefined;
613
+ }
614
+ }
615
+ render() {
616
+ // Show placeholder until we've measured the component width
617
+ if (!this.observedWidth) {
618
+ return index.h("pennlibs-fallback-img", null);
619
+ }
620
+ if (this.hasError && this.showFallback) {
621
+ return (index.h("div", { class: "fallback-container" }, index.h("pennlibs-fallback-img", null)));
622
+ }
623
+ const imgClasses = {
624
+ 'iiif-img': true,
625
+ 'loaded': this.isLoaded
626
+ };
627
+ 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 })));
628
+ }
629
+ get hostElement() { return index.getElement(this); }
630
+ static get watchers() { return {
631
+ "region": ["handleRegionChange"]
632
+ }; }
633
+ };
634
+ IIIFImg.style = pennlibsIiifImgCss;
635
+
379
636
  exports.pennlibs_autocomplete = Autocomplete;
637
+ exports.pennlibs_fallback_img = NoImage;
380
638
  exports.pennlibs_footer = Footer;
381
639
  exports.pennlibs_header = Header;
382
- //# sourceMappingURL=pennlibs-autocomplete.pennlibs-footer.pennlibs-header.entry.cjs.js.map
640
+ exports.pennlibs_iiif_img = IIIFImg;
641
+ //# sourceMappingURL=pennlibs-autocomplete.pennlibs-fallback-img.pennlibs-footer.pennlibs-header.pennlibs-iiif-img.entry.cjs.js.map
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var index = require('./index-C0qvW4Ra.js');
3
+ var index = require('./index-DVr0pLZy.js');
4
4
 
5
5
  const pennlibsBannerCss = ":host{font-family:var(--pl-font-family);font-size:var(--pl-font-size);--max-width:1080px}.viewport-margins{max-width:calc(var(--max-width) + 0.5em);margin:0 auto;padding:0 var(--pl-viewport-margins-gutter, 1em)}.skip-to-content-link{position:absolute;transform:translateY(-300%);background:var(--pl-color-bg-default);left:0.5em;padding:0.5em 1em;margin-top:0.5em;position:absolute}.skip-to-content-link:focus{transform:translateY(0%);color:var(--pl-color-fg-default)}.universal-nav{background:var(--pl-color-penn-blue)}.universal-nav ul{display:flex;align-items:baseline;flex-wrap:wrap;scrollbar-color:var(--pl-color-penn-red);list-style:none;padding:0;margin:0}.universal-nav li{display:inline-block}.universal-nav a{display:inline-block;text-transform:uppercase;font-size:0.75em;letter-spacing:0.075em;font-weight:600;color:var(--pl-color-fg-subtle-on-emphasis);padding:0.5em;text-decoration:none}.universal-nav a:hover{text-decoration:underline;text-decoration-thickness:2px;text-underline-offset:.15em}.universal-nav__shield-image{vertical-align:sub;height:1em;padding-right:0.5em;height:auto}";
6
6
 
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var index = require('./index-C0qvW4Ra.js');
3
+ var index = require('./index-DVr0pLZy.js');
4
4
 
5
5
  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}";
6
6
 
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var index = require('./index-C0qvW4Ra.js');
3
+ var index = require('./index-DVr0pLZy.js');
4
4
 
5
5
  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}";
6
6
 
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var index = require('./index-C0qvW4Ra.js');
3
+ var index = require('./index-DVr0pLZy.js');
4
4
 
5
5
  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}}";
6
6
 
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var index = require('./index-C0qvW4Ra.js');
3
+ var index = require('./index-DVr0pLZy.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-iiif-img.cjs",[[257,"pennlibs-iiif-img",{"uuid":[1],"alt":[1],"region":[1],"rotation":[1],"quality":[1],"loading":[1],"showFallback":[4,"show-fallback"],"isLoaded":[32],"hasError":[32]}]]],["pennlibs-autocomplete_3.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-footer",{"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-feedback.cjs",[[257,"pennlibs-feedback",{"error":[32],"answer":[32]}]]],["pennlibs-hero.cjs",[[257,"pennlibs-hero",{"heroPictureElement":[32],"heroHeadingElement":[32],"heroParagraphElement":[32],"heroSrc":[32]}]]],["pennlibs-fallback-img.cjs",[[257,"pennlibs-fallback-img"]]]], options);
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);
22
22
  });
23
23
 
24
24
  exports.setNonce = index.setNonce;
@@ -2,6 +2,8 @@
2
2
  display: block;
3
3
  width: 100%;
4
4
  max-width: 100%;
5
+ align-self: flex-start;
6
+ aspect-ratio: var(--aspect-ratio, auto);
5
7
  }
6
8
 
7
9
  picture {
@@ -27,11 +29,16 @@ picture {
27
29
  .fallback-container {
28
30
  display: block;
29
31
  width: 100%;
30
- height: auto;
31
- min-height: 200px;
32
+ height: 100%;
32
33
  }
33
34
 
34
35
  .fallback-container pennlibs-fallback-img {
35
36
  width: 100%;
36
37
  height: 100%;
37
38
  }
39
+
40
+ pennlibs-fallback-img {
41
+ display: block;
42
+ width: 100%;
43
+ height: 100%;
44
+ }
@@ -1,10 +1,9 @@
1
1
  import { h } from "@stencil/core";
2
- const PIXEL_DENSITY_MULTIPLIERS = [0.5, 0.75, 1, 1.5, 2];
3
- const DEFAULT_IMAGE_SIZES = [400, 600, 800, 1200, 1600, 2400];
2
+ const PIXEL_DENSITIES = [1, 1.5, 2];
4
3
  /**
5
4
  * Display responsive, high-quality images from Penn Libraries' IIIF Image API 3.0 server.
6
- * Automatically generates optimal image sources for different viewport sizes and pixel densities,
7
- * with modern WebP format and JPG fallback support.
5
+ * Measures its own rendered width and automatically generates optimal image sources at
6
+ * multiple pixel densities (1x, 1.5x, 2x) with modern WebP format and JPG fallback support.
8
7
  *
9
8
  * @component
10
9
  * @example
@@ -23,6 +22,10 @@ export class IIIFImg {
23
22
  *
24
23
  * `square`: A square area where width and height equal the shorter dimension.
25
24
  *
25
+ * `width:height`: Any aspect ratio format (e.g., `16:9`, `4:3`, `3:2`, `21:9`) applies
26
+ * a centered crop based on the source image dimensions and sets the CSS aspect-ratio
27
+ * property for layout reservation.
28
+ *
26
29
  * `x,y,w,h`: Absolute pixel coordinates (x, y position; w, h dimensions).
27
30
  *
28
31
  * `pct:x,y,w,h`: Percentage-based coordinates of full image dimensions.
@@ -77,26 +80,137 @@ export class IIIFImg {
77
80
  this.hasError = true;
78
81
  };
79
82
  }
83
+ /**
84
+ * Fetch IIIF image info to get actual dimensions.
85
+ */
86
+ async fetchImageInfo() {
87
+ try {
88
+ const currentUuid = this.uuid; // Capture current UUID
89
+ const infoUrl = `${this.baseUrl}/${currentUuid}/info.json`;
90
+ const response = await fetch(infoUrl);
91
+ if (!response.ok) {
92
+ console.error(`Failed to fetch IIIF info.json for ${currentUuid}`);
93
+ return;
94
+ }
95
+ const info = await response.json();
96
+ // Only update dimensions if UUID hasn't changed during fetch
97
+ // (protects against race conditions if component updates)
98
+ if (this.uuid === currentUuid) {
99
+ this.imageDimensions = {
100
+ width: info.width,
101
+ height: info.height,
102
+ };
103
+ this.fetchedUuid = currentUuid;
104
+ }
105
+ }
106
+ catch (error) {
107
+ console.error(`Error fetching IIIF image info for ${this.uuid}:`, error);
108
+ }
109
+ }
110
+ /**
111
+ * Check if a region value matches aspect ratio format (e.g., "16:9", "4:3", "3:2").
112
+ */
113
+ isAspectRatio(value) {
114
+ return /^\d+:\d+$/.test(value);
115
+ }
116
+ /**
117
+ * Set CSS custom property for aspect ratio on the host element.
118
+ */
119
+ setAspectRatioCss(aspectRatio) {
120
+ const [widthRatio, heightRatio] = aspectRatio.split(':');
121
+ this.hostElement.style.setProperty('--aspect-ratio', `${widthRatio} / ${heightRatio}`);
122
+ }
123
+ /**
124
+ * Clear CSS custom property for aspect ratio from the host element.
125
+ */
126
+ clearAspectRatioCss() {
127
+ this.hostElement.style.removeProperty('--aspect-ratio');
128
+ }
129
+ /**
130
+ * Format a number for IIIF percentage values (remove trailing zeros).
131
+ * IIIF spec requires no trailing zeros per section 4.7.
132
+ */
133
+ formatPercent(value) {
134
+ return value.toFixed(2).replace(/\.?0+$/, '');
135
+ }
136
+ /**
137
+ * Calculate a centered crop region for a given aspect ratio.
138
+ * Requires actual image dimensions to calculate correctly.
139
+ * @param aspectRatio - The target aspect ratio (e.g., "3:2" or "2:3")
140
+ * @returns A IIIF percentage-based region string (e.g., "pct:0,16.67,100,66.67")
141
+ */
142
+ calculateCenteredCrop(aspectRatio) {
143
+ if (!this.imageDimensions) {
144
+ throw new Error('Image dimensions required for aspect ratio calculation');
145
+ }
146
+ const [widthRatio, heightRatio] = aspectRatio.split(':').map(Number);
147
+ const targetRatio = widthRatio / heightRatio;
148
+ const sourceRatio = this.imageDimensions.width / this.imageDimensions.height;
149
+ // Determine if we need to crop width or height
150
+ if (sourceRatio > targetRatio) {
151
+ // Source is wider than target - crop left and right
152
+ const widthPercent = (targetRatio / sourceRatio) * 100;
153
+ const xOffset = (100 - widthPercent) / 2;
154
+ return `pct:${this.formatPercent(xOffset)},0,${this.formatPercent(widthPercent)},100`;
155
+ }
156
+ else {
157
+ // Source is taller than target - crop top and bottom
158
+ const heightPercent = (sourceRatio / targetRatio) * 100;
159
+ const yOffset = (100 - heightPercent) / 2;
160
+ return `pct:0,${this.formatPercent(yOffset)},100,${this.formatPercent(heightPercent)}`;
161
+ }
162
+ }
163
+ /**
164
+ * Get the region parameter for the IIIF URL.
165
+ */
166
+ getRegionParam() {
167
+ // Handle custom aspect ratios
168
+ let regionParam = this.region || 'full';
169
+ if (this.isAspectRatio(regionParam)) {
170
+ // Only apply crop if we have dimensions for the current UUID
171
+ if (this.imageDimensions && this.fetchedUuid === this.uuid) {
172
+ regionParam = this.calculateCenteredCrop(regionParam);
173
+ }
174
+ else {
175
+ // Use full image until dimensions are loaded
176
+ regionParam = 'full';
177
+ }
178
+ }
179
+ return regionParam;
180
+ }
80
181
  buildIIIFUrl(width, format = 'jpg') {
81
182
  const sizeParam = width ? `${width},` : 'max';
82
- return `${this.baseUrl}/${this.uuid}/${this.region}/${sizeParam}/${this.rotation}/${this.quality}.${format}`;
183
+ const regionParam = this.getRegionParam();
184
+ return `${this.baseUrl}/${this.uuid}/${regionParam}/${sizeParam}/${this.rotation}/${this.quality}.${format}`;
83
185
  }
84
- generateSrcset(sizes, format) {
85
- const uniqueSizes = [...new Set(sizes)].sort((a, b) => a - b);
86
- const srcsetEntries = uniqueSizes.map(size => {
87
- const url = this.buildIIIFUrl(size, format);
88
- return `${url} ${size}w`;
89
- });
90
- return srcsetEntries.join(', ');
186
+ generateSrcset(baseWidth, format) {
187
+ return PIXEL_DENSITIES.map(density => {
188
+ const width = Math.round(baseWidth * density);
189
+ const url = this.buildIIIFUrl(width, format);
190
+ return `${url} ${density}x`;
191
+ }).join(', ');
91
192
  }
92
- getOptimalSizes() {
93
- if (this.observedWidth) {
94
- const baseSizes = PIXEL_DENSITY_MULTIPLIERS.map(multiplier => Math.round(this.observedWidth * multiplier));
95
- return baseSizes;
193
+ handleRegionChange(newValue, oldValue) {
194
+ // Only process if region actually changed
195
+ if (newValue === oldValue) {
196
+ return;
197
+ }
198
+ // If new region is an aspect ratio, set CSS and fetch info
199
+ if (newValue && this.isAspectRatio(newValue)) {
200
+ this.setAspectRatioCss(newValue);
201
+ this.fetchImageInfo();
202
+ }
203
+ else {
204
+ // Clear aspect ratio CSS if no longer using aspect ratio format
205
+ this.clearAspectRatioCss();
96
206
  }
97
- return [...DEFAULT_IMAGE_SIZES];
98
207
  }
99
208
  componentDidLoad() {
209
+ // Set CSS aspect ratio and fetch image info if using aspect ratio format
210
+ if (this.region && this.isAspectRatio(this.region)) {
211
+ this.setAspectRatioCss(this.region);
212
+ this.fetchImageInfo();
213
+ }
100
214
  if ('ResizeObserver' in window) {
101
215
  this.resizeObserver = new ResizeObserver(entries => {
102
216
  for (const entry of entries) {
@@ -105,8 +219,6 @@ export class IIIFImg {
105
219
  this.resizeObserver.disconnect();
106
220
  this.resizeObserver = undefined;
107
221
  }
108
- // Trigger re-render with optimized sizes
109
- this.isLoaded = this.isLoaded;
110
222
  }
111
223
  });
112
224
  this.resizeObserver.observe(this.hostElement);
@@ -119,7 +231,10 @@ export class IIIFImg {
119
231
  }
120
232
  }
121
233
  render() {
122
- const sizes = this.getOptimalSizes();
234
+ // Show placeholder until we've measured the component width
235
+ if (!this.observedWidth) {
236
+ return h("pennlibs-fallback-img", null);
237
+ }
123
238
  if (this.hasError && this.showFallback) {
124
239
  return (h("div", { class: "fallback-container" }, h("pennlibs-fallback-img", null)));
125
240
  }
@@ -127,8 +242,7 @@ export class IIIFImg {
127
242
  'iiif-img': true,
128
243
  'loaded': this.isLoaded
129
244
  };
130
- const defaultSize = sizes[0];
131
- 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 })));
245
+ return (h("picture", null, h("source", { type: "image/webp", srcSet: this.generateSrcset(this.observedWidth, 'webp') }), 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 })));
132
246
  }
133
247
  static get is() { return "pennlibs-iiif-img"; }
134
248
  static get encapsulation() { return "shadow"; }
@@ -197,11 +311,11 @@ export class IIIFImg {
197
311
  "name": "default",
198
312
  "text": "'full'"
199
313
  }],
200
- "text": "The IIIF [region](https://iiif.io/api/image/3.0/#41-region) of the image to display.\nDefines 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."
314
+ "text": "The IIIF [region](https://iiif.io/api/image/3.0/#41-region) of the image to display.\nDefines 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\na centered crop based on the source image dimensions and sets the CSS aspect-ratio\nproperty 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."
201
315
  },
202
316
  "getter": false,
203
317
  "setter": false,
204
- "reflect": false,
318
+ "reflect": true,
205
319
  "attribute": "region",
206
320
  "defaultValue": "'full'"
207
321
  },
@@ -302,9 +416,17 @@ export class IIIFImg {
302
416
  static get states() {
303
417
  return {
304
418
  "isLoaded": {},
305
- "hasError": {}
419
+ "hasError": {},
420
+ "imageDimensions": {},
421
+ "observedWidth": {}
306
422
  };
307
423
  }
308
424
  static get elementRef() { return "hostElement"; }
425
+ static get watchers() {
426
+ return [{
427
+ "propName": "region",
428
+ "methodName": "handleRegionChange"
429
+ }];
430
+ }
309
431
  }
310
432
  //# sourceMappingURL=pennlibs-iiif-img.js.map