@taprootio/espalier 4.16.0 → 4.17.0

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.
@@ -4,6 +4,7 @@ import { type FocusPoint } from "./image-focus.js";
4
4
  import "./esp-image-option.js";
5
5
  export type ImageScrim = "auto" | "none" | "flat" | "top" | "bottom" | "left" | "right" | "radial";
6
6
  export type ImageScrimStrength = "soft" | "medium" | "strong";
7
+ export type ImageScrimEdge = "none" | "flush";
7
8
  export type ImageTexture = "none" | "dots" | "halftone" | "paper" | "grain" | "grunge" | "scanlines" | "duotone";
8
9
  export type ImageTextureScale = "fine" | "medium" | "coarse";
9
10
  export type ImageBannerScheme = "auto" | "light" | "dark";
@@ -116,6 +117,66 @@ export declare function parseImageRatio(value: string): string | null;
116
117
  * </esp-image>
117
118
  * ```
118
119
  *
120
+ * ### Meeting an adjoining band
121
+ *
122
+ * A directional scrim is capped by `scrim-strength`, so even `strong` leaves
123
+ * roughly a quarter of the photograph showing at the very edge it is anchored
124
+ * to. `scrim-edge="flush"` paints that edge at full alpha and relaxes to the
125
+ * ordinary strength over `--esp-image-scrim-edge-depth` (12% of the gradient
126
+ * axis by default), so a banner dissolves into a section band directly beneath
127
+ * it with no seam. Above that band the scrim is unchanged — `flush` is an edge
128
+ * treatment, not a heavier veil over the subject.
129
+ *
130
+ * The join is exact because both sides paint the same token: when
131
+ * `banner-scheme` resolves to the page scheme the scrim ink *is*
132
+ * `--esp-color-background`, which is what [esp-section](/components/section)
133
+ * paints its band with. Put the banner in a section with no padding, directly
134
+ * above a section in the same context:
135
+ *
136
+ * ```html
137
+ * <esp-section style="--esp-section-max-width: none; --esp-section-padding-inline: 0; --esp-section-padding-block: 0;">
138
+ * <esp-image banner ratio="3/1" scrim="bottom" scrim-strength="strong" scrim-edge="flush" focus="0.65 0.4">
139
+ * <img src="/assets/focus-picker-unsplash.jpg" alt="A woman standing in a sunlit field with her back to the camera" />
140
+ * <h2 slot="overlay">Grow somewhere wonderful</h2>
141
+ * </esp-image>
142
+ * </esp-section>
143
+ * <esp-section>
144
+ * <p>The banner's bottom edge and this band are the same color, so the photograph fades into the page rather than stopping at a line.</p>
145
+ * </esp-section>
146
+ * ```
147
+ *
148
+ * `flush` is inert unless the *resolved* scrim is directional — an `auto`
149
+ * scrim that resolves to `top` or `bottom` picks it up, while `none`, `flat`,
150
+ * and `radial` ignore it, having no anchored edge for it to close.
151
+ *
152
+ * It is its own attribute rather than a fourth `scrim-strength` because the two
153
+ * are independent: `scrim-strength` is how much veil sits over the subject,
154
+ * `scrim-edge` is whether the anchored edge completes. A light veil with a
155
+ * closed edge is exactly the composition this exists for, and a strength step
156
+ * could not express it.
157
+ *
158
+ * A seamless tile can bleed across the join too — any `background-image` value,
159
+ * a tile the application serves, a data URI, or a gradient. Hand the edge the
160
+ * same background the adjoining section paints and it tiles up over the
161
+ * photograph, fading out over the same band:
162
+ *
163
+ * ```html
164
+ * <esp-image
165
+ * banner
166
+ * ratio="3/1"
167
+ * scrim="bottom"
168
+ * scrim-edge="flush"
169
+ * style="--esp-image-scrim-edge-image: repeating-linear-gradient(45deg, oklch(0.55 0.08 255 / 0.18) 0 6px, transparent 6px 18px); --esp-image-scrim-edge-size: 18px 18px; --esp-image-scrim-edge-depth: 22%;"
170
+ * >
171
+ * <img src="/assets/focus-picker-unsplash.jpg" alt="A woman standing in a sunlit field with her back to the camera" />
172
+ * </esp-image>
173
+ * ```
174
+ *
175
+ * The component anchors its own side of the tile to the scrim's edge, so the
176
+ * banner's last tile row ends exactly at the join. Continuity into the section
177
+ * below is the consumer's half of the contract: the same `background-size`,
178
+ * and a `top`-anchored `background-position` on the adjoining band.
179
+ *
119
180
  * Every procedural preset supports `fine`, `medium`, and `coarse` scales.
120
181
  *
121
182
  * ### Dots
@@ -232,8 +293,11 @@ export declare function parseImageRatio(value: string): string | null;
232
293
  * @cssprop --esp-image-border-radius - Corner radius; defaults to `var(--esp-size-border-radius)` or zero in banner mode.
233
294
  * @cssprop --esp-image-object-position - Final object-position override.
234
295
  * @cssprop --esp-image-compact-width - Component-width threshold; defaults to `40rem`.
235
- * @cssprop --esp-image-scrim-color - Scrim ink; defaults to the theme background when the banner polarity matches the scheme, else a polarity-pinned derivation.
296
+ * @cssprop --esp-image-scrim-color - Scrim ink; defaults to the theme background when the banner polarity matches the scheme, else a polarity-pinned derivation. A `scrim-edge="flush"` join is seamless only while the ink is the adjoining band's own color, so an explicit cross-scheme `banner-scheme` (or an override to any other color) makes the edge a deliberate color break rather than a dissolve.
236
297
  * @cssprop --esp-image-scrim-opacity - Scrim-strength opacity override.
298
+ * @cssprop --esp-image-scrim-edge-depth - How far a `scrim-edge="flush"` edge takes to relax from full alpha to the ordinary scrim strength, along the gradient axis; defaults to `12%`.
299
+ * @cssprop --esp-image-scrim-edge-image - Background image painted over the flush edge and faded out with it — the adjoining band's seamless tile; defaults to `none`. Inert unless `scrim-edge="flush"` resolves onto a directional scrim.
300
+ * @cssprop --esp-image-scrim-edge-size - Background size for `--esp-image-scrim-edge-image`; defaults to `auto`. Match the adjoining band's own `background-size` for a continuous tile.
237
301
  * @cssprop --esp-image-texture-color - Procedural texture ink; defaults to a theme-derived color following `banner-scheme`.
238
302
  * @cssprop --esp-image-texture-opacity - Procedural texture opacity override.
239
303
  * @cssprop --esp-image-texture-scale - CSS-gradient texture pitch override.
@@ -309,6 +373,14 @@ export declare class EspalierImage extends EspalierElementBase {
309
373
  scrim: ImageScrim;
310
374
  /** Preset opacity for the scrim. */
311
375
  scrimStrength: ImageScrimStrength;
376
+ /**
377
+ * Edge treatment for a directional scrim. `flush` paints the scrim ink at
378
+ * full alpha along the edge the scrim is anchored to, relaxing to the
379
+ * ordinary strength over `--esp-image-scrim-edge-depth`, so a banner can
380
+ * dissolve into a section band it adjoins. Inert unless the resolved scrim
381
+ * is `top`, `bottom`, `left`, or `right`.
382
+ */
383
+ scrimEdge: ImageScrimEdge;
312
384
  /**
313
385
  * Optional texture: a built-in procedural preset, or the name of a
314
386
  * texture registered with `registerImageTexture`. Unknown names render
@@ -1,11 +1,11 @@
1
- var n=function(d,e,t,r){var i=arguments.length,s=i<3?e:r===null?r=Object.getOwnPropertyDescriptor(e,t):r,o;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")s=Reflect.decorate(d,e,t,r);else for(var l=d.length-1;l>=0;l--)(o=d[l])&&(s=(i<3?o(s):i>3?o(e,t,s):o(e,t))||s);return i>3&&s&&Object.defineProperty(e,t,s),s},p;import{css as I,html as m,nothing as C}from"lit";import{customElement as $,property as c,state as g}from"lit/decorators.js";import{EspalierElementBase as v}from"../shared/esp-element-base.js";import{slotHasContent as y}from"../shared/slot-content.js";import{renderVisualOverlay as T,visualOverlayStyles as A}from"../shared/visual-overlay.js";import{focusConverter as N,normalizeFocus as R}from"./image-focus.js";import{ensureImageTextureFilters as W,imageTextureFilterId as E}from"./image-texture-filters.js";import{getImageTexture as b,isBuiltInImageTexture as x,subscribeToImageTextureRegistry as M}from"../shared/image-texture-registry.js";import{validImageDimensions as _}from"./image-dimensions.js";import"./esp-image-option.js";const k=new Set(["auto","none","flat","top","bottom","left","right","radial"]),S=new Set(["soft","medium","strong"]),w=new Set(["fine","medium","coarse"]),j=new Set(["auto","light","dark"]),O=new Set(["bottom-start","bottom","bottom-end","center","top-start","top","top-end"]);function h(d,e){return{fromAttribute(t){return t&&d.has(t)?t:e},toAttribute(t){return d.has(t)?t:e}}}function z(d){const e=d.trim();if(!e)return null;const t=e.split("/").map(i=>i.trim());if(t.length>2||t.some(i=>i.length===0))return null;const r=t.map(Number);return r.some(i=>!Number.isFinite(i)||i<=0)?null:r.length===1?String(r[0]):`${r[0]} / ${r[1]}`}let a=p=class extends v{constructor(){super(...arguments),this.originalHeight=0,this.originalWidth=0,this.imageUrl="",this.localImage="",this.caption="",this.sizes="100vw",this.loading="eager",this.deferOffscreen=!1,this.banner=!1,this.bannerScheme="auto",this.scrim="auto",this.scrimStrength="medium",this.texture="none",this.textureScale="medium",this.contentPosition="bottom-start",this.focusPoint={x:.5,y:.5},this.ratio="",this.compactRatio="",this.renderedWidth=0,this.measuredWidth=0,this.compact=!1,this.overlayHasContent=!1,this.resizeObserver=null,this.offscreenObserver=null,this.offscreenAdmitted=!1,this.deferredProjectedImage=null,this.unsubscribeTextures=null,this.handleSlotChange=e=>{if(e.target instanceof HTMLSlotElement){if(e.target.name==="overlay"){const t=y(e.target);t!==this.overlayHasContent&&(this.overlayHasContent=t);return}this.requestUpdate()}}}get isPortrait(){const e=_(this.originalWidth,this.originalHeight);return e!==null&&e.height>e.width}findProjected(){for(const e of this.children)if(e.tagName==="PICTURE"||e.tagName==="IMG")return e;return null}findProjectedImage(e=this.findProjected()){return e?e.tagName==="IMG"?e:e.querySelector("img"):null}requiresOffscreenDeferral(e=this.findProjectedImage()){return this.deferOffscreen&&e?.getAttribute("loading")?.toLowerCase()==="lazy"}shouldDeferProjectedSizes(){return this.requiresOffscreenDeferral()&&!this.offscreenAdmitted}applyMeasuredWidth(){this.measuredWidth>0&&this.measuredWidth!==this.renderedWidth&&(this.renderedWidth=this.measuredWidth)}syncOffscreenDeferral(){const e=this.findProjectedImage();if(e!==this.deferredProjectedImage&&(this.offscreenAdmitted=!1,this.offscreenObserver?.disconnect(),this.offscreenObserver=null,this.deferredProjectedImage=e),!this.requiresOffscreenDeferral(e)){this.offscreenObserver?.disconnect(),this.offscreenObserver=null,this.offscreenAdmitted=!1,this.deferredProjectedImage=null,this.applyMeasuredWidth();return}if(!this.offscreenAdmitted){if(typeof IntersectionObserver>"u"){this.offscreenAdmitted=!0,this.applyMeasuredWidth();return}if(!this.offscreenObserver){const t=e,r=new IntersectionObserver(i=>{if(this.offscreenObserver===r){if(this.findProjectedImage()!==t){this.syncOffscreenDeferral();return}i.some(s=>s.isIntersecting)&&(this.offscreenAdmitted=!0,this.offscreenObserver?.disconnect(),this.offscreenObserver=null,this.applyMeasuredWidth(),this.requestUpdate())}},{rootMargin:"200px"});this.offscreenObserver=r}this.offscreenObserver.observe(this)}}connectedCallback(){super.connectedCallback(),p.ensureProjectionStyles(this.getRootNode()),this.unsubscribeTextures??=M(()=>this.requestUpdate()),this.resizeObserver??=new ResizeObserver(e=>{const t=e[0]?.contentBoxSize?.[0],r=Math.round(t?t.inlineSize:e[0]?.contentRect.width??0);r<=0||(this.measuredWidth=r,this.syncOffscreenDeferral(),this.applyMeasuredWidth())}),this.resizeObserver.observe(this),this.syncOffscreenDeferral()}disconnectedCallback(){super.disconnectedCallback(),this.unsubscribeTextures?.(),this.unsubscribeTextures=null,this.resizeObserver?.disconnect(),this.resizeObserver=null,this.offscreenObserver?.disconnect(),this.offscreenObserver=null,this.offscreenAdmitted=!1,this.deferredProjectedImage=null}effectiveSizes(){return this.renderedWidth>0?`${this.renderedWidth}px`:this.sizes}buildSources(){const e=new Map;for(const t of this.querySelectorAll("esp-image-option")){const r=t.getAttribute("url"),i=Number(t.getAttribute("width"));if(!r||!Number.isFinite(i)||i<=0)continue;const s=t.getAttribute("type")??"",o=e.get(s)??[];o.push({url:r,width:i}),e.set(s,o)}return Array.from(e,([t,r])=>({type:t,srcset:r.sort((i,s)=>i.width-s.width).map(i=>`${i.url} ${i.width}w`).join(", ")})).filter(t=>t.srcset.length>0)}resolvedBannerScheme(){return this.bannerScheme!=="auto"?this.bannerScheme:this.scheme==="dark"?"dark":"light"}resolvedTexture(){return x(this.texture)?this.texture:b(this.texture)?this.texture:"none"}resolvedScrim(){return this.scrim!=="auto"?this.scrim:this.overlayHasContent?this.contentPosition.startsWith("top")?"top":this.contentPosition.startsWith("bottom")?"bottom":"radial":"none"}resolvedAspectRatio(){const e=_(this.originalWidth,this.originalHeight);if(!this.banner)return e?`${e.width} / ${e.height}`:null;const t=z(this.ratio);return(this.compact?z(this.compactRatio):null)??t??(e?`${e.width} / ${e.height}`:null)}normalizeEnums(e){e.has("scrim")&&!k.has(this.scrim)&&(this.scrim="auto"),e.has("bannerScheme")&&!j.has(this.bannerScheme)&&(this.bannerScheme="auto"),e.has("scrimStrength")&&!S.has(this.scrimStrength)&&(this.scrimStrength="medium"),e.has("textureScale")&&!w.has(this.textureScale)&&(this.textureScale="medium"),e.has("contentPosition")&&!O.has(this.contentPosition)&&(this.contentPosition="bottom-start")}willUpdate(e){super.willUpdate(e),this.normalizeEnums(e);const t=R(this.focusPoint),r=Number((t.x*100).toFixed(4)),i=Number((t.y*100).toFixed(4));this.style.setProperty("--_esp-image-object-position",`${r}% ${i}%`);const s=this.resolvedAspectRatio();s?this.style.setProperty("--_esp-image-aspect-ratio",s):this.style.removeProperty("--_esp-image-aspect-ratio");const o=this.resolvedTexture();if(["paper","grain","grunge"].includes(o)){const u=o==="grunge"&&this.resolvedBannerScheme()==="light"?"grunge-light":o;this.style.setProperty("--_esp-image-texture-filter",`url(#${E(u,this.textureScale)})`)}else this.style.removeProperty("--_esp-image-texture-filter");const l=x(o)?void 0:b(o),P=[["--_esp-image-registered-image",l?.image],["--_esp-image-registered-size",l?.size],["--_esp-image-registered-repeat",l?.repeat],["--_esp-image-registered-position",l?.position],["--_esp-image-registered-opacity",l?.opacity?.toString()],["--_esp-image-registered-blend-mode",l?.blendMode]];for(const[u,f]of P)f===void 0?this.style.removeProperty(u):this.style.setProperty(u,f)}firstUpdated(e){super.firstUpdated(e);const t=this.shadowRoot?.querySelector('slot[name="overlay"]');t&&(this.overlayHasContent=y(t))}updated(e){if(super.updated(e),this.syncOffscreenDeferral(),["paper","grain","grunge"].includes(this.resolvedTexture())&&this.renderRoot&&W(this.renderRoot),this.renderedWidth>0){const o=this.shadowRoot?.querySelector(".compact-sentinel")?.getBoundingClientRect().width??0,l=o>0&&this.renderedWidth<o;l!==this.compact&&(this.compact=l)}if(this.renderedWidth<=0||this.shouldDeferProjectedSizes())return;const t=this.findProjected();if(!t)return;const r=this.effectiveSizes();if(t.tagName==="PICTURE")for(const s of t.querySelectorAll("source"))s.setAttribute("sizes",r);const i=this.findProjectedImage(t);i?.hasAttribute("srcset")&&i.setAttribute("sizes",r)}renderMedia(){if(this.localImage)return m`<img
1
+ var n=function(d,e,t,r){var i=arguments.length,s=i<3?e:r===null?r=Object.getOwnPropertyDescriptor(e,t):r,o;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")s=Reflect.decorate(d,e,t,r);else for(var l=d.length-1;l>=0;l--)(o=d[l])&&(s=(i<3?o(s):i>3?o(e,t,s):o(e,t))||s);return i>3&&s&&Object.defineProperty(e,t,s),s},p;import{css as T,html as h,nothing as C}from"lit";import{customElement as $,property as c,state as u}from"lit/decorators.js";import{EspalierElementBase as v}from"../shared/esp-element-base.js";import{slotHasContent as y}from"../shared/slot-content.js";import{renderVisualOverlay as E,visualOverlayStyles as A}from"../shared/visual-overlay.js";import{focusConverter as N,normalizeFocus as R}from"./image-focus.js";import{ensureImageTextureFilters as W,imageTextureFilterId as M}from"./image-texture-filters.js";import{getImageTexture as b,isBuiltInImageTexture as x,subscribeToImageTextureRegistry as D}from"../shared/image-texture-registry.js";import{validImageDimensions as _}from"./image-dimensions.js";import"./esp-image-option.js";const k=new Set(["auto","none","flat","top","bottom","left","right","radial"]),w=new Set(["soft","medium","strong"]),S=new Set(["none","flush"]),z=new Set(["fine","medium","coarse"]),j=new Set(["auto","light","dark"]),O=new Set(["bottom-start","bottom","bottom-end","center","top-start","top","top-end"]);function m(d,e){return{fromAttribute(t){return t&&d.has(t)?t:e},toAttribute(t){return d.has(t)?t:e}}}function I(d){const e=d.trim();if(!e)return null;const t=e.split("/").map(i=>i.trim());if(t.length>2||t.some(i=>i.length===0))return null;const r=t.map(Number);return r.some(i=>!Number.isFinite(i)||i<=0)?null:r.length===1?String(r[0]):`${r[0]} / ${r[1]}`}let a=p=class extends v{constructor(){super(...arguments),this.originalHeight=0,this.originalWidth=0,this.imageUrl="",this.localImage="",this.caption="",this.sizes="100vw",this.loading="eager",this.deferOffscreen=!1,this.banner=!1,this.bannerScheme="auto",this.scrim="auto",this.scrimStrength="medium",this.scrimEdge="none",this.texture="none",this.textureScale="medium",this.contentPosition="bottom-start",this.focusPoint={x:.5,y:.5},this.ratio="",this.compactRatio="",this.renderedWidth=0,this.measuredWidth=0,this.compact=!1,this.overlayHasContent=!1,this.resizeObserver=null,this.offscreenObserver=null,this.offscreenAdmitted=!1,this.deferredProjectedImage=null,this.unsubscribeTextures=null,this.handleSlotChange=e=>{if(e.target instanceof HTMLSlotElement){if(e.target.name==="overlay"){const t=y(e.target);t!==this.overlayHasContent&&(this.overlayHasContent=t);return}this.requestUpdate()}}}get isPortrait(){const e=_(this.originalWidth,this.originalHeight);return e!==null&&e.height>e.width}findProjected(){for(const e of this.children)if(e.tagName==="PICTURE"||e.tagName==="IMG")return e;return null}findProjectedImage(e=this.findProjected()){return e?e.tagName==="IMG"?e:e.querySelector("img"):null}requiresOffscreenDeferral(e=this.findProjectedImage()){return this.deferOffscreen&&e?.getAttribute("loading")?.toLowerCase()==="lazy"}shouldDeferProjectedSizes(){return this.requiresOffscreenDeferral()&&!this.offscreenAdmitted}applyMeasuredWidth(){this.measuredWidth>0&&this.measuredWidth!==this.renderedWidth&&(this.renderedWidth=this.measuredWidth)}syncOffscreenDeferral(){const e=this.findProjectedImage();if(e!==this.deferredProjectedImage&&(this.offscreenAdmitted=!1,this.offscreenObserver?.disconnect(),this.offscreenObserver=null,this.deferredProjectedImage=e),!this.requiresOffscreenDeferral(e)){this.offscreenObserver?.disconnect(),this.offscreenObserver=null,this.offscreenAdmitted=!1,this.deferredProjectedImage=null,this.applyMeasuredWidth();return}if(!this.offscreenAdmitted){if(typeof IntersectionObserver>"u"){this.offscreenAdmitted=!0,this.applyMeasuredWidth();return}if(!this.offscreenObserver){const t=e,r=new IntersectionObserver(i=>{if(this.offscreenObserver===r){if(this.findProjectedImage()!==t){this.syncOffscreenDeferral();return}i.some(s=>s.isIntersecting)&&(this.offscreenAdmitted=!0,this.offscreenObserver?.disconnect(),this.offscreenObserver=null,this.applyMeasuredWidth(),this.requestUpdate())}},{rootMargin:"200px"});this.offscreenObserver=r}this.offscreenObserver.observe(this)}}connectedCallback(){super.connectedCallback(),p.ensureProjectionStyles(this.getRootNode()),this.unsubscribeTextures??=D(()=>this.requestUpdate()),this.resizeObserver??=new ResizeObserver(e=>{const t=e[0]?.contentBoxSize?.[0],r=Math.round(t?t.inlineSize:e[0]?.contentRect.width??0);r<=0||(this.measuredWidth=r,this.syncOffscreenDeferral(),this.applyMeasuredWidth())}),this.resizeObserver.observe(this),this.syncOffscreenDeferral()}disconnectedCallback(){super.disconnectedCallback(),this.unsubscribeTextures?.(),this.unsubscribeTextures=null,this.resizeObserver?.disconnect(),this.resizeObserver=null,this.offscreenObserver?.disconnect(),this.offscreenObserver=null,this.offscreenAdmitted=!1,this.deferredProjectedImage=null}effectiveSizes(){return this.renderedWidth>0?`${this.renderedWidth}px`:this.sizes}buildSources(){const e=new Map;for(const t of this.querySelectorAll("esp-image-option")){const r=t.getAttribute("url"),i=Number(t.getAttribute("width"));if(!r||!Number.isFinite(i)||i<=0)continue;const s=t.getAttribute("type")??"",o=e.get(s)??[];o.push({url:r,width:i}),e.set(s,o)}return Array.from(e,([t,r])=>({type:t,srcset:r.sort((i,s)=>i.width-s.width).map(i=>`${i.url} ${i.width}w`).join(", ")})).filter(t=>t.srcset.length>0)}resolvedBannerScheme(){return this.bannerScheme!=="auto"?this.bannerScheme:this.scheme==="dark"?"dark":"light"}resolvedTexture(){return x(this.texture)?this.texture:b(this.texture)?this.texture:"none"}resolvedScrim(){return this.scrim!=="auto"?this.scrim:this.overlayHasContent?this.contentPosition.startsWith("top")?"top":this.contentPosition.startsWith("bottom")?"bottom":"radial":"none"}resolvedAspectRatio(){const e=_(this.originalWidth,this.originalHeight);if(!this.banner)return e?`${e.width} / ${e.height}`:null;const t=I(this.ratio);return(this.compact?I(this.compactRatio):null)??t??(e?`${e.width} / ${e.height}`:null)}normalizeEnums(e){e.has("scrim")&&!k.has(this.scrim)&&(this.scrim="auto"),e.has("bannerScheme")&&!j.has(this.bannerScheme)&&(this.bannerScheme="auto"),e.has("scrimStrength")&&!w.has(this.scrimStrength)&&(this.scrimStrength="medium"),e.has("scrimEdge")&&!S.has(this.scrimEdge)&&(this.scrimEdge="none"),e.has("textureScale")&&!z.has(this.textureScale)&&(this.textureScale="medium"),e.has("contentPosition")&&!O.has(this.contentPosition)&&(this.contentPosition="bottom-start")}willUpdate(e){super.willUpdate(e),this.normalizeEnums(e);const t=R(this.focusPoint),r=Number((t.x*100).toFixed(4)),i=Number((t.y*100).toFixed(4));this.style.setProperty("--_esp-image-object-position",`${r}% ${i}%`);const s=this.resolvedAspectRatio();s?this.style.setProperty("--_esp-image-aspect-ratio",s):this.style.removeProperty("--_esp-image-aspect-ratio");const o=this.resolvedTexture();if(["paper","grain","grunge"].includes(o)){const g=o==="grunge"&&this.resolvedBannerScheme()==="light"?"grunge-light":o;this.style.setProperty("--_esp-image-texture-filter",`url(#${M(g,this.textureScale)})`)}else this.style.removeProperty("--_esp-image-texture-filter");const l=x(o)?void 0:b(o),P=[["--_esp-image-registered-image",l?.image],["--_esp-image-registered-size",l?.size],["--_esp-image-registered-repeat",l?.repeat],["--_esp-image-registered-position",l?.position],["--_esp-image-registered-opacity",l?.opacity?.toString()],["--_esp-image-registered-blend-mode",l?.blendMode]];for(const[g,f]of P)f===void 0?this.style.removeProperty(g):this.style.setProperty(g,f)}firstUpdated(e){super.firstUpdated(e);const t=this.shadowRoot?.querySelector('slot[name="overlay"]');t&&(this.overlayHasContent=y(t))}updated(e){if(super.updated(e),this.syncOffscreenDeferral(),["paper","grain","grunge"].includes(this.resolvedTexture())&&this.renderRoot&&W(this.renderRoot),this.renderedWidth>0){const o=this.shadowRoot?.querySelector(".compact-sentinel")?.getBoundingClientRect().width??0,l=o>0&&this.renderedWidth<o;l!==this.compact&&(this.compact=l)}if(this.renderedWidth<=0||this.shouldDeferProjectedSizes())return;const t=this.findProjected();if(!t)return;const r=this.effectiveSizes();if(t.tagName==="PICTURE")for(const s of t.querySelectorAll("source"))s.setAttribute("sizes",r);const i=this.findProjectedImage(t);i?.hasAttribute("srcset")&&i.setAttribute("sizes",r)}renderMedia(){if(this.localImage)return h`<img
2
2
  src=${this.localImage}
3
3
  alt=${this.caption}
4
4
  decoding="async"
5
5
  loading=${this.loading}
6
- />`;const e=m`<slot @slotchange=${this.handleSlotChange}></slot>`;if(this.findProjected())return e;const t=this.buildSources();return m`
6
+ />`;const e=h`<slot @slotchange=${this.handleSlotChange}></slot>`;if(this.findProjected())return e;const t=this.buildSources();return h`
7
7
  <picture>
8
- ${t.map(r=>m`
8
+ ${t.map(r=>h`
9
9
  <source
10
10
  srcset=${r.srcset}
11
11
  sizes=${this.effectiveSizes()}
@@ -15,19 +15,20 @@ var n=function(d,e,t,r){var i=arguments.length,s=i<3?e:r===null?r=Object.getOwnP
15
15
  <img src=${this.imageUrl} alt=${this.caption} decoding="async" loading=${this.loading} />
16
16
  </picture>
17
17
  ${e}
18
- `}render(){return m`
18
+ `}render(){return h`
19
19
  <div
20
20
  part="frame"
21
21
  class="frame"
22
22
  data-banner-scheme=${this.resolvedBannerScheme()}
23
23
  data-scrim=${this.resolvedScrim()}
24
24
  data-scrim-strength=${this.scrimStrength}
25
+ data-scrim-edge=${this.scrimEdge}
25
26
  data-texture=${this.resolvedTexture()}
26
27
  data-texture-scale=${this.textureScale}
27
28
  data-content-position=${this.contentPosition}
28
29
  >
29
30
  <div part="image" class="image">${this.renderMedia()}</div>
30
- ${T({className:"banner-layers",imagePart:"texture",scrimPart:"scrim",texturePart:"texture"})}
31
+ ${E({className:"banner-layers",imagePart:"texture",scrimPart:"scrim",texturePart:"texture"})}
31
32
  <div part="overlay" class="overlay" ?hidden=${!this.banner||!this.overlayHasContent}>
32
33
  <slot name="overlay" @slotchange=${this.handleSlotChange}></slot>
33
34
  </div>
@@ -49,7 +50,7 @@ var n=function(d,e,t,r){var i=arguments.length,s=i<3?e:r===null?r=Object.getOwnP
49
50
  object-fit: var(--_esp-image-object-fit, cover);
50
51
  object-position: var(--esp-image-object-position, var(--_esp-image-object-position, center));
51
52
  }
52
- `,a.styles=[...v.styles,I`
53
+ `,a.styles=[...v.styles,T`
53
54
  :host {
54
55
  display: block;
55
56
  aspect-ratio: var(--_esp-image-aspect-ratio, auto);
@@ -224,6 +225,71 @@ var n=function(d,e,t,r){var i=arguments.length,s=i<3?e:r===null?r=Object.getOwnP
224
225
  }
225
226
 
226
227
 
228
+ :host([banner]) .banner-layers::after {
229
+ content: var(--_esp-image-scrim-edge-content, none);
230
+ position: absolute;
231
+ inset: 0;
232
+ pointer-events: none;
233
+ background-color: var(--_esp-image-scrim-edge-color, transparent);
234
+ background-image: var(--_esp-image-scrim-edge-image, none);
235
+ background-position: var(--_esp-image-scrim-edge-position, 0 0);
236
+ background-repeat: repeat;
237
+ background-size: var(--_esp-image-scrim-edge-size, auto);
238
+ mask-image: var(--_esp-image-scrim-edge-mask, none);
239
+ mask-mode: alpha;
240
+ }
241
+
242
+
243
+ .frame[data-scrim-edge="flush"][data-scrim="top"] {
244
+ --_esp-image-scrim-edge-content: "";
245
+ --_esp-image-scrim-edge-color: var(--_esp-image-scrim-resolved);
246
+ --_esp-image-scrim-edge-image: var(--esp-image-scrim-edge-image, none);
247
+ --_esp-image-scrim-edge-size: var(--esp-image-scrim-edge-size, auto);
248
+ --_esp-image-scrim-edge-position: left top;
249
+ --_esp-image-scrim-edge-mask: linear-gradient(
250
+ to bottom,
251
+ black 0%,
252
+ transparent var(--esp-image-scrim-edge-depth, 12%)
253
+ );
254
+ }
255
+ .frame[data-scrim-edge="flush"][data-scrim="bottom"] {
256
+ --_esp-image-scrim-edge-content: "";
257
+ --_esp-image-scrim-edge-color: var(--_esp-image-scrim-resolved);
258
+ --_esp-image-scrim-edge-image: var(--esp-image-scrim-edge-image, none);
259
+ --_esp-image-scrim-edge-size: var(--esp-image-scrim-edge-size, auto);
260
+ --_esp-image-scrim-edge-position: left bottom;
261
+ --_esp-image-scrim-edge-mask: linear-gradient(
262
+ to top,
263
+ black 0%,
264
+ transparent var(--esp-image-scrim-edge-depth, 12%)
265
+ );
266
+ }
267
+ .frame[data-scrim-edge="flush"][data-scrim="left"] {
268
+ --_esp-image-scrim-edge-content: "";
269
+ --_esp-image-scrim-edge-color: var(--_esp-image-scrim-resolved);
270
+ --_esp-image-scrim-edge-image: var(--esp-image-scrim-edge-image, none);
271
+ --_esp-image-scrim-edge-size: var(--esp-image-scrim-edge-size, auto);
272
+ --_esp-image-scrim-edge-position: left top;
273
+ --_esp-image-scrim-edge-mask: linear-gradient(
274
+ to right,
275
+ black 0%,
276
+ transparent var(--esp-image-scrim-edge-depth, 12%)
277
+ );
278
+ }
279
+ .frame[data-scrim-edge="flush"][data-scrim="right"] {
280
+ --_esp-image-scrim-edge-content: "";
281
+ --_esp-image-scrim-edge-color: var(--_esp-image-scrim-resolved);
282
+ --_esp-image-scrim-edge-image: var(--esp-image-scrim-edge-image, none);
283
+ --_esp-image-scrim-edge-size: var(--esp-image-scrim-edge-size, auto);
284
+ --_esp-image-scrim-edge-position: right top;
285
+ --_esp-image-scrim-edge-mask: linear-gradient(
286
+ to left,
287
+ black 0%,
288
+ transparent var(--esp-image-scrim-edge-depth, 12%)
289
+ );
290
+ }
291
+
292
+
227
293
  .frame[data-banner-scheme="dark"] {
228
294
  --_esp-image-scrim-ink: oklch(from var(--esp-color-background, oklch(0.98 0 0)) 0.13 c h);
229
295
  --_esp-image-texture-ink: oklch(from var(--esp-color-background, oklch(0.98 0 0)) 0.12 c h);
@@ -439,4 +505,4 @@ var n=function(d,e,t,r){var i=arguments.length,s=i<3?e:r===null?r=Object.getOwnP
439
505
  transition: none !important;
440
506
  }
441
507
  }
442
- `,A],n([c({attribute:"original-height",type:Number})],a.prototype,"originalHeight",void 0),n([c({attribute:"original-width",type:Number})],a.prototype,"originalWidth",void 0),n([c({attribute:"low-res",type:String})],a.prototype,"imageUrl",void 0),n([c({attribute:"local-image",type:String})],a.prototype,"localImage",void 0),n([c({type:String})],a.prototype,"caption",void 0),n([c({attribute:"sizes",type:String})],a.prototype,"sizes",void 0),n([c({attribute:"loading",type:String})],a.prototype,"loading",void 0),n([c({attribute:"defer-offscreen",type:Boolean})],a.prototype,"deferOffscreen",void 0),n([c({type:Boolean,reflect:!0})],a.prototype,"banner",void 0),n([c({attribute:"banner-scheme",converter:h(j,"auto"),reflect:!0})],a.prototype,"bannerScheme",void 0),n([c({converter:h(k,"auto"),reflect:!0})],a.prototype,"scrim",void 0),n([c({attribute:"scrim-strength",converter:h(S,"medium"),reflect:!0})],a.prototype,"scrimStrength",void 0),n([c({type:String,reflect:!0})],a.prototype,"texture",void 0),n([c({attribute:"texture-scale",converter:h(w,"medium"),reflect:!0})],a.prototype,"textureScale",void 0),n([c({attribute:"content-position",converter:h(O,"bottom-start"),reflect:!0})],a.prototype,"contentPosition",void 0),n([c({attribute:"focus",converter:N,reflect:!0})],a.prototype,"focusPoint",void 0),n([c({type:String})],a.prototype,"ratio",void 0),n([c({attribute:"compact-ratio",type:String})],a.prototype,"compactRatio",void 0),n([g()],a.prototype,"renderedWidth",void 0),n([g()],a.prototype,"compact",void 0),n([g()],a.prototype,"overlayHasContent",void 0),a=p=n([$("esp-image")],a);export{a as EspalierImage,z as parseImageRatio};
508
+ `,A],n([c({attribute:"original-height",type:Number})],a.prototype,"originalHeight",void 0),n([c({attribute:"original-width",type:Number})],a.prototype,"originalWidth",void 0),n([c({attribute:"low-res",type:String})],a.prototype,"imageUrl",void 0),n([c({attribute:"local-image",type:String})],a.prototype,"localImage",void 0),n([c({type:String})],a.prototype,"caption",void 0),n([c({attribute:"sizes",type:String})],a.prototype,"sizes",void 0),n([c({attribute:"loading",type:String})],a.prototype,"loading",void 0),n([c({attribute:"defer-offscreen",type:Boolean})],a.prototype,"deferOffscreen",void 0),n([c({type:Boolean,reflect:!0})],a.prototype,"banner",void 0),n([c({attribute:"banner-scheme",converter:m(j,"auto"),reflect:!0})],a.prototype,"bannerScheme",void 0),n([c({converter:m(k,"auto"),reflect:!0})],a.prototype,"scrim",void 0),n([c({attribute:"scrim-strength",converter:m(w,"medium"),reflect:!0})],a.prototype,"scrimStrength",void 0),n([c({attribute:"scrim-edge",converter:m(S,"none"),reflect:!0})],a.prototype,"scrimEdge",void 0),n([c({type:String,reflect:!0})],a.prototype,"texture",void 0),n([c({attribute:"texture-scale",converter:m(z,"medium"),reflect:!0})],a.prototype,"textureScale",void 0),n([c({attribute:"content-position",converter:m(O,"bottom-start"),reflect:!0})],a.prototype,"contentPosition",void 0),n([c({attribute:"focus",converter:N,reflect:!0})],a.prototype,"focusPoint",void 0),n([c({type:String})],a.prototype,"ratio",void 0),n([c({attribute:"compact-ratio",type:String})],a.prototype,"compactRatio",void 0),n([u()],a.prototype,"renderedWidth",void 0),n([u()],a.prototype,"compact",void 0),n([u()],a.prototype,"overlayHasContent",void 0),a=p=n([$("esp-image")],a);export{a as EspalierImage,I as parseImageRatio};
@@ -73,6 +73,7 @@ var d=function(c,e,t,i){var s=arguments.length,r=s<3?e:i===null?i=Object.getOwnP
73
73
  aria-modal="true"
74
74
  aria-label="Image lightbox"
75
75
  tabindex="-1"
76
+ data-no-swipe
76
77
  @keydown=${this._handleKeyDown}
77
78
  @click=${this._handleLightboxClick}
78
79
  @touchstart=${this._handleTouchStart}
@@ -1043,7 +1043,19 @@
1043
1043
  },
1044
1044
  {
1045
1045
  "name": "--esp-image-scrim-color",
1046
- "description": "Scrim ink; defaults to the theme background when the banner polarity matches the scheme, else a polarity-pinned derivation."
1046
+ "description": "Scrim ink; defaults to the theme background when the banner polarity matches the scheme, else a polarity-pinned derivation. A `scrim-edge=\"flush\"` join is seamless only while the ink is the adjoining band's own color, so an explicit cross-scheme `banner-scheme` (or an override to any other color) makes the edge a deliberate color break rather than a dissolve."
1047
+ },
1048
+ {
1049
+ "name": "--esp-image-scrim-edge-depth",
1050
+ "description": "How far a `scrim-edge=\"flush\"` edge takes to relax from full alpha to the ordinary scrim strength, along the gradient axis; defaults to `12%`."
1051
+ },
1052
+ {
1053
+ "name": "--esp-image-scrim-edge-image",
1054
+ "description": "Background image painted over the flush edge and faded out with it — the adjoining band's seamless tile; defaults to `none`. Inert unless `scrim-edge=\"flush\"` resolves onto a directional scrim."
1055
+ },
1056
+ {
1057
+ "name": "--esp-image-scrim-edge-size",
1058
+ "description": "Background size for `--esp-image-scrim-edge-image`; defaults to `auto`. Match the adjoining band's own `background-size` for a continuous tile."
1047
1059
  },
1048
1060
  {
1049
1061
  "name": "--esp-image-scrim-opacity",
@@ -1480,7 +1480,25 @@
1480
1480
  "name": "--esp-image-scrim-color",
1481
1481
  "category": "component",
1482
1482
  "component": "esp-image",
1483
- "description": "Scrim ink; defaults to the theme background when the banner polarity matches the scheme, else a polarity-pinned derivation."
1483
+ "description": "Scrim ink; defaults to the theme background when the banner polarity matches the scheme, else a polarity-pinned derivation. A `scrim-edge=\"flush\"` join is seamless only while the ink is the adjoining band's own color, so an explicit cross-scheme `banner-scheme` (or an override to any other color) makes the edge a deliberate color break rather than a dissolve."
1484
+ },
1485
+ {
1486
+ "name": "--esp-image-scrim-edge-depth",
1487
+ "category": "component",
1488
+ "component": "esp-image",
1489
+ "description": "How far a `scrim-edge=\"flush\"` edge takes to relax from full alpha to the ordinary scrim strength, along the gradient axis; defaults to `12%`."
1490
+ },
1491
+ {
1492
+ "name": "--esp-image-scrim-edge-image",
1493
+ "category": "component",
1494
+ "component": "esp-image",
1495
+ "description": "Background image painted over the flush edge and faded out with it — the adjoining band's seamless tile; defaults to `none`. Inert unless `scrim-edge=\"flush\"` resolves onto a directional scrim."
1496
+ },
1497
+ {
1498
+ "name": "--esp-image-scrim-edge-size",
1499
+ "category": "component",
1500
+ "component": "esp-image",
1501
+ "description": "Background size for `--esp-image-scrim-edge-image`; defaults to `auto`. Match the adjoining band's own `background-size` for a continuous tile."
1484
1502
  },
1485
1503
  {
1486
1504
  "name": "--esp-image-scrim-opacity",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taprootio/espalier",
3
- "version": "4.16.0",
3
+ "version": "4.17.0",
4
4
  "packageManager": "bun@1.3.12",
5
5
  "description": "Espalier — a themeable, accessible, framework-agnostic, enterprise-grade design system built on web standards and love.",
6
6
  "customElements": "custom-elements.json",