@taprootio/espalier 4.20.0 → 4.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ # 4.20.1
2
+
3
+ An image banner's scrim now follows the overlay content it protects, so every line of a long heading stays legible on a short banner.
4
+
5
+ - **Fixed: text on a short or wide `esp-image` banner could run off its scrim.** Directional scrims held full strength over a fixed 20% of the frame and faded out by 78%, whatever the overlay contained, so a three-line heading on a 16:9 card cover put its first line where the scrim had nearly faded away, and that line's contrast depended on the photograph. The banner now measures its overlay content and holds the scrim at full strength until the content is covered, with the fade keeping its shape in the space beyond. This holds for the `top`, `bottom`, `left`, and `right` scrims, with any overlay padding, at the `compact-ratio`, with `scrim-edge="flush"`, and as the content changes size when fonts load, the text changes, or the banner resizes. The measurement watches the elements slotted into the overlay, so slot a changing heading directly rather than through a `display: contents` wrapper. An overlay that already fits inside the 20% band renders exactly as before.
6
+ - **The radial scrim now covers centered content.** Its full-strength region used to be the exact center point, so centered text always sat on a fading scrim. It now grows an opaque core out to the overlay content's farthest corner. **Banners with centered overlay text look darker (or lighter, on a light banner) around the text as a result.**
7
+ - An explicit scrim that does not sit behind the content, such as `scrim="top"` or `scrim="radial"` over `bottom-start` content, keeps its original shape rather than stretching into a veil over the whole photograph.
8
+ - `prefers-reduced-transparency`, forced colors, and print treat the scrim as before.
9
+
1
10
  # 4.20.0
2
11
 
3
12
  Sections and pages stop failing accessibility audits they should pass: section decoration becomes an explicit opt-in, and page asides become landmarks only when they have content, each with its own name.
@@ -13657,52 +13657,6 @@
13657
13657
  }
13658
13658
  ]
13659
13659
  },
13660
- {
13661
- "kind": "javascript-module",
13662
- "path": "dist/cli/espalier.js",
13663
- "declarations": [],
13664
- "exports": []
13665
- },
13666
- {
13667
- "kind": "javascript-module",
13668
- "path": "dist/cli/theme-check.js",
13669
- "declarations": [
13670
- {
13671
- "kind": "function",
13672
- "name": "runEspalierCli",
13673
- "return": {
13674
- "type": {
13675
- "text": "number"
13676
- }
13677
- },
13678
- "parameters": [
13679
- {
13680
- "name": "argv",
13681
- "type": {
13682
- "text": "string[]"
13683
- }
13684
- },
13685
- {
13686
- "name": "io",
13687
- "type": {
13688
- "text": "ThemeCheckIo"
13689
- }
13690
- }
13691
- ],
13692
- "description": "Run the CLI for the given argv (everything after the executable and\nscript). Returns the process exit code."
13693
- }
13694
- ],
13695
- "exports": [
13696
- {
13697
- "kind": "js",
13698
- "name": "runEspalierCli",
13699
- "declaration": {
13700
- "name": "runEspalierCli",
13701
- "module": "dist/cli/theme-check.js"
13702
- }
13703
- }
13704
- ]
13705
- },
13706
13660
  {
13707
13661
  "kind": "javascript-module",
13708
13662
  "path": "dist/color-picker/esp-color-picker.js",
@@ -13877,6 +13831,52 @@
13877
13831
  }
13878
13832
  ]
13879
13833
  },
13834
+ {
13835
+ "kind": "javascript-module",
13836
+ "path": "dist/cli/espalier.js",
13837
+ "declarations": [],
13838
+ "exports": []
13839
+ },
13840
+ {
13841
+ "kind": "javascript-module",
13842
+ "path": "dist/cli/theme-check.js",
13843
+ "declarations": [
13844
+ {
13845
+ "kind": "function",
13846
+ "name": "runEspalierCli",
13847
+ "return": {
13848
+ "type": {
13849
+ "text": "number"
13850
+ }
13851
+ },
13852
+ "parameters": [
13853
+ {
13854
+ "name": "argv",
13855
+ "type": {
13856
+ "text": "string[]"
13857
+ }
13858
+ },
13859
+ {
13860
+ "name": "io",
13861
+ "type": {
13862
+ "text": "ThemeCheckIo"
13863
+ }
13864
+ }
13865
+ ],
13866
+ "description": "Run the CLI for the given argv (everything after the executable and\nscript). Returns the process exit code."
13867
+ }
13868
+ ],
13869
+ "exports": [
13870
+ {
13871
+ "kind": "js",
13872
+ "name": "runEspalierCli",
13873
+ "declaration": {
13874
+ "name": "runEspalierCli",
13875
+ "module": "dist/cli/theme-check.js"
13876
+ }
13877
+ }
13878
+ ]
13879
+ },
13880
13880
  {
13881
13881
  "kind": "javascript-module",
13882
13882
  "path": "dist/data-cell/esp-data-cell.js",
@@ -36650,7 +36650,7 @@
36650
36650
  },
36651
36651
  {
36652
36652
  "kind": "class",
36653
- "description": "A responsive image with a native `<picture>` and an optional\nfocal-point-aware banner mode.\n\n`esp-image` accepts two authoring shapes:\n\n1. **Project a light-DOM `<picture>`** — the recommended form for published /\n server-rendered markup. Provide a real `<picture>` (or a bare `<img>`) as\n a child and `esp-image` projects it through a `<slot>`, adding only the\n layout box, border, and lightbox wiring. Because the image lives in the\n served HTML it is discoverable by the preload scanner and visible to\n crawlers, and it is fetched exactly once.\n2. **Declare [esp-image-option](/components/image/image-option) children** —\n the editor / fallback form. `esp-image` builds a shadow `<picture>` and\n `srcset` from them. Use `local-image` when an editor needs a blob preview.\n\nSource precedence is `local-image` → a projected `<picture>` / `<img>` →\n`esp-image-option` children. In every mode the host reserves its layout box\nup front from `original-width` / `original-height`, when supplied.\n\n```html\n<!-- Projected native <picture> — published markup, crawlable + preloadable -->\n<esp-image original-width=\"1200\" original-height=\"800\" caption=\"A scenic mountain lake\">\n <picture>\n <source\n srcset=\"https://picsum.photos/600/400 400w, https://picsum.photos/900/600 800w, https://picsum.photos/1200/800 1200w\"\n sizes=\"100vw\"\n />\n <img src=\"https://picsum.photos/1200/800\" alt=\"A scenic mountain lake\" />\n </picture>\n</esp-image>\n```\n\n```html\n<!-- esp-image-option children — editor / fallback form -->\n<esp-image\n original-width=\"1200\"\n original-height=\"800\"\n low-res=\"https://picsum.photos/300/200\"\n caption=\"A scenic mountain lake\"\n sizes=\"100vw\"\n>\n <esp-image-option width=\"400\" url=\"https://picsum.photos/600/400\"></esp-image-option>\n <esp-image-option width=\"800\" url=\"https://picsum.photos/900/600\"></esp-image-option>\n <esp-image-option width=\"1200\" url=\"https://picsum.photos/1200/800\"></esp-image-option>\n</esp-image>\n```\n\nOnce upgraded, the component measures its own width and updates `sizes` so a\ncard or gallery column does not over-fetch. The `sizes` attribute covers the\nserver-rendered initial fetch and the brief window before measurement:\n\n```html\n<esp-image\n original-width=\"1200\"\n original-height=\"800\"\n low-res=\"https://picsum.photos/300/200\"\n sizes=\"(max-width: 600px) 100vw, 320px\"\n style=\"max-width: 320px;\"\n>\n <esp-image-option width=\"320\" url=\"https://picsum.photos/640/427\"></esp-image-option>\n <esp-image-option width=\"640\" url=\"https://picsum.photos/960/640\"></esp-image-option>\n</esp-image>\n```\n\nFor a long projected lazy-image list, `defer-offscreen` keeps the served\n`sizes` hint until an image approaches the viewport. This avoids replacing a\nsmall fallback selection with a larger candidate before the image can be\nseen. Eager projected images and every non-projected source keep the normal\nimmediate measured-size behavior.\n\nBanner mode gives all three source modes the same focal position, scrim,\ntexture, and semantic overlay stack. `ratio` applies at wide widths and\n`compact-ratio` below the component-relative compact threshold.\n\n```html\n<esp-image\n banner\n ratio=\"3/1\"\n compact-ratio=\"3/2\"\n focus=\"0.65 0.4\"\n scrim=\"auto\"\n scrim-strength=\"medium\"\n content-position=\"bottom-start\"\n>\n <img src=\"/assets/focus-picker-unsplash.jpg\" alt=\"A woman standing in a sunlit field with her back to the camera\" />\n <h2 slot=\"overlay\">Grow somewhere wonderful</h2>\n</esp-image>\n```\n\n`banner-scheme` sets the banner's polarity and defaults to the active\nscheme: a light page lightens the image behind dark text, a dark page\ndarkens it behind light text. Fix either polarity explicitly when a banner\nmust not follow the page scheme:\n\n```html\n<esp-image banner ratio=\"3/1\" banner-scheme=\"light\" focus=\"0.65 0.4\">\n <img src=\"/assets/focus-picker-unsplash.jpg\" alt=\"A woman standing in a sunlit field with her back to the camera\" />\n <h2 slot=\"overlay\">Light banner: dark ink on a lifted image</h2>\n</esp-image>\n<esp-image banner ratio=\"3/1\" banner-scheme=\"dark\" focus=\"0.65 0.4\">\n <img src=\"/assets/focus-picker-unsplash.jpg\" alt=\"A woman standing in a sunlit field with her back to the camera\" />\n <h2 slot=\"overlay\">Dark banner: light ink on a deepened image</h2>\n</esp-image>\n```\n\n### Meeting an adjoining band\n\nA directional scrim is capped by `scrim-strength`, so even `strong` leaves\nroughly a quarter of the photograph showing at the very edge it is anchored\nto. `scrim-edge=\"flush\"` paints that edge at full alpha and relaxes to the\nordinary strength over `--esp-image-scrim-edge-depth` (12% of the gradient\naxis by default), so a banner dissolves into a section band directly beneath\nit with no seam. Above that band the scrim is unchanged — `flush` is an edge\ntreatment, not a heavier veil over the subject.\n\nThe join is exact because both sides paint the same token: when\n`banner-scheme` resolves to the page scheme the scrim ink *is*\n`--esp-color-background`, which is what [esp-section](/components/section)\npaints its band with. Put the banner in a section with no padding, directly\nabove a section in the same context:\n\n```html\n<esp-section style=\"--esp-section-max-width: none; --esp-section-padding-inline: 0; --esp-section-padding-block: 0;\">\n <esp-image banner ratio=\"3/1\" scrim=\"bottom\" scrim-strength=\"strong\" scrim-edge=\"flush\" focus=\"0.65 0.4\">\n <img src=\"/assets/focus-picker-unsplash.jpg\" alt=\"A woman standing in a sunlit field with her back to the camera\" />\n <h2 slot=\"overlay\">Grow somewhere wonderful</h2>\n </esp-image>\n</esp-section>\n<esp-section>\n <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>\n</esp-section>\n```\n\n`flush` is inert unless the *resolved* scrim is directional — an `auto`\nscrim that resolves to `top` or `bottom` picks it up, while `none`, `flat`,\nand `radial` ignore it, having no anchored edge for it to close.\n\nIt is its own attribute rather than a fourth `scrim-strength` because the two\nare independent: `scrim-strength` is how much veil sits over the subject,\n`scrim-edge` is whether the anchored edge completes. A light veil with a\nclosed edge is exactly the composition this exists for, and a strength step\ncould not express it.\n\nA seamless tile can bleed across the join too — any `background-image` value,\na tile the application serves, a data URI, or a gradient. Hand the edge the\nsame background the adjoining section paints and it tiles up over the\nphotograph, fading out over the same band:\n\n```html\n<esp-image\n banner\n ratio=\"3/1\"\n scrim=\"bottom\"\n scrim-edge=\"flush\"\n 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%;\"\n>\n <img src=\"/assets/focus-picker-unsplash.jpg\" alt=\"A woman standing in a sunlit field with her back to the camera\" />\n</esp-image>\n```\n\nThe component anchors its own side of the tile to the scrim's edge, so the\nbanner's last tile row ends exactly at the join. Continuity into the section\nbelow is the consumer's half of the contract: the same `background-size`,\nand a `top`-anchored `background-position` on the adjoining band.\n\nEvery procedural preset supports `fine`, `medium`, and `coarse` scales.\n\n### Dots\n\nA uniform tiled ink screen (shown at `fine`):\n\n```html\n<esp-image banner ratio=\"3/1\" texture=\"dots\" texture-scale=\"fine\"><img src=\"/assets/canvas-bg.jpg\" alt=\"A colorful floral pattern\" /></esp-image>\n```\n\n### Halftone\n\nThe dot screen ramping toward the content edge (shown at `medium`):\n\n```html\n<esp-image banner ratio=\"3/1\" texture=\"halftone\" texture-scale=\"medium\"><img src=\"/assets/canvas-bg.jpg\" alt=\"A colorful floral pattern\" /></esp-image>\n```\n\n### Paper\n\nSoft fractal-noise mottle, like printed stock (shown at `coarse`):\n\n```html\n<esp-image banner ratio=\"3/1\" texture=\"paper\" texture-scale=\"coarse\"><img src=\"/assets/canvas-bg.jpg\" alt=\"A colorful floral pattern\" /></esp-image>\n```\n\n### Grain\n\nHigh-frequency film grain that also kills banding (shown at `fine`):\n\n```html\n<esp-image banner ratio=\"3/1\" texture=\"grain\" texture-scale=\"fine\"><img src=\"/assets/canvas-bg.jpg\" alt=\"A colorful floral pattern\" /></esp-image>\n```\n\n### Grunge\n\nThresholded turbulence, a distressed ink wash (shown at `medium`):\n\n```html\n<esp-image banner ratio=\"3/1\" texture=\"grunge\" texture-scale=\"medium\"><img src=\"/assets/canvas-bg.jpg\" alt=\"A colorful floral pattern\" /></esp-image>\n```\n\n### Scanlines\n\nHairline rules along the block axis (shown at `coarse`):\n\n```html\n<esp-image banner ratio=\"3/1\" texture=\"scanlines\" texture-scale=\"coarse\"><img src=\"/assets/canvas-bg.jpg\" alt=\"A colorful floral pattern\" /></esp-image>\n```\n\n### Duotone\n\nThe photo mapped onto two theme-derived hues:\n\n```html\n<esp-image banner ratio=\"3/1\" texture=\"duotone\"><img src=\"/assets/canvas-bg.jpg\" alt=\"A colorful floral pattern\" /></esp-image>\n```\n\n### Consumer texture tokens\n\nAny `background-image` value — a tile the application serves, a data URI,\nor a gradient — can be wired directly through the public consumer-image\ntoken path (the optional `@taprootio/espalier/textures/banner-textures.css`\nstylesheet ships `.esp-texture-paper`/`-grain`/`-grunge` classes with the\ntiles inlined as data URIs, so nothing needs asset configuration):\n\n```html\n<esp-image\n banner\n ratio=\"3/1\"\n style=\"--esp-image-texture-image: repeating-linear-gradient(45deg, oklch(0.1 0 0 / 0.2) 0 2px, transparent 2px 7px); --esp-image-texture-blend-mode: multiply;\"\n>\n <img src=\"/assets/canvas-bg.jpg\" alt=\"A colorful floral pattern\" />\n</esp-image>\n```\n\n### Registered textures\n\nWiring those tokens on every banner is verbose and not reusable. The\napplication's texture vocabulary lives on [esp-root](/components/root):\nregister the look once — `registerImageTexture(name, definition)` from\nthe module, or the root's `registerTexture` static in script-only\ncontexts — and any banner under the root selects it by name. Live\nbanners re-render on registration, so markup order and registration\norder never race, and the root's `registeredTextures()` enumerates the\npresets for an editor's texture picker:\n\n```html\n<esp-image id=\"registered-texture-banner\" banner ratio=\"3/1\" texture=\"pressed-paper\">\n <img src=\"/assets/canvas-bg.jpg\" alt=\"A colorful floral pattern\" />\n</esp-image>\n<script>\n const banner = findById(\"registered-texture-banner\");\n const root = banner.closest(\"esp-root\");\n root.constructor.registerTexture(\"pressed-paper\", {\n image:\n \"repeating-linear-gradient(-45deg, oklch(1 0 0 / 0.3) 0 3px, transparent 3px 9px)\",\n blendMode: \"soft-light\",\n opacity: 0.9,\n });\n</script>\n```",
36653
+ "description": "A responsive image with a native `<picture>` and an optional\nfocal-point-aware banner mode.\n\n`esp-image` accepts two authoring shapes:\n\n1. **Project a light-DOM `<picture>`** — the recommended form for published /\n server-rendered markup. Provide a real `<picture>` (or a bare `<img>`) as\n a child and `esp-image` projects it through a `<slot>`, adding only the\n layout box, border, and lightbox wiring. Because the image lives in the\n served HTML it is discoverable by the preload scanner and visible to\n crawlers, and it is fetched exactly once.\n2. **Declare [esp-image-option](/components/image/image-option) children** —\n the editor / fallback form. `esp-image` builds a shadow `<picture>` and\n `srcset` from them. Use `local-image` when an editor needs a blob preview.\n\nSource precedence is `local-image` → a projected `<picture>` / `<img>` →\n`esp-image-option` children. In every mode the host reserves its layout box\nup front from `original-width` / `original-height`, when supplied.\n\n```html\n<!-- Projected native <picture> — published markup, crawlable + preloadable -->\n<esp-image original-width=\"1200\" original-height=\"800\" caption=\"A scenic mountain lake\">\n <picture>\n <source\n srcset=\"https://picsum.photos/600/400 400w, https://picsum.photos/900/600 800w, https://picsum.photos/1200/800 1200w\"\n sizes=\"100vw\"\n />\n <img src=\"https://picsum.photos/1200/800\" alt=\"A scenic mountain lake\" />\n </picture>\n</esp-image>\n```\n\n```html\n<!-- esp-image-option children — editor / fallback form -->\n<esp-image\n original-width=\"1200\"\n original-height=\"800\"\n low-res=\"https://picsum.photos/300/200\"\n caption=\"A scenic mountain lake\"\n sizes=\"100vw\"\n>\n <esp-image-option width=\"400\" url=\"https://picsum.photos/600/400\"></esp-image-option>\n <esp-image-option width=\"800\" url=\"https://picsum.photos/900/600\"></esp-image-option>\n <esp-image-option width=\"1200\" url=\"https://picsum.photos/1200/800\"></esp-image-option>\n</esp-image>\n```\n\nOnce upgraded, the component measures its own width and updates `sizes` so a\ncard or gallery column does not over-fetch. The `sizes` attribute covers the\nserver-rendered initial fetch and the brief window before measurement:\n\n```html\n<esp-image\n original-width=\"1200\"\n original-height=\"800\"\n low-res=\"https://picsum.photos/300/200\"\n sizes=\"(max-width: 600px) 100vw, 320px\"\n style=\"max-width: 320px;\"\n>\n <esp-image-option width=\"320\" url=\"https://picsum.photos/640/427\"></esp-image-option>\n <esp-image-option width=\"640\" url=\"https://picsum.photos/960/640\"></esp-image-option>\n</esp-image>\n```\n\nFor a long projected lazy-image list, `defer-offscreen` keeps the served\n`sizes` hint until an image approaches the viewport. This avoids replacing a\nsmall fallback selection with a larger candidate before the image can be\nseen. Eager projected images and every non-projected source keep the normal\nimmediate measured-size behavior.\n\nBanner mode gives all three source modes the same focal position, scrim,\ntexture, and semantic overlay stack. `ratio` applies at wide widths and\n`compact-ratio` below the component-relative compact threshold.\n\n```html\n<esp-image\n banner\n ratio=\"3/1\"\n compact-ratio=\"3/2\"\n focus=\"0.65 0.4\"\n scrim=\"auto\"\n scrim-strength=\"medium\"\n content-position=\"bottom-start\"\n>\n <img src=\"/assets/focus-picker-unsplash.jpg\" alt=\"A woman standing in a sunlit field with her back to the camera\" />\n <h2 slot=\"overlay\">Grow somewhere wonderful</h2>\n</esp-image>\n```\n\n`banner-scheme` sets the banner's polarity and defaults to the active\nscheme: a light page lightens the image behind dark text, a dark page\ndarkens it behind light text. Fix either polarity explicitly when a banner\nmust not follow the page scheme:\n\n```html\n<esp-image banner ratio=\"3/1\" banner-scheme=\"light\" focus=\"0.65 0.4\">\n <img src=\"/assets/focus-picker-unsplash.jpg\" alt=\"A woman standing in a sunlit field with her back to the camera\" />\n <h2 slot=\"overlay\">Light banner: dark ink on a lifted image</h2>\n</esp-image>\n<esp-image banner ratio=\"3/1\" banner-scheme=\"dark\" focus=\"0.65 0.4\">\n <img src=\"/assets/focus-picker-unsplash.jpg\" alt=\"A woman standing in a sunlit field with her back to the camera\" />\n <h2 slot=\"overlay\">Dark banner: light ink on a deepened image</h2>\n</esp-image>\n```\n\n### The scrim follows its content\n\nA directional scrim holds full strength over at least the first 20% of the\nframe, and further when the overlay content reaches past it, so every line\nof a long heading on a short card cover sits on the full-strength scrim. A\nradial scrim grows an opaque core that covers centered content. Overlays\nthat already fit the 20% band render exactly as before.\n\n```html\n<div style=\"max-width: 24rem\">\n <esp-image banner ratio=\"3/1\" compact-ratio=\"16/9\" focus=\"0.65 0.4\">\n <img src=\"/assets/focus-picker-unsplash.jpg\" alt=\"A woman standing in a sunlit field with her back to the camera\" />\n <h3 slot=\"overlay\">A three-line title stays on the scrim however short the cover gets</h3>\n </esp-image>\n</div>\n```\n\n### Meeting an adjoining band\n\nA directional scrim is capped by `scrim-strength`, so even `strong` leaves\nroughly a quarter of the photograph showing at the very edge it is anchored\nto. `scrim-edge=\"flush\"` paints that edge at full alpha and relaxes to the\nordinary strength over `--esp-image-scrim-edge-depth` (12% of the gradient\naxis by default), so a banner dissolves into a section band directly beneath\nit with no seam. Above that band the scrim is unchanged — `flush` is an edge\ntreatment, not a heavier veil over the subject.\n\nThe join is exact because both sides paint the same token: when\n`banner-scheme` resolves to the page scheme the scrim ink *is*\n`--esp-color-background`, which is what [esp-section](/components/section)\npaints its band with. Put the banner in a section with no padding, directly\nabove a section in the same context:\n\n```html\n<esp-section style=\"--esp-section-max-width: none; --esp-section-padding-inline: 0; --esp-section-padding-block: 0;\">\n <esp-image banner ratio=\"3/1\" scrim=\"bottom\" scrim-strength=\"strong\" scrim-edge=\"flush\" focus=\"0.65 0.4\">\n <img src=\"/assets/focus-picker-unsplash.jpg\" alt=\"A woman standing in a sunlit field with her back to the camera\" />\n <h2 slot=\"overlay\">Grow somewhere wonderful</h2>\n </esp-image>\n</esp-section>\n<esp-section>\n <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>\n</esp-section>\n```\n\n`flush` is inert unless the *resolved* scrim is directional — an `auto`\nscrim that resolves to `top` or `bottom` picks it up, while `none`, `flat`,\nand `radial` ignore it, having no anchored edge for it to close.\n\nIt is its own attribute rather than a fourth `scrim-strength` because the two\nare independent: `scrim-strength` is how much veil sits over the subject,\n`scrim-edge` is whether the anchored edge completes. A light veil with a\nclosed edge is exactly the composition this exists for, and a strength step\ncould not express it.\n\nA seamless tile can bleed across the join too — any `background-image` value,\na tile the application serves, a data URI, or a gradient. Hand the edge the\nsame background the adjoining section paints and it tiles up over the\nphotograph, fading out over the same band:\n\n```html\n<esp-image\n banner\n ratio=\"3/1\"\n scrim=\"bottom\"\n scrim-edge=\"flush\"\n 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%;\"\n>\n <img src=\"/assets/focus-picker-unsplash.jpg\" alt=\"A woman standing in a sunlit field with her back to the camera\" />\n</esp-image>\n```\n\nThe component anchors its own side of the tile to the scrim's edge, so the\nbanner's last tile row ends exactly at the join. Continuity into the section\nbelow is the consumer's half of the contract: the same `background-size`,\nand a `top`-anchored `background-position` on the adjoining band.\n\nEvery procedural preset supports `fine`, `medium`, and `coarse` scales.\n\n### Dots\n\nA uniform tiled ink screen (shown at `fine`):\n\n```html\n<esp-image banner ratio=\"3/1\" texture=\"dots\" texture-scale=\"fine\"><img src=\"/assets/canvas-bg.jpg\" alt=\"A colorful floral pattern\" /></esp-image>\n```\n\n### Halftone\n\nThe dot screen ramping toward the content edge (shown at `medium`):\n\n```html\n<esp-image banner ratio=\"3/1\" texture=\"halftone\" texture-scale=\"medium\"><img src=\"/assets/canvas-bg.jpg\" alt=\"A colorful floral pattern\" /></esp-image>\n```\n\n### Paper\n\nSoft fractal-noise mottle, like printed stock (shown at `coarse`):\n\n```html\n<esp-image banner ratio=\"3/1\" texture=\"paper\" texture-scale=\"coarse\"><img src=\"/assets/canvas-bg.jpg\" alt=\"A colorful floral pattern\" /></esp-image>\n```\n\n### Grain\n\nHigh-frequency film grain that also kills banding (shown at `fine`):\n\n```html\n<esp-image banner ratio=\"3/1\" texture=\"grain\" texture-scale=\"fine\"><img src=\"/assets/canvas-bg.jpg\" alt=\"A colorful floral pattern\" /></esp-image>\n```\n\n### Grunge\n\nThresholded turbulence, a distressed ink wash (shown at `medium`):\n\n```html\n<esp-image banner ratio=\"3/1\" texture=\"grunge\" texture-scale=\"medium\"><img src=\"/assets/canvas-bg.jpg\" alt=\"A colorful floral pattern\" /></esp-image>\n```\n\n### Scanlines\n\nHairline rules along the block axis (shown at `coarse`):\n\n```html\n<esp-image banner ratio=\"3/1\" texture=\"scanlines\" texture-scale=\"coarse\"><img src=\"/assets/canvas-bg.jpg\" alt=\"A colorful floral pattern\" /></esp-image>\n```\n\n### Duotone\n\nThe photo mapped onto two theme-derived hues:\n\n```html\n<esp-image banner ratio=\"3/1\" texture=\"duotone\"><img src=\"/assets/canvas-bg.jpg\" alt=\"A colorful floral pattern\" /></esp-image>\n```\n\n### Consumer texture tokens\n\nAny `background-image` value — a tile the application serves, a data URI,\nor a gradient — can be wired directly through the public consumer-image\ntoken path (the optional `@taprootio/espalier/textures/banner-textures.css`\nstylesheet ships `.esp-texture-paper`/`-grain`/`-grunge` classes with the\ntiles inlined as data URIs, so nothing needs asset configuration):\n\n```html\n<esp-image\n banner\n ratio=\"3/1\"\n style=\"--esp-image-texture-image: repeating-linear-gradient(45deg, oklch(0.1 0 0 / 0.2) 0 2px, transparent 2px 7px); --esp-image-texture-blend-mode: multiply;\"\n>\n <img src=\"/assets/canvas-bg.jpg\" alt=\"A colorful floral pattern\" />\n</esp-image>\n```\n\n### Registered textures\n\nWiring those tokens on every banner is verbose and not reusable. The\napplication's texture vocabulary lives on [esp-root](/components/root):\nregister the look once — `registerImageTexture(name, definition)` from\nthe module, or the root's `registerTexture` static in script-only\ncontexts — and any banner under the root selects it by name. Live\nbanners re-render on registration, so markup order and registration\norder never race, and the root's `registeredTextures()` enumerates the\npresets for an editor's texture picker:\n\n```html\n<esp-image id=\"registered-texture-banner\" banner ratio=\"3/1\" texture=\"pressed-paper\">\n <img src=\"/assets/canvas-bg.jpg\" alt=\"A colorful floral pattern\" />\n</esp-image>\n<script>\n const banner = findById(\"registered-texture-banner\");\n const root = banner.closest(\"esp-root\");\n root.constructor.registerTexture(\"pressed-paper\", {\n image:\n \"repeating-linear-gradient(-45deg, oklch(1 0 0 / 0.3) 0 3px, transparent 3px 9px)\",\n blendMode: \"soft-light\",\n opacity: 0.9,\n });\n</script>\n```",
36654
36654
  "name": "EspalierImage",
36655
36655
  "cssProperties": [
36656
36656
  {
@@ -37099,6 +37099,26 @@
37099
37099
  "privacy": "private",
37100
37100
  "default": "null"
37101
37101
  },
37102
+ {
37103
+ "kind": "field",
37104
+ "name": "scrimObserver",
37105
+ "type": {
37106
+ "text": "ResizeObserver | null"
37107
+ },
37108
+ "privacy": "private",
37109
+ "default": "null",
37110
+ "description": "Watches everything the scrim's geometry depends on; see `observeScrimTargets`."
37111
+ },
37112
+ {
37113
+ "kind": "field",
37114
+ "name": "appliedScrimReach",
37115
+ "type": {
37116
+ "text": "number"
37117
+ },
37118
+ "privacy": "private",
37119
+ "default": "0",
37120
+ "description": "The reach last written to the frame, so an unchanged measurement writes nothing."
37121
+ },
37102
37122
  {
37103
37123
  "kind": "field",
37104
37124
  "name": "offscreenObserver",
@@ -37224,6 +37244,38 @@
37224
37244
  }
37225
37245
  }
37226
37246
  },
37247
+ {
37248
+ "kind": "method",
37249
+ "name": "observeScrimTargets",
37250
+ "privacy": "private",
37251
+ "return": {
37252
+ "type": {
37253
+ "text": "void"
37254
+ }
37255
+ },
37256
+ "description": "(Re)arm the observer the scrim geometry depends on: the overlay region,\nwhose content box tracks the frame (it is inset to it) and shrinks with\n`--esp-image-overlay-padding`, and every element assigned to the overlay\nslot, which is what resizes when fonts load or text changes.\n\nObserving a target always delivers a first notification after the next\nlayout, so re-arming is also how a change that moves content without\nresizing anything (`content-position`, `scrim`) schedules a measurement,\nwithout reading layout here."
37257
+ },
37258
+ {
37259
+ "kind": "method",
37260
+ "name": "overlayElements",
37261
+ "privacy": "private",
37262
+ "return": {
37263
+ "type": {
37264
+ "text": "Element[]"
37265
+ }
37266
+ }
37267
+ },
37268
+ {
37269
+ "kind": "method",
37270
+ "name": "measureScrimReach",
37271
+ "privacy": "private",
37272
+ "return": {
37273
+ "type": {
37274
+ "text": "void"
37275
+ }
37276
+ },
37277
+ "description": "Measure the overlay content against the frame and hand the result to CSS\nas `--_esp-image-scrim-reach`. This only runs from the observer's\ncallback, after layout, so the reads force nothing; the one write moves\ngradient stops and cannot resize anything observed, so it cannot loop."
37278
+ },
37227
37279
  {
37228
37280
  "kind": "method",
37229
37281
  "name": "effectiveSizes",
@@ -38573,6 +38625,103 @@
38573
38625
  }
38574
38626
  ]
38575
38627
  },
38628
+ {
38629
+ "kind": "javascript-module",
38630
+ "path": "dist/image/scrim-reach.js",
38631
+ "declarations": [
38632
+ {
38633
+ "kind": "function",
38634
+ "name": "isMeasuredScrim",
38635
+ "return": {
38636
+ "type": {
38637
+ "text": "scrim is MeasuredScrim"
38638
+ }
38639
+ },
38640
+ "parameters": [
38641
+ {
38642
+ "name": "scrim",
38643
+ "type": {
38644
+ "text": "string"
38645
+ }
38646
+ }
38647
+ ]
38648
+ },
38649
+ {
38650
+ "kind": "function",
38651
+ "name": "unionScrimBox",
38652
+ "return": {
38653
+ "type": {
38654
+ "text": "ScrimBox | null"
38655
+ }
38656
+ },
38657
+ "parameters": [
38658
+ {
38659
+ "name": "boxes",
38660
+ "type": {
38661
+ "text": "Iterable<ScrimBox | null>"
38662
+ }
38663
+ }
38664
+ ],
38665
+ "description": "The smallest box containing every non-empty box given, or `null` when none is."
38666
+ },
38667
+ {
38668
+ "kind": "function",
38669
+ "name": "scrimReach",
38670
+ "return": {
38671
+ "type": {
38672
+ "text": "number"
38673
+ }
38674
+ },
38675
+ "parameters": [
38676
+ {
38677
+ "name": "scrim",
38678
+ "type": {
38679
+ "text": "MeasuredScrim"
38680
+ }
38681
+ },
38682
+ {
38683
+ "name": "frame",
38684
+ "type": {
38685
+ "text": "ScrimBox"
38686
+ }
38687
+ },
38688
+ {
38689
+ "name": "content",
38690
+ "type": {
38691
+ "text": "ScrimBox | null"
38692
+ }
38693
+ }
38694
+ ],
38695
+ "description": "How far a scrim's full-strength region must reach to cover `content` inside\n`frame`, as a percentage of the scrim's own gradient line: the frame's block\nor inline size for a directional scrim, the center-to-corner ray for\n`radial`. `0` leaves the recipe exactly as authored.\n\nA directional scrim only follows content that starts in its own half of the\nframe, and a radial scrim only content that covers the frame's center, so an\nexplicit `top` scrim over bottom-anchored content, or `radial` over a corner,\nkeeps its authored shape instead of turning into a full-frame veil. The result is rounded\nup so the covered region never falls a fraction of a pixel short."
38696
+ }
38697
+ ],
38698
+ "exports": [
38699
+ {
38700
+ "kind": "js",
38701
+ "name": "isMeasuredScrim",
38702
+ "declaration": {
38703
+ "name": "isMeasuredScrim",
38704
+ "module": "dist/image/scrim-reach.js"
38705
+ }
38706
+ },
38707
+ {
38708
+ "kind": "js",
38709
+ "name": "unionScrimBox",
38710
+ "declaration": {
38711
+ "name": "unionScrimBox",
38712
+ "module": "dist/image/scrim-reach.js"
38713
+ }
38714
+ },
38715
+ {
38716
+ "kind": "js",
38717
+ "name": "scrimReach",
38718
+ "declaration": {
38719
+ "name": "scrimReach",
38720
+ "module": "dist/image/scrim-reach.js"
38721
+ }
38722
+ }
38723
+ ]
38724
+ },
38576
38725
  {
38577
38726
  "kind": "javascript-module",
38578
38727
  "path": "dist/image-picker/esp-image-picker.js",
@@ -117,6 +117,23 @@ export declare function parseImageRatio(value: string): string | null;
117
117
  * </esp-image>
118
118
  * ```
119
119
  *
120
+ * ### The scrim follows its content
121
+ *
122
+ * A directional scrim holds full strength over at least the first 20% of the
123
+ * frame, and further when the overlay content reaches past it, so every line
124
+ * of a long heading on a short card cover sits on the full-strength scrim. A
125
+ * radial scrim grows an opaque core that covers centered content. Overlays
126
+ * that already fit the 20% band render exactly as before.
127
+ *
128
+ * ```html
129
+ * <div style="max-width: 24rem">
130
+ * <esp-image banner ratio="3/1" compact-ratio="16/9" focus="0.65 0.4">
131
+ * <img src="/assets/focus-picker-unsplash.jpg" alt="A woman standing in a sunlit field with her back to the camera" />
132
+ * <h3 slot="overlay">A three-line title stays on the scrim however short the cover gets</h3>
133
+ * </esp-image>
134
+ * </div>
135
+ * ```
136
+ *
120
137
  * ### Meeting an adjoining band
121
138
  *
122
139
  * A directional scrim is capped by `scrim-strength`, so even `strong` leaves
@@ -1,4 +1,4 @@
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
1
+ var n=function(l,e,t,r){var i=arguments.length,a=i<3?e:r===null?r=Object.getOwnPropertyDescriptor(e,t):r,o;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")a=Reflect.decorate(l,e,t,r);else for(var m=l.length-1;m>=0;m--)(o=l[m])&&(a=(i<3?o(a):i>3?o(e,t,a):o(e,t))||a);return i>3&&a&&Object.defineProperty(e,t,a),a},p;import{css as I,html as h,nothing as P}from"lit";import{customElement as E,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 $,visualOverlayStyles as A}from"../shared/visual-overlay.js";import{focusConverter as N,normalizeFocus as W}from"./image-focus.js";import{ensureImageTextureFilters as D,imageTextureFilterId as M}from"./image-texture-filters.js";import{getImageTexture as b,isBuiltInImageTexture as x,subscribeToImageTextureRegistry as B}from"../shared/image-texture-registry.js";import{validImageDimensions as _}from"./image-dimensions.js";import{isMeasuredScrim as q,scrimReach as H,unionScrimBox as U}from"./scrim-reach.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"]),O=new Set(["auto","light","dark"]),j=new Set(["bottom-start","bottom","bottom-end","center","top-start","top","top-end"]);function d(l,e){return{fromAttribute(t){return t&&l.has(t)?t:e},toAttribute(t){return l.has(t)?t:e}}}function R(l){const e=l.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]}`}function T(l){const e=[];for(const t of l)getComputedStyle(t).display==="contents"?e.push(...T(t.children)):e.push(t);return e}function F(l){const e=l.getBoundingClientRect();if(e.width>0||e.height>0)return e;const t=l.ownerDocument.createRange();t.selectNodeContents(l);const r=t.getBoundingClientRect();return r.width>0||r.height>0?r:null}let s=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.scrimObserver=null,this.appliedScrimReach=0,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),this.observeScrimTargets();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(a=>a.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??=B(()=>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(),this.hasUpdated&&this.observeScrimTargets()}disconnectedCallback(){super.disconnectedCallback(),this.unsubscribeTextures?.(),this.unsubscribeTextures=null,this.resizeObserver?.disconnect(),this.resizeObserver=null,this.scrimObserver?.disconnect(),this.scrimObserver=null,this.offscreenObserver?.disconnect(),this.offscreenObserver=null,this.offscreenAdmitted=!1,this.deferredProjectedImage=null}observeScrimTargets(){if(!this.shadowRoot)return;this.scrimObserver??=new ResizeObserver(()=>this.measureScrimReach()),this.scrimObserver.disconnect();const e=this.shadowRoot?.querySelector(".overlay");e&&this.scrimObserver.observe(e);const t=this.shadowRoot?.querySelector('[part="scrim"]');t&&this.scrimObserver.observe(t);for(const r of T(this.overlayElements()))this.scrimObserver.observe(r,{box:"border-box"})}overlayElements(){return this.shadowRoot?.querySelector('slot[name="overlay"]')?.assignedElements({flatten:!0})??[]}measureScrimReach(){const e=this.shadowRoot?.querySelector(".frame");if(!e)return;const t=this.resolvedScrim();let r=0;const i=this.shadowRoot?.querySelector('[part="scrim"]');if(this.banner&&this.overlayHasContent&&i&&q(t)){const a=i.getBoundingClientRect();if(a.width<=0||a.height<=0)return;const o=U(this.overlayElements().map(F));r=H(t,a,o)}r!==this.appliedScrimReach&&(this.appliedScrimReach=r,r>0?e.style.setProperty("--_esp-image-scrim-reach",`${r}%`):e.style.removeProperty("--_esp-image-scrim-reach"))}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 a=t.getAttribute("type")??"",o=e.get(a)??[];o.push({url:r,width:i}),e.set(a,o)}return Array.from(e,([t,r])=>({type:t,srcset:r.sort((i,a)=>i.width-a.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=R(this.ratio);return(this.compact?R(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")&&!O.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")&&!j.has(this.contentPosition)&&(this.contentPosition="bottom-start")}willUpdate(e){super.willUpdate(e),this.normalizeEnums(e);const t=W(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 a=this.resolvedAspectRatio();a?this.style.setProperty("--_esp-image-aspect-ratio",a):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 m=x(o)?void 0:b(o),C=[["--_esp-image-registered-image",m?.image],["--_esp-image-registered-size",m?.size],["--_esp-image-registered-repeat",m?.repeat],["--_esp-image-registered-position",m?.position],["--_esp-image-registered-opacity",m?.opacity?.toString()],["--_esp-image-registered-blend-mode",m?.blendMode]];for(const[g,f]of C)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)),this.observeScrimTargets()}updated(e){if(super.updated(e),this.syncOffscreenDeferral(),(e.has("banner")||e.has("scrim")||e.has("contentPosition")||e.has("overlayHasContent")||e.has("compact"))&&this.observeScrimTargets(),["paper","grain","grunge"].includes(this.resolvedTexture())&&this.renderRoot&&D(this.renderRoot),this.renderedWidth>0){const o=this.shadowRoot?.querySelector(".compact-sentinel")?.getBoundingClientRect().width??0,m=o>0&&this.renderedWidth<o;m!==this.compact&&(this.compact=m)}if(this.renderedWidth<=0||this.shouldDeferProjectedSizes())return;const t=this.findProjected();if(!t)return;const r=this.effectiveSizes();if(t.tagName==="PICTURE")for(const a of t.querySelectorAll("source"))a.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"
@@ -9,7 +9,7 @@ var n=function(d,e,t,r){var i=arguments.length,s=i<3?e:r===null?r=Object.getOwnP
9
9
  <source
10
10
  srcset=${r.srcset}
11
11
  sizes=${this.effectiveSizes()}
12
- type=${r.type||C}
12
+ type=${r.type||P}
13
13
  />
14
14
  `)}
15
15
  <img src=${this.imageUrl} alt=${this.caption} decoding="async" loading=${this.loading} />
@@ -28,13 +28,13 @@ var n=function(d,e,t,r){var i=arguments.length,s=i<3?e:r===null?r=Object.getOwnP
28
28
  data-content-position=${this.contentPosition}
29
29
  >
30
30
  <div part="image" class="image">${this.renderMedia()}</div>
31
- ${E({className:"banner-layers",imagePart:"texture",scrimPart:"scrim",texturePart:"texture"})}
31
+ ${$({className:"banner-layers",imagePart:"texture",scrimPart:"scrim",texturePart:"texture"})}
32
32
  <div part="overlay" class="overlay" ?hidden=${!this.banner||!this.overlayHasContent}>
33
33
  <slot name="overlay" @slotchange=${this.handleSlotChange}></slot>
34
34
  </div>
35
35
  <span class="compact-sentinel" aria-hidden="true"></span>
36
36
  </div>
37
- `}static ensureProjectionStyles(e){const t=e.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&"host"in e?e:e.nodeType===Node.DOCUMENT_NODE?e.head:e.ownerDocument?.head??document.head,r=Array.from(t.querySelectorAll("style[data-esp-image-projection-styles]")),i=r.find(o=>o.textContent===p.projectionStyles);if(i){for(const o of r)o!==i&&o.remove();return}for(const o of r)o.remove();const s=t.ownerDocument.createElement("style");s.dataset.espImageProjectionStyles="",s.textContent=p.projectionStyles,t.append(s)}};a.projectionStyles=`
37
+ `}static ensureProjectionStyles(e){const t=e.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&"host"in e?e:e.nodeType===Node.DOCUMENT_NODE?e.head:e.ownerDocument?.head??document.head,r=Array.from(t.querySelectorAll("style[data-esp-image-projection-styles]")),i=r.find(o=>o.textContent===p.projectionStyles);if(i){for(const o of r)o!==i&&o.remove();return}for(const o of r)o.remove();const a=t.ownerDocument.createElement("style");a.dataset.espImageProjectionStyles="",a.textContent=p.projectionStyles,t.append(a)}};s.projectionStyles=`
38
38
  esp-image > picture,
39
39
  esp-image > img {
40
40
  display: block;
@@ -50,7 +50,7 @@ var n=function(d,e,t,r){var i=arguments.length,s=i<3?e:r===null?r=Object.getOwnP
50
50
  object-fit: var(--_esp-image-object-fit, cover);
51
51
  object-position: var(--esp-image-object-position, var(--_esp-image-object-position, center));
52
52
  }
53
- `,a.styles=[...v.styles,T`
53
+ `,s.styles=[...v.styles,I`
54
54
  :host {
55
55
  display: block;
56
56
  aspect-ratio: var(--_esp-image-aspect-ratio, auto);
@@ -176,6 +176,18 @@ var n=function(d,e,t,r){var i=arguments.length,s=i<3?e:r===null?r=Object.getOwnP
176
176
  --_esp-image-scrim-opacity: 0.76;
177
177
  }
178
178
 
179
+
180
+ .frame {
181
+ --_esp-image-scrim-reach: 0%;
182
+ --_esp-image-scrim-plateau: max(20%, var(--_esp-image-scrim-reach));
183
+ --_esp-image-scrim-fade-mid: calc(
184
+ var(--_esp-image-scrim-plateau) + (100% - var(--_esp-image-scrim-plateau)) * 0.35
185
+ );
186
+ --_esp-image-scrim-fade-end: calc(
187
+ var(--_esp-image-scrim-plateau) + (100% - var(--_esp-image-scrim-plateau)) * 0.725
188
+ );
189
+ }
190
+
179
191
  .frame[data-scrim="none"] {
180
192
  --_esp-overlay-scrim: none;
181
193
  }
@@ -185,42 +197,54 @@ var n=function(d,e,t,r){var i=arguments.length,s=i<3?e:r===null?r=Object.getOwnP
185
197
  .frame[data-scrim="top"] {
186
198
  --_esp-overlay-scrim: linear-gradient(
187
199
  to bottom,
188
- var(--_esp-image-scrim-resolved) 0% 20%,
189
- color-mix(in oklch, var(--_esp-image-scrim-resolved) 68%, transparent) 48%,
190
- transparent 78%
200
+ var(--_esp-image-scrim-resolved) 0% var(--_esp-image-scrim-plateau),
201
+ color-mix(in oklch, var(--_esp-image-scrim-resolved) 68%, transparent)
202
+ var(--_esp-image-scrim-fade-mid),
203
+ transparent var(--_esp-image-scrim-fade-end)
191
204
  );
192
205
  }
193
206
  .frame[data-scrim="bottom"] {
194
207
  --_esp-overlay-scrim: linear-gradient(
195
208
  to top,
196
- var(--_esp-image-scrim-resolved) 0% 20%,
197
- color-mix(in oklch, var(--_esp-image-scrim-resolved) 68%, transparent) 48%,
198
- transparent 78%
209
+ var(--_esp-image-scrim-resolved) 0% var(--_esp-image-scrim-plateau),
210
+ color-mix(in oklch, var(--_esp-image-scrim-resolved) 68%, transparent)
211
+ var(--_esp-image-scrim-fade-mid),
212
+ transparent var(--_esp-image-scrim-fade-end)
199
213
  );
200
214
  }
201
215
  .frame[data-scrim="left"] {
202
216
  --_esp-overlay-scrim: linear-gradient(
203
217
  to right,
204
- var(--_esp-image-scrim-resolved) 0% 20%,
205
- color-mix(in oklch, var(--_esp-image-scrim-resolved) 68%, transparent) 48%,
206
- transparent 78%
218
+ var(--_esp-image-scrim-resolved) 0% var(--_esp-image-scrim-plateau),
219
+ color-mix(in oklch, var(--_esp-image-scrim-resolved) 68%, transparent)
220
+ var(--_esp-image-scrim-fade-mid),
221
+ transparent var(--_esp-image-scrim-fade-end)
207
222
  );
208
223
  }
209
224
  .frame[data-scrim="right"] {
210
225
  --_esp-overlay-scrim: linear-gradient(
211
226
  to left,
212
- var(--_esp-image-scrim-resolved) 0% 20%,
213
- color-mix(in oklch, var(--_esp-image-scrim-resolved) 68%, transparent) 48%,
214
- transparent 78%
227
+ var(--_esp-image-scrim-resolved) 0% var(--_esp-image-scrim-plateau),
228
+ color-mix(in oklch, var(--_esp-image-scrim-resolved) 68%, transparent)
229
+ var(--_esp-image-scrim-fade-mid),
230
+ transparent var(--_esp-image-scrim-fade-end)
215
231
  );
216
232
  }
233
+
217
234
  .frame[data-scrim="radial"] {
218
235
  --_esp-overlay-scrim: radial-gradient(
219
236
  circle at center,
220
- var(--_esp-image-scrim-resolved) 0%,
221
- color-mix(in oklch, var(--_esp-image-scrim-resolved) 72%, transparent) 34%,
222
- color-mix(in oklch, var(--_esp-image-scrim-resolved) 28%, transparent) 64%,
223
- transparent 82%
237
+ var(--_esp-image-scrim-resolved) 0% var(--_esp-image-scrim-reach),
238
+ color-mix(in oklch, var(--_esp-image-scrim-resolved) 72%, transparent)
239
+ calc(
240
+ var(--_esp-image-scrim-reach) + (100% - var(--_esp-image-scrim-reach)) * 0.34
241
+ ),
242
+ color-mix(in oklch, var(--_esp-image-scrim-resolved) 28%, transparent)
243
+ calc(
244
+ var(--_esp-image-scrim-reach) + (100% - var(--_esp-image-scrim-reach)) * 0.64
245
+ ),
246
+ transparent
247
+ calc(var(--_esp-image-scrim-reach) + (100% - var(--_esp-image-scrim-reach)) * 0.82)
224
248
  );
225
249
  }
226
250
 
@@ -505,4 +529,4 @@ var n=function(d,e,t,r){var i=arguments.length,s=i<3?e:r===null?r=Object.getOwnP
505
529
  transition: none !important;
506
530
  }
507
531
  }
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};
532
+ `,A],n([c({attribute:"original-height",type:Number})],s.prototype,"originalHeight",void 0),n([c({attribute:"original-width",type:Number})],s.prototype,"originalWidth",void 0),n([c({attribute:"low-res",type:String})],s.prototype,"imageUrl",void 0),n([c({attribute:"local-image",type:String})],s.prototype,"localImage",void 0),n([c({type:String})],s.prototype,"caption",void 0),n([c({attribute:"sizes",type:String})],s.prototype,"sizes",void 0),n([c({attribute:"loading",type:String})],s.prototype,"loading",void 0),n([c({attribute:"defer-offscreen",type:Boolean})],s.prototype,"deferOffscreen",void 0),n([c({type:Boolean,reflect:!0})],s.prototype,"banner",void 0),n([c({attribute:"banner-scheme",converter:d(O,"auto"),reflect:!0})],s.prototype,"bannerScheme",void 0),n([c({converter:d(k,"auto"),reflect:!0})],s.prototype,"scrim",void 0),n([c({attribute:"scrim-strength",converter:d(w,"medium"),reflect:!0})],s.prototype,"scrimStrength",void 0),n([c({attribute:"scrim-edge",converter:d(S,"none"),reflect:!0})],s.prototype,"scrimEdge",void 0),n([c({type:String,reflect:!0})],s.prototype,"texture",void 0),n([c({attribute:"texture-scale",converter:d(z,"medium"),reflect:!0})],s.prototype,"textureScale",void 0),n([c({attribute:"content-position",converter:d(j,"bottom-start"),reflect:!0})],s.prototype,"contentPosition",void 0),n([c({attribute:"focus",converter:N,reflect:!0})],s.prototype,"focusPoint",void 0),n([c({type:String})],s.prototype,"ratio",void 0),n([c({attribute:"compact-ratio",type:String})],s.prototype,"compactRatio",void 0),n([u()],s.prototype,"renderedWidth",void 0),n([u()],s.prototype,"compact",void 0),n([u()],s.prototype,"overlayHasContent",void 0),s=p=n([E("esp-image")],s);export{s as EspalierImage,R as parseImageRatio};
@@ -0,0 +1 @@
1
+ const M=new Set(["top","bottom","left","right","radial"]);function u(h){return M.has(h)}function f(h){let o=null;for(const t of h)!t||t.right<=t.left||t.bottom<=t.top||(o=o?{left:Math.min(o.left,t.left),top:Math.min(o.top,t.top),right:Math.max(o.right,t.right),bottom:Math.max(o.bottom,t.bottom)}:{left:t.left,top:t.top,right:t.right,bottom:t.bottom});return o}function x(h,o,t){const l=o.right-o.left,p=o.bottom-o.top;if(!t||l<=0||p<=0)return 0;let b;if(h==="radial"){const i=(o.left+o.right)/2,r=(o.top+o.bottom)/2;if(!(t.left<=i&&i<=t.right&&t.top<=r&&r<=t.bottom))return 0;const e=Math.hypot(l/2,p/2);b=Math.hypot(Math.max(Math.abs(t.left-i),Math.abs(t.right-i)),Math.max(Math.abs(t.top-r),Math.abs(t.bottom-r)))/e}else{const i=h==="top"||h==="bottom"?p:l,[r,a]=h==="bottom"?[o.bottom-t.bottom,o.bottom-t.top]:h==="top"?[t.top-o.top,t.bottom-o.top]:h==="left"?[t.left-o.left,t.right-o.left]:[o.right-t.right,o.right-t.left];if(r>i/2)return 0;b=a/i}const s=Math.min(1,Math.max(0,b))*100;return Math.ceil(s*100-1e-6)/100}export{u as isMeasuredScrim,x as scrimReach,f as unionScrimBox};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taprootio/espalier",
3
- "version": "4.20.0",
3
+ "version": "4.20.1",
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",