@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
package/hydrate/index.js CHANGED
@@ -127,7 +127,7 @@ function hydrateFactory($stencilWindow, $stencilHydrateOpts, $stencilHydrateResu
127
127
 
128
128
 
129
129
  const NAMESPACE = 'web';
130
- const BUILD = /* web */ { hydratedSelectorName: "hydrated", propChangeCallback: false, slotRelocation: true, updatable: true};
130
+ const BUILD = /* web */ { hydratedSelectorName: "hydrated", slotRelocation: true, updatable: true};
131
131
 
132
132
  /*
133
133
  Stencil Hydrate Platform v4.38.1 | MIT Licensed | https://stenciljs.com
@@ -2105,6 +2105,14 @@ var renderVdom = (hostRef, renderFnResults, isInitialLoad = false) => {
2105
2105
  const isHostElement = isHost(renderFnResults);
2106
2106
  const rootVnode = isHostElement ? renderFnResults : h(null, null, renderFnResults);
2107
2107
  hostTagName = hostElm.tagName;
2108
+ if (cmpMeta.$attrsToReflect$) {
2109
+ rootVnode.$attrs$ = rootVnode.$attrs$ || {};
2110
+ cmpMeta.$attrsToReflect$.forEach(([propName, attribute]) => {
2111
+ {
2112
+ rootVnode.$attrs$[attribute] = hostElm[propName];
2113
+ }
2114
+ });
2115
+ }
2108
2116
  if (isInitialLoad && rootVnode.$attrs$) {
2109
2117
  for (const key of Object.keys(rootVnode.$attrs$)) {
2110
2118
  if (hostElm.hasAttribute(key) && !["key", "ref", "style", "class"].includes(key)) {
@@ -2406,6 +2414,7 @@ var setValue = (ref, propName, newVal, cmpMeta) => {
2406
2414
  `Couldn't find host element for "${cmpMeta.$tagName$}" as it is unknown to this Stencil runtime. This usually happens when integrating a 3rd party Stencil component with another Stencil component or application. Please reach out to the maintainers of the 3rd party Stencil component or report this on the Stencil Discord server (https://chat.stenciljs.com) or comment on this similar [GitHub issue](https://github.com/stenciljs/core/issues/5457).`
2407
2415
  );
2408
2416
  }
2417
+ const elm = hostRef.$hostElement$ ;
2409
2418
  const oldVal = hostRef.$instanceValues$.get(propName);
2410
2419
  const flags = hostRef.$flags$;
2411
2420
  const instance = hostRef.$lazyInstance$ ;
@@ -2417,6 +2426,18 @@ var setValue = (ref, propName, newVal, cmpMeta) => {
2417
2426
  if ((!(flags & 8 /* isConstructingInstance */) || oldVal === void 0) && didValueChange) {
2418
2427
  hostRef.$instanceValues$.set(propName, newVal);
2419
2428
  if (instance) {
2429
+ if (cmpMeta.$watchers$ && flags & 128 /* isWatchReady */) {
2430
+ const watchMethods = cmpMeta.$watchers$[propName];
2431
+ if (watchMethods) {
2432
+ watchMethods.map((watchMethodName) => {
2433
+ try {
2434
+ instance[watchMethodName](newVal, oldVal, propName);
2435
+ } catch (e) {
2436
+ consoleError(e, elm);
2437
+ }
2438
+ });
2439
+ }
2440
+ }
2420
2441
  if ((flags & (2 /* hasRendered */ | 16 /* isQueuedForUpdate */)) === 2 /* hasRendered */) {
2421
2442
  if (instance.componentShouldUpdate) {
2422
2443
  if (instance.componentShouldUpdate(newVal, oldVal, propName) === false) {
@@ -2433,7 +2454,18 @@ var setValue = (ref, propName, newVal, cmpMeta) => {
2433
2454
  var proxyComponent = (Cstr, cmpMeta, flags) => {
2434
2455
  var _a;
2435
2456
  const prototype = Cstr.prototype;
2436
- if (cmpMeta.$members$ || BUILD.propChangeCallback) {
2457
+ {
2458
+ {
2459
+ if (Cstr.watchers && !cmpMeta.$watchers$) {
2460
+ cmpMeta.$watchers$ = Cstr.watchers;
2461
+ }
2462
+ if (Cstr.deserializers && !cmpMeta.$deserializers$) {
2463
+ cmpMeta.$deserializers$ = Cstr.deserializers;
2464
+ }
2465
+ if (Cstr.serializers && !cmpMeta.$serializers$) {
2466
+ cmpMeta.$serializers$ = Cstr.serializers;
2467
+ }
2468
+ }
2437
2469
  const members = Object.entries((_a = cmpMeta.$members$) != null ? _a : {});
2438
2470
  members.map(([memberName, [memberFlags]]) => {
2439
2471
  if ((memberFlags & 31 /* Prop */ || memberFlags & 32 /* State */)) {
@@ -2510,6 +2542,11 @@ var initializeComponent = async (elm, hostRef, cmpMeta, hmrVersionId) => {
2510
2542
  throw new Error(`Constructor for "${cmpMeta.$tagName$}#${hostRef.$modeName$}" was not found`);
2511
2543
  }
2512
2544
  if (!Cstr.isProxied) {
2545
+ {
2546
+ cmpMeta.$watchers$ = Cstr.watchers;
2547
+ cmpMeta.$serializers$ = Cstr.serializers;
2548
+ cmpMeta.$deserializers$ = Cstr.deserializers;
2549
+ }
2513
2550
  proxyComponent(Cstr, cmpMeta);
2514
2551
  Cstr.isProxied = true;
2515
2552
  }
@@ -2525,6 +2562,9 @@ var initializeComponent = async (elm, hostRef, cmpMeta, hmrVersionId) => {
2525
2562
  {
2526
2563
  hostRef.$flags$ &= -9 /* isConstructingInstance */;
2527
2564
  }
2565
+ {
2566
+ hostRef.$flags$ |= 128 /* isWatchReady */;
2567
+ }
2528
2568
  endNewInstance();
2529
2569
  fireConnectedCallback(hostRef.$lazyInstance$, elm);
2530
2570
  } else {
@@ -3875,14 +3915,13 @@ class Hero {
3875
3915
  }; }
3876
3916
  }
3877
3917
 
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%}";
3918
+ 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%}";
3879
3919
 
3880
- const PIXEL_DENSITY_MULTIPLIERS = [0.5, 0.75, 1, 1.5, 2];
3881
- const DEFAULT_IMAGE_SIZES = [400, 600, 800, 1200, 1600, 2400];
3920
+ const PIXEL_DENSITIES = [1, 1.5, 2];
3882
3921
  /**
3883
3922
  * 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.
3923
+ * Measures its own rendered width and automatically generates optimal image sources at
3924
+ * multiple pixel densities (1x, 1.5x, 2x) with modern WebP format and JPG fallback support.
3886
3925
  *
3887
3926
  * @component
3888
3927
  * @example
@@ -3902,6 +3941,10 @@ class IIIFImg {
3902
3941
  *
3903
3942
  * `square`: A square area where width and height equal the shorter dimension.
3904
3943
  *
3944
+ * `width:height`: Any aspect ratio format (e.g., `16:9`, `4:3`, `3:2`, `21:9`) applies
3945
+ * a centered crop based on the source image dimensions and sets the CSS aspect-ratio
3946
+ * property for layout reservation.
3947
+ *
3905
3948
  * `x,y,w,h`: Absolute pixel coordinates (x, y position; w, h dimensions).
3906
3949
  *
3907
3950
  * `pct:x,y,w,h`: Percentage-based coordinates of full image dimensions.
@@ -3956,26 +3999,137 @@ class IIIFImg {
3956
3999
  this.hasError = true;
3957
4000
  };
3958
4001
  }
4002
+ /**
4003
+ * Fetch IIIF image info to get actual dimensions.
4004
+ */
4005
+ async fetchImageInfo() {
4006
+ try {
4007
+ const currentUuid = this.uuid; // Capture current UUID
4008
+ const infoUrl = `${this.baseUrl}/${currentUuid}/info.json`;
4009
+ const response = await fetch(infoUrl);
4010
+ if (!response.ok) {
4011
+ console.error(`Failed to fetch IIIF info.json for ${currentUuid}`);
4012
+ return;
4013
+ }
4014
+ const info = await response.json();
4015
+ // Only update dimensions if UUID hasn't changed during fetch
4016
+ // (protects against race conditions if component updates)
4017
+ if (this.uuid === currentUuid) {
4018
+ this.imageDimensions = {
4019
+ width: info.width,
4020
+ height: info.height,
4021
+ };
4022
+ this.fetchedUuid = currentUuid;
4023
+ }
4024
+ }
4025
+ catch (error) {
4026
+ console.error(`Error fetching IIIF image info for ${this.uuid}:`, error);
4027
+ }
4028
+ }
4029
+ /**
4030
+ * Check if a region value matches aspect ratio format (e.g., "16:9", "4:3", "3:2").
4031
+ */
4032
+ isAspectRatio(value) {
4033
+ return /^\d+:\d+$/.test(value);
4034
+ }
4035
+ /**
4036
+ * Set CSS custom property for aspect ratio on the host element.
4037
+ */
4038
+ setAspectRatioCss(aspectRatio) {
4039
+ const [widthRatio, heightRatio] = aspectRatio.split(':');
4040
+ this.hostElement.style.setProperty('--aspect-ratio', `${widthRatio} / ${heightRatio}`);
4041
+ }
4042
+ /**
4043
+ * Clear CSS custom property for aspect ratio from the host element.
4044
+ */
4045
+ clearAspectRatioCss() {
4046
+ this.hostElement.style.removeProperty('--aspect-ratio');
4047
+ }
4048
+ /**
4049
+ * Format a number for IIIF percentage values (remove trailing zeros).
4050
+ * IIIF spec requires no trailing zeros per section 4.7.
4051
+ */
4052
+ formatPercent(value) {
4053
+ return value.toFixed(2).replace(/\.?0+$/, '');
4054
+ }
4055
+ /**
4056
+ * Calculate a centered crop region for a given aspect ratio.
4057
+ * Requires actual image dimensions to calculate correctly.
4058
+ * @param aspectRatio - The target aspect ratio (e.g., "3:2" or "2:3")
4059
+ * @returns A IIIF percentage-based region string (e.g., "pct:0,16.67,100,66.67")
4060
+ */
4061
+ calculateCenteredCrop(aspectRatio) {
4062
+ if (!this.imageDimensions) {
4063
+ throw new Error('Image dimensions required for aspect ratio calculation');
4064
+ }
4065
+ const [widthRatio, heightRatio] = aspectRatio.split(':').map(Number);
4066
+ const targetRatio = widthRatio / heightRatio;
4067
+ const sourceRatio = this.imageDimensions.width / this.imageDimensions.height;
4068
+ // Determine if we need to crop width or height
4069
+ if (sourceRatio > targetRatio) {
4070
+ // Source is wider than target - crop left and right
4071
+ const widthPercent = (targetRatio / sourceRatio) * 100;
4072
+ const xOffset = (100 - widthPercent) / 2;
4073
+ return `pct:${this.formatPercent(xOffset)},0,${this.formatPercent(widthPercent)},100`;
4074
+ }
4075
+ else {
4076
+ // Source is taller than target - crop top and bottom
4077
+ const heightPercent = (sourceRatio / targetRatio) * 100;
4078
+ const yOffset = (100 - heightPercent) / 2;
4079
+ return `pct:0,${this.formatPercent(yOffset)},100,${this.formatPercent(heightPercent)}`;
4080
+ }
4081
+ }
4082
+ /**
4083
+ * Get the region parameter for the IIIF URL.
4084
+ */
4085
+ getRegionParam() {
4086
+ // Handle custom aspect ratios
4087
+ let regionParam = this.region || 'full';
4088
+ if (this.isAspectRatio(regionParam)) {
4089
+ // Only apply crop if we have dimensions for the current UUID
4090
+ if (this.imageDimensions && this.fetchedUuid === this.uuid) {
4091
+ regionParam = this.calculateCenteredCrop(regionParam);
4092
+ }
4093
+ else {
4094
+ // Use full image until dimensions are loaded
4095
+ regionParam = 'full';
4096
+ }
4097
+ }
4098
+ return regionParam;
4099
+ }
3959
4100
  buildIIIFUrl(width, format = 'jpg') {
3960
4101
  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;
4102
+ const regionParam = this.getRegionParam();
4103
+ return `${this.baseUrl}/${this.uuid}/${regionParam}/${sizeParam}/${this.rotation}/${this.quality}.${format}`;
4104
+ }
4105
+ generateSrcset(baseWidth, format) {
4106
+ return PIXEL_DENSITIES.map(density => {
4107
+ const width = Math.round(baseWidth * density);
4108
+ const url = this.buildIIIFUrl(width, format);
4109
+ return `${url} ${density}x`;
4110
+ }).join(', ');
4111
+ }
4112
+ handleRegionChange(newValue, oldValue) {
4113
+ // Only process if region actually changed
4114
+ if (newValue === oldValue) {
4115
+ return;
4116
+ }
4117
+ // If new region is an aspect ratio, set CSS and fetch info
4118
+ if (newValue && this.isAspectRatio(newValue)) {
4119
+ this.setAspectRatioCss(newValue);
4120
+ this.fetchImageInfo();
4121
+ }
4122
+ else {
4123
+ // Clear aspect ratio CSS if no longer using aspect ratio format
4124
+ this.clearAspectRatioCss();
3975
4125
  }
3976
- return [...DEFAULT_IMAGE_SIZES];
3977
4126
  }
3978
4127
  componentDidLoad() {
4128
+ // Set CSS aspect ratio and fetch image info if using aspect ratio format
4129
+ if (this.region && this.isAspectRatio(this.region)) {
4130
+ this.setAspectRatioCss(this.region);
4131
+ this.fetchImageInfo();
4132
+ }
3979
4133
  if ('ResizeObserver' in window) {
3980
4134
  this.resizeObserver = new ResizeObserver(entries => {
3981
4135
  for (const entry of entries) {
@@ -3984,8 +4138,6 @@ class IIIFImg {
3984
4138
  this.resizeObserver.disconnect();
3985
4139
  this.resizeObserver = undefined;
3986
4140
  }
3987
- // Trigger re-render with optimized sizes
3988
- this.isLoaded = this.isLoaded;
3989
4141
  }
3990
4142
  });
3991
4143
  this.resizeObserver.observe(this.hostElement);
@@ -3998,7 +4150,10 @@ class IIIFImg {
3998
4150
  }
3999
4151
  }
4000
4152
  render() {
4001
- const sizes = this.getOptimalSizes();
4153
+ // Show placeholder until we've measured the component width
4154
+ if (!this.observedWidth) {
4155
+ return hAsync("pennlibs-fallback-img", null);
4156
+ }
4002
4157
  if (this.hasError && this.showFallback) {
4003
4158
  return (hAsync("div", { class: "fallback-container" }, hAsync("pennlibs-fallback-img", null)));
4004
4159
  }
@@ -4006,10 +4161,12 @@ class IIIFImg {
4006
4161
  'iiif-img': true,
4007
4162
  'loaded': this.isLoaded
4008
4163
  };
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 })));
4164
+ return (hAsync("picture", null, hAsync("source", { type: "image/webp", srcSet: this.generateSrcset(this.observedWidth, 'webp') }), hAsync("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 })));
4011
4165
  }
4012
4166
  get hostElement() { return getElement(this); }
4167
+ static get watchers() { return {
4168
+ "region": ["handleRegionChange"]
4169
+ }; }
4013
4170
  static get style() { return pennlibsIiifImgCss; }
4014
4171
  static get cmpMeta() { return {
4015
4172
  "$flags$": 265,
@@ -4017,17 +4174,19 @@ class IIIFImg {
4017
4174
  "$members$": {
4018
4175
  "uuid": [1],
4019
4176
  "alt": [1],
4020
- "region": [1],
4177
+ "region": [513],
4021
4178
  "rotation": [1],
4022
4179
  "quality": [1],
4023
4180
  "loading": [1],
4024
4181
  "showFallback": [4, "show-fallback"],
4025
4182
  "isLoaded": [32],
4026
- "hasError": [32]
4183
+ "hasError": [32],
4184
+ "imageDimensions": [32],
4185
+ "observedWidth": [32]
4027
4186
  },
4028
4187
  "$listeners$": undefined,
4029
4188
  "$lazyBundleId$": "-",
4030
- "$attrsToReflect$": []
4189
+ "$attrsToReflect$": [["region", "region"]]
4031
4190
  }; }
4032
4191
  }
4033
4192
 
package/hydrate/index.mjs CHANGED
@@ -125,7 +125,7 @@ function hydrateFactory($stencilWindow, $stencilHydrateOpts, $stencilHydrateResu
125
125
 
126
126
 
127
127
  const NAMESPACE = 'web';
128
- const BUILD = /* web */ { hydratedSelectorName: "hydrated", propChangeCallback: false, slotRelocation: true, updatable: true};
128
+ const BUILD = /* web */ { hydratedSelectorName: "hydrated", slotRelocation: true, updatable: true};
129
129
 
130
130
  /*
131
131
  Stencil Hydrate Platform v4.38.1 | MIT Licensed | https://stenciljs.com
@@ -2103,6 +2103,14 @@ var renderVdom = (hostRef, renderFnResults, isInitialLoad = false) => {
2103
2103
  const isHostElement = isHost(renderFnResults);
2104
2104
  const rootVnode = isHostElement ? renderFnResults : h(null, null, renderFnResults);
2105
2105
  hostTagName = hostElm.tagName;
2106
+ if (cmpMeta.$attrsToReflect$) {
2107
+ rootVnode.$attrs$ = rootVnode.$attrs$ || {};
2108
+ cmpMeta.$attrsToReflect$.forEach(([propName, attribute]) => {
2109
+ {
2110
+ rootVnode.$attrs$[attribute] = hostElm[propName];
2111
+ }
2112
+ });
2113
+ }
2106
2114
  if (isInitialLoad && rootVnode.$attrs$) {
2107
2115
  for (const key of Object.keys(rootVnode.$attrs$)) {
2108
2116
  if (hostElm.hasAttribute(key) && !["key", "ref", "style", "class"].includes(key)) {
@@ -2404,6 +2412,7 @@ var setValue = (ref, propName, newVal, cmpMeta) => {
2404
2412
  `Couldn't find host element for "${cmpMeta.$tagName$}" as it is unknown to this Stencil runtime. This usually happens when integrating a 3rd party Stencil component with another Stencil component or application. Please reach out to the maintainers of the 3rd party Stencil component or report this on the Stencil Discord server (https://chat.stenciljs.com) or comment on this similar [GitHub issue](https://github.com/stenciljs/core/issues/5457).`
2405
2413
  );
2406
2414
  }
2415
+ const elm = hostRef.$hostElement$ ;
2407
2416
  const oldVal = hostRef.$instanceValues$.get(propName);
2408
2417
  const flags = hostRef.$flags$;
2409
2418
  const instance = hostRef.$lazyInstance$ ;
@@ -2415,6 +2424,18 @@ var setValue = (ref, propName, newVal, cmpMeta) => {
2415
2424
  if ((!(flags & 8 /* isConstructingInstance */) || oldVal === void 0) && didValueChange) {
2416
2425
  hostRef.$instanceValues$.set(propName, newVal);
2417
2426
  if (instance) {
2427
+ if (cmpMeta.$watchers$ && flags & 128 /* isWatchReady */) {
2428
+ const watchMethods = cmpMeta.$watchers$[propName];
2429
+ if (watchMethods) {
2430
+ watchMethods.map((watchMethodName) => {
2431
+ try {
2432
+ instance[watchMethodName](newVal, oldVal, propName);
2433
+ } catch (e) {
2434
+ consoleError(e, elm);
2435
+ }
2436
+ });
2437
+ }
2438
+ }
2418
2439
  if ((flags & (2 /* hasRendered */ | 16 /* isQueuedForUpdate */)) === 2 /* hasRendered */) {
2419
2440
  if (instance.componentShouldUpdate) {
2420
2441
  if (instance.componentShouldUpdate(newVal, oldVal, propName) === false) {
@@ -2431,7 +2452,18 @@ var setValue = (ref, propName, newVal, cmpMeta) => {
2431
2452
  var proxyComponent = (Cstr, cmpMeta, flags) => {
2432
2453
  var _a;
2433
2454
  const prototype = Cstr.prototype;
2434
- if (cmpMeta.$members$ || BUILD.propChangeCallback) {
2455
+ {
2456
+ {
2457
+ if (Cstr.watchers && !cmpMeta.$watchers$) {
2458
+ cmpMeta.$watchers$ = Cstr.watchers;
2459
+ }
2460
+ if (Cstr.deserializers && !cmpMeta.$deserializers$) {
2461
+ cmpMeta.$deserializers$ = Cstr.deserializers;
2462
+ }
2463
+ if (Cstr.serializers && !cmpMeta.$serializers$) {
2464
+ cmpMeta.$serializers$ = Cstr.serializers;
2465
+ }
2466
+ }
2435
2467
  const members = Object.entries((_a = cmpMeta.$members$) != null ? _a : {});
2436
2468
  members.map(([memberName, [memberFlags]]) => {
2437
2469
  if ((memberFlags & 31 /* Prop */ || memberFlags & 32 /* State */)) {
@@ -2508,6 +2540,11 @@ var initializeComponent = async (elm, hostRef, cmpMeta, hmrVersionId) => {
2508
2540
  throw new Error(`Constructor for "${cmpMeta.$tagName$}#${hostRef.$modeName$}" was not found`);
2509
2541
  }
2510
2542
  if (!Cstr.isProxied) {
2543
+ {
2544
+ cmpMeta.$watchers$ = Cstr.watchers;
2545
+ cmpMeta.$serializers$ = Cstr.serializers;
2546
+ cmpMeta.$deserializers$ = Cstr.deserializers;
2547
+ }
2511
2548
  proxyComponent(Cstr, cmpMeta);
2512
2549
  Cstr.isProxied = true;
2513
2550
  }
@@ -2523,6 +2560,9 @@ var initializeComponent = async (elm, hostRef, cmpMeta, hmrVersionId) => {
2523
2560
  {
2524
2561
  hostRef.$flags$ &= -9 /* isConstructingInstance */;
2525
2562
  }
2563
+ {
2564
+ hostRef.$flags$ |= 128 /* isWatchReady */;
2565
+ }
2526
2566
  endNewInstance();
2527
2567
  fireConnectedCallback(hostRef.$lazyInstance$, elm);
2528
2568
  } else {
@@ -3873,14 +3913,13 @@ class Hero {
3873
3913
  }; }
3874
3914
  }
3875
3915
 
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%}";
3916
+ 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%}";
3877
3917
 
3878
- const PIXEL_DENSITY_MULTIPLIERS = [0.5, 0.75, 1, 1.5, 2];
3879
- const DEFAULT_IMAGE_SIZES = [400, 600, 800, 1200, 1600, 2400];
3918
+ const PIXEL_DENSITIES = [1, 1.5, 2];
3880
3919
  /**
3881
3920
  * 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.
3921
+ * Measures its own rendered width and automatically generates optimal image sources at
3922
+ * multiple pixel densities (1x, 1.5x, 2x) with modern WebP format and JPG fallback support.
3884
3923
  *
3885
3924
  * @component
3886
3925
  * @example
@@ -3900,6 +3939,10 @@ class IIIFImg {
3900
3939
  *
3901
3940
  * `square`: A square area where width and height equal the shorter dimension.
3902
3941
  *
3942
+ * `width:height`: Any aspect ratio format (e.g., `16:9`, `4:3`, `3:2`, `21:9`) applies
3943
+ * a centered crop based on the source image dimensions and sets the CSS aspect-ratio
3944
+ * property for layout reservation.
3945
+ *
3903
3946
  * `x,y,w,h`: Absolute pixel coordinates (x, y position; w, h dimensions).
3904
3947
  *
3905
3948
  * `pct:x,y,w,h`: Percentage-based coordinates of full image dimensions.
@@ -3954,26 +3997,137 @@ class IIIFImg {
3954
3997
  this.hasError = true;
3955
3998
  };
3956
3999
  }
4000
+ /**
4001
+ * Fetch IIIF image info to get actual dimensions.
4002
+ */
4003
+ async fetchImageInfo() {
4004
+ try {
4005
+ const currentUuid = this.uuid; // Capture current UUID
4006
+ const infoUrl = `${this.baseUrl}/${currentUuid}/info.json`;
4007
+ const response = await fetch(infoUrl);
4008
+ if (!response.ok) {
4009
+ console.error(`Failed to fetch IIIF info.json for ${currentUuid}`);
4010
+ return;
4011
+ }
4012
+ const info = await response.json();
4013
+ // Only update dimensions if UUID hasn't changed during fetch
4014
+ // (protects against race conditions if component updates)
4015
+ if (this.uuid === currentUuid) {
4016
+ this.imageDimensions = {
4017
+ width: info.width,
4018
+ height: info.height,
4019
+ };
4020
+ this.fetchedUuid = currentUuid;
4021
+ }
4022
+ }
4023
+ catch (error) {
4024
+ console.error(`Error fetching IIIF image info for ${this.uuid}:`, error);
4025
+ }
4026
+ }
4027
+ /**
4028
+ * Check if a region value matches aspect ratio format (e.g., "16:9", "4:3", "3:2").
4029
+ */
4030
+ isAspectRatio(value) {
4031
+ return /^\d+:\d+$/.test(value);
4032
+ }
4033
+ /**
4034
+ * Set CSS custom property for aspect ratio on the host element.
4035
+ */
4036
+ setAspectRatioCss(aspectRatio) {
4037
+ const [widthRatio, heightRatio] = aspectRatio.split(':');
4038
+ this.hostElement.style.setProperty('--aspect-ratio', `${widthRatio} / ${heightRatio}`);
4039
+ }
4040
+ /**
4041
+ * Clear CSS custom property for aspect ratio from the host element.
4042
+ */
4043
+ clearAspectRatioCss() {
4044
+ this.hostElement.style.removeProperty('--aspect-ratio');
4045
+ }
4046
+ /**
4047
+ * Format a number for IIIF percentage values (remove trailing zeros).
4048
+ * IIIF spec requires no trailing zeros per section 4.7.
4049
+ */
4050
+ formatPercent(value) {
4051
+ return value.toFixed(2).replace(/\.?0+$/, '');
4052
+ }
4053
+ /**
4054
+ * Calculate a centered crop region for a given aspect ratio.
4055
+ * Requires actual image dimensions to calculate correctly.
4056
+ * @param aspectRatio - The target aspect ratio (e.g., "3:2" or "2:3")
4057
+ * @returns A IIIF percentage-based region string (e.g., "pct:0,16.67,100,66.67")
4058
+ */
4059
+ calculateCenteredCrop(aspectRatio) {
4060
+ if (!this.imageDimensions) {
4061
+ throw new Error('Image dimensions required for aspect ratio calculation');
4062
+ }
4063
+ const [widthRatio, heightRatio] = aspectRatio.split(':').map(Number);
4064
+ const targetRatio = widthRatio / heightRatio;
4065
+ const sourceRatio = this.imageDimensions.width / this.imageDimensions.height;
4066
+ // Determine if we need to crop width or height
4067
+ if (sourceRatio > targetRatio) {
4068
+ // Source is wider than target - crop left and right
4069
+ const widthPercent = (targetRatio / sourceRatio) * 100;
4070
+ const xOffset = (100 - widthPercent) / 2;
4071
+ return `pct:${this.formatPercent(xOffset)},0,${this.formatPercent(widthPercent)},100`;
4072
+ }
4073
+ else {
4074
+ // Source is taller than target - crop top and bottom
4075
+ const heightPercent = (sourceRatio / targetRatio) * 100;
4076
+ const yOffset = (100 - heightPercent) / 2;
4077
+ return `pct:0,${this.formatPercent(yOffset)},100,${this.formatPercent(heightPercent)}`;
4078
+ }
4079
+ }
4080
+ /**
4081
+ * Get the region parameter for the IIIF URL.
4082
+ */
4083
+ getRegionParam() {
4084
+ // Handle custom aspect ratios
4085
+ let regionParam = this.region || 'full';
4086
+ if (this.isAspectRatio(regionParam)) {
4087
+ // Only apply crop if we have dimensions for the current UUID
4088
+ if (this.imageDimensions && this.fetchedUuid === this.uuid) {
4089
+ regionParam = this.calculateCenteredCrop(regionParam);
4090
+ }
4091
+ else {
4092
+ // Use full image until dimensions are loaded
4093
+ regionParam = 'full';
4094
+ }
4095
+ }
4096
+ return regionParam;
4097
+ }
3957
4098
  buildIIIFUrl(width, format = 'jpg') {
3958
4099
  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;
4100
+ const regionParam = this.getRegionParam();
4101
+ return `${this.baseUrl}/${this.uuid}/${regionParam}/${sizeParam}/${this.rotation}/${this.quality}.${format}`;
4102
+ }
4103
+ generateSrcset(baseWidth, format) {
4104
+ return PIXEL_DENSITIES.map(density => {
4105
+ const width = Math.round(baseWidth * density);
4106
+ const url = this.buildIIIFUrl(width, format);
4107
+ return `${url} ${density}x`;
4108
+ }).join(', ');
4109
+ }
4110
+ handleRegionChange(newValue, oldValue) {
4111
+ // Only process if region actually changed
4112
+ if (newValue === oldValue) {
4113
+ return;
4114
+ }
4115
+ // If new region is an aspect ratio, set CSS and fetch info
4116
+ if (newValue && this.isAspectRatio(newValue)) {
4117
+ this.setAspectRatioCss(newValue);
4118
+ this.fetchImageInfo();
4119
+ }
4120
+ else {
4121
+ // Clear aspect ratio CSS if no longer using aspect ratio format
4122
+ this.clearAspectRatioCss();
3973
4123
  }
3974
- return [...DEFAULT_IMAGE_SIZES];
3975
4124
  }
3976
4125
  componentDidLoad() {
4126
+ // Set CSS aspect ratio and fetch image info if using aspect ratio format
4127
+ if (this.region && this.isAspectRatio(this.region)) {
4128
+ this.setAspectRatioCss(this.region);
4129
+ this.fetchImageInfo();
4130
+ }
3977
4131
  if ('ResizeObserver' in window) {
3978
4132
  this.resizeObserver = new ResizeObserver(entries => {
3979
4133
  for (const entry of entries) {
@@ -3982,8 +4136,6 @@ class IIIFImg {
3982
4136
  this.resizeObserver.disconnect();
3983
4137
  this.resizeObserver = undefined;
3984
4138
  }
3985
- // Trigger re-render with optimized sizes
3986
- this.isLoaded = this.isLoaded;
3987
4139
  }
3988
4140
  });
3989
4141
  this.resizeObserver.observe(this.hostElement);
@@ -3996,7 +4148,10 @@ class IIIFImg {
3996
4148
  }
3997
4149
  }
3998
4150
  render() {
3999
- const sizes = this.getOptimalSizes();
4151
+ // Show placeholder until we've measured the component width
4152
+ if (!this.observedWidth) {
4153
+ return hAsync("pennlibs-fallback-img", null);
4154
+ }
4000
4155
  if (this.hasError && this.showFallback) {
4001
4156
  return (hAsync("div", { class: "fallback-container" }, hAsync("pennlibs-fallback-img", null)));
4002
4157
  }
@@ -4004,10 +4159,12 @@ class IIIFImg {
4004
4159
  'iiif-img': true,
4005
4160
  'loaded': this.isLoaded
4006
4161
  };
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 })));
4162
+ return (hAsync("picture", null, hAsync("source", { type: "image/webp", srcSet: this.generateSrcset(this.observedWidth, 'webp') }), hAsync("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 })));
4009
4163
  }
4010
4164
  get hostElement() { return getElement(this); }
4165
+ static get watchers() { return {
4166
+ "region": ["handleRegionChange"]
4167
+ }; }
4011
4168
  static get style() { return pennlibsIiifImgCss; }
4012
4169
  static get cmpMeta() { return {
4013
4170
  "$flags$": 265,
@@ -4015,17 +4172,19 @@ class IIIFImg {
4015
4172
  "$members$": {
4016
4173
  "uuid": [1],
4017
4174
  "alt": [1],
4018
- "region": [1],
4175
+ "region": [513],
4019
4176
  "rotation": [1],
4020
4177
  "quality": [1],
4021
4178
  "loading": [1],
4022
4179
  "showFallback": [4, "show-fallback"],
4023
4180
  "isLoaded": [32],
4024
- "hasError": [32]
4181
+ "hasError": [32],
4182
+ "imageDimensions": [32],
4183
+ "observedWidth": [32]
4025
4184
  },
4026
4185
  "$listeners$": undefined,
4027
4186
  "$lazyBundleId$": "-",
4028
- "$attrsToReflect$": []
4187
+ "$attrsToReflect$": [["region", "region"]]
4029
4188
  }; }
4030
4189
  }
4031
4190
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@penn-libraries/web",
3
- "version": "1.1.1-dev.1",
3
+ "version": "1.1.1-dev.2",
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",