@taprootio/espalier 6.2.0 → 6.4.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.
Files changed (65) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +5 -5
  3. package/custom-elements.json +1498 -778
  4. package/dist/action-menu/esp-action-menu-item.js +1 -0
  5. package/dist/action-menu/esp-action-menu.js +4 -3
  6. package/dist/box/esp-box.d.ts +49 -3
  7. package/dist/box/esp-box.js +26 -11
  8. package/dist/button/esp-button.d.ts +4 -4
  9. package/dist/date-picker/helpers/styles.js +1 -0
  10. package/dist/details/esp-details.d.ts +3 -0
  11. package/dist/details/esp-details.js +13 -6
  12. package/dist/dialog/esp-dialog.d.ts +82 -2
  13. package/dist/dialog/esp-dialog.js +71 -59
  14. package/dist/flyout/esp-flyout.d.ts +2 -2
  15. package/dist/flyout/esp-flyout.js +6 -5
  16. package/dist/focus-picker/esp-focus-picker.js +3 -2
  17. package/dist/footer/esp-footer.d.ts +3 -3
  18. package/dist/footer/esp-footer.js +10 -10
  19. package/dist/grid/esp-grid.d.ts +3 -0
  20. package/dist/grid/esp-grid.js +25 -17
  21. package/dist/header/esp-header-button.d.ts +6 -15
  22. package/dist/header/esp-header.d.ts +4 -2
  23. package/dist/header/esp-header.js +8 -7
  24. package/dist/image-picker/esp-image-picker.js +1 -0
  25. package/dist/image-upload/esp-image-upload.js +1 -0
  26. package/dist/info/esp-info.d.ts +3 -1
  27. package/dist/info/esp-info.js +8 -8
  28. package/dist/lightbox/esp-lightbox.d.ts +3 -0
  29. package/dist/lightbox/esp-lightbox.js +12 -9
  30. package/dist/menu/esp-menu-group.js +6 -2
  31. package/dist/menu/esp-menu-item.js +7 -3
  32. package/dist/menu/esp-menu.js +4 -2
  33. package/dist/page/esp-page.d.ts +1 -1
  34. package/dist/page/esp-page.styles.js +1 -0
  35. package/dist/pickers/esp-pick-one.d.ts +1 -2
  36. package/dist/pickers/esp-pick-some.d.ts +1 -1
  37. package/dist/pickers/esp-picker-item.js +9 -5
  38. package/dist/pickers/esp-picker-menu.js +2 -0
  39. package/dist/popover/esp-popover.d.ts +2 -4
  40. package/dist/repeater/esp-repeater.js +1 -0
  41. package/dist/root/esp-root.d.ts +4 -4
  42. package/dist/root/esp-root.js +5 -3
  43. package/dist/row/esp-row.d.ts +1 -2
  44. package/dist/search/esp-search.js +2 -0
  45. package/dist/section/esp-section.d.ts +1 -1
  46. package/dist/section/esp-section.js +2 -1
  47. package/dist/shared/configured-brand.d.ts +11 -37
  48. package/dist/shared/esp-element-base.d.ts +4 -1
  49. package/dist/shared/esp-element-base.js +27 -30
  50. package/dist/shared/justified-layout.d.ts +6 -18
  51. package/dist/shared/surface-layer.d.ts +12 -0
  52. package/dist/shared/surface-layer.js +10 -0
  53. package/dist/shared/theme.d.ts +7 -18
  54. package/dist/slider/esp-slider.d.ts +2 -4
  55. package/dist/stack/esp-stack.d.ts +1 -2
  56. package/dist/switch/esp-switch.d.ts +2 -4
  57. package/dist/tabs/esp-tab-group.d.ts +3 -0
  58. package/dist/tabs/esp-tab-group.js +7 -7
  59. package/dist/tooltip/esp-tooltip.js +1 -0
  60. package/dist/tree/esp-tree.d.ts +0 -5
  61. package/dist/tree/esp-tree.js +3 -0
  62. package/espalier.css-data.json +17 -1
  63. package/espalier.token-manifest.json +25 -1
  64. package/licenses/README.md +18 -15
  65. package/package.json +5 -1
@@ -1967,7 +1967,7 @@
1967
1967
  "name": "--esp-action-menu-item-icon-border-color"
1968
1968
  },
1969
1969
  {
1970
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
1970
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
1971
1971
  "name": "--esp-field-background",
1972
1972
  "inheritedFrom": {
1973
1973
  "name": "EspalierElementBase",
@@ -2909,6 +2909,17 @@
2909
2909
  "description": "Where the optional icon slot is positioned within the item.\n\nUse `\"right\"` for Taproot-style action rows where the icon sits\nin a differentiated trailing cell with a dotted divider.",
2910
2910
  "fieldName": "iconPosition"
2911
2911
  },
2912
+ {
2913
+ "type": {
2914
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
2915
+ },
2916
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
2917
+ "name": "surface-layer",
2918
+ "inheritedFrom": {
2919
+ "name": "EspalierElementBase",
2920
+ "module": "dist/shared/esp-element-base.js"
2921
+ }
2922
+ },
2912
2923
  {
2913
2924
  "name": "scheme",
2914
2925
  "type": {
@@ -3020,7 +3031,7 @@
3020
3031
  "name": "--esp-action-menu-min-width"
3021
3032
  },
3022
3033
  {
3023
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
3034
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
3024
3035
  "name": "--esp-field-background",
3025
3036
  "inheritedFrom": {
3026
3037
  "name": "EspalierElementBase",
@@ -4202,6 +4213,17 @@
4202
4213
  "description": "Controls where item icons appear inside the menu.\n\n`\"left\"` keeps icons before labels. `\"right\"` moves icons into\na trailing cell with a differentiated background and dotted divider.",
4203
4214
  "fieldName": "iconPosition"
4204
4215
  },
4216
+ {
4217
+ "type": {
4218
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
4219
+ },
4220
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
4221
+ "name": "surface-layer",
4222
+ "inheritedFrom": {
4223
+ "name": "EspalierElementBase",
4224
+ "module": "dist/shared/esp-element-base.js"
4225
+ }
4226
+ },
4205
4227
  {
4206
4228
  "name": "scheme",
4207
4229
  "type": {
@@ -4454,7 +4476,7 @@
4454
4476
  "name": "--esp-avatar-size"
4455
4477
  },
4456
4478
  {
4457
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
4479
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
4458
4480
  "name": "--esp-field-background",
4459
4481
  "inheritedFrom": {
4460
4482
  "name": "EspalierElementBase",
@@ -5472,6 +5494,17 @@
5472
5494
  "description": "Referrer policy forwarded to the internal image. Defaults to\n`no-referrer`.",
5473
5495
  "fieldName": "referrerPolicy"
5474
5496
  },
5497
+ {
5498
+ "type": {
5499
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
5500
+ },
5501
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
5502
+ "name": "surface-layer",
5503
+ "inheritedFrom": {
5504
+ "name": "EspalierElementBase",
5505
+ "module": "dist/shared/esp-element-base.js"
5506
+ }
5507
+ },
5475
5508
  {
5476
5509
  "name": "scheme",
5477
5510
  "type": {
@@ -5612,7 +5645,7 @@
5612
5645
  "name": "--esp-avatar-border-radius"
5613
5646
  },
5614
5647
  {
5615
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
5648
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
5616
5649
  "name": "--esp-field-background",
5617
5650
  "inheritedFrom": {
5618
5651
  "name": "EspalierElementBase",
@@ -6521,6 +6554,17 @@
6521
6554
  "description": "Optional presence state passed to the internal avatar.",
6522
6555
  "fieldName": "presence"
6523
6556
  },
6557
+ {
6558
+ "type": {
6559
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
6560
+ },
6561
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
6562
+ "name": "surface-layer",
6563
+ "inheritedFrom": {
6564
+ "name": "EspalierElementBase",
6565
+ "module": "dist/shared/esp-element-base.js"
6566
+ }
6567
+ },
6524
6568
  {
6525
6569
  "name": "scheme",
6526
6570
  "type": {
@@ -6621,7 +6665,7 @@
6621
6665
  "name": "--esp-badge-border-radius"
6622
6666
  },
6623
6667
  {
6624
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
6668
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
6625
6669
  "name": "--esp-field-background",
6626
6670
  "inheritedFrom": {
6627
6671
  "name": "EspalierElementBase",
@@ -7468,6 +7512,17 @@
7468
7512
  "description": "Badge size.",
7469
7513
  "fieldName": "size"
7470
7514
  },
7515
+ {
7516
+ "type": {
7517
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
7518
+ },
7519
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
7520
+ "name": "surface-layer",
7521
+ "inheritedFrom": {
7522
+ "name": "EspalierElementBase",
7523
+ "module": "dist/shared/esp-element-base.js"
7524
+ }
7525
+ },
7471
7526
  {
7472
7527
  "name": "scheme",
7473
7528
  "type": {
@@ -7560,7 +7615,7 @@
7560
7615
  "declarations": [
7561
7616
  {
7562
7617
  "kind": "class",
7563
- "description": "Used as a base to group context visually. `esp-box`\nhas a css part of `box` that can be styled to create cards,\nforms, and other UI elements.\n\n```html\n<esp-box>\n <h2>Box things</h2>\n <p>Things grouped visually.</p>\n</esp-box>\n```\n\n**What esp-box is not:** a full-bleed page band. The box is a *card* —\nit paints its own raised surface (`--esp-color-layer-1`), carries\npadding, and fills its container's height. A band is\n[`esp-section`](/components/section): it paints the local canvas edge\nto edge with a centered well and hosts a theme zone (`context=\"…\"`)\nwith nothing to neutralize. Keep boxes for what they are for — cards,\npanels, and form surfaces — and replace any pre-3.2 \"neutralized box\"\nband with a bare section.",
7618
+ "description": "Groups related content in a themed container. Style its `box` CSS part\nto create cards, forms, or other grouped regions.\n\nA field fills one layer above the surface it sits on.\nUse `surface-layer=\"1|2|3|4\"` to select the default painted layer.\n\n```html\n<esp-box>\n <h2>Box things</h2>\n <p>Things grouped visually.</p>\n</esp-box>\n```\n\n**What esp-box is not:** a full-bleed page band. The box is a *card* —\nit paints its own raised surface (`--esp-color-layer-1`), carries\npadding, and fills its container's height. A band is\n[`esp-section`](/components/section): it paints the local canvas edge\nto edge with a centered well and hosts a theme zone (`context=\"…\"`)\nwith nothing to neutralize. Keep boxes for what they are for — cards,\npanels, and form surfaces — and replace any pre-3.2 \"neutralized box\"\nband with a bare section.\n\nSelect a raised form surface with `surface-layer`; fields follow it automatically.\n\n```html\n<esp-box surface-layer=\"2\">\n <esp-input placeholder=\"Search\"></esp-input>\n <esp-input readonly value=\"A read-only value\"></esp-input>\n</esp-box>\n```",
7564
7619
  "name": "EspalierBox",
7565
7620
  "cssProperties": [
7566
7621
  {
@@ -7584,7 +7639,7 @@
7584
7639
  "name": "--esp-box-shadow"
7585
7640
  },
7586
7641
  {
7587
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
7642
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
7588
7643
  "name": "--esp-field-background",
7589
7644
  "inheritedFrom": {
7590
7645
  "name": "EspalierElementBase",
@@ -7661,7 +7716,7 @@
7661
7716
  },
7662
7717
  "privacy": "public",
7663
7718
  "default": "false",
7664
- "description": "Whether the box should take up the full screen. Useful when\ndisplaying a dialog in full-screen mode.\n\n```html\n<esp-box>\n <h2>Box things</h2>\n <esp-box>\n <h2>Box in box</h2>\n <esp-button label=\"Toggle full screen\"></esp-button>\n </esp-box>\n</esp-box>\n<script>\n let fullScreen = false;\n const theBox = findByTagName(\"esp-box\")[0];\n const theButton = findByTagName(\"esp-button\")[0];\n theButton.addEventListener(\"esp-clicked\", () => {\n fullScreen = !fullScreen;\n\n if(fullScreen) {\n theBox.setAttribute(\"full-screen\", true);\n } else {\n theBox.removeAttribute(\"full-screen\");\n }\n });\n</script>\n```",
7719
+ "description": "Whether the box should take up the full screen. Useful when\ndisplaying a dialog in full-screen mode.\n\n```html\n<esp-box>\n <h2>Box things</h2>\n <esp-box>\n <h2>Box in box</h2>\n <esp-button label=\"Toggle full screen\"></esp-button>\n </esp-box>\n</esp-box>\n<script>\n let fullScreen = false;\n const theBox = findByTagName(\"esp-box\")[0];\n const theButton = findByTagName(\"esp-button\")[0];\n theButton.addEventListener(\"esp-clicked\", () => {\n fullScreen = !fullScreen;\n\n if(fullScreen) {\n theBox.setAttribute(\"full-screen\", true);\n } else {\n theBox.removeAttribute(\"full-screen\");\n }\n });\n</script>\n```\n\nA full-screen box fills the viewport. Inside a\n[bounded dialog](/components/dialog) it fills exactly the dialog's\nrectangle instead: it takes its position and size from the same\n`--esp-dialog-inset-*` custom properties, set on the `esp-dialog` or any\nancestor of the box.\n\n```html\n<style>\n esp-dialog.bounded-box {\n --esp-dialog-inset-block-start: 6rem;\n --esp-dialog-inset-inline-start: 3rem;\n --esp-dialog-inset-inline-end: 3rem;\n --esp-dialog-inset-block-end: 2rem;\n }\n</style>\n<esp-dialog class=\"bounded-box\" full-screen>\n <esp-box full-screen>\n <h2>Box in a bounded dialog</h2>\n <p>This box fills the rectangle the dialog is bounded to.</p>\n <esp-button intent=\"danger\" label=\"Close\"></esp-button>\n </esp-box>\n</esp-dialog>\n<esp-button label=\"Show Dialog\"></esp-button>\n<script>\n const theDialog = findByTagName(\"esp-dialog\")[0];\n const buttons = findByTagName(\"esp-button\");\n buttons[0].addEventListener(\"esp-clicked\", () => {\n theDialog.toggleOpen();\n });\n buttons[1].addEventListener(\"esp-clicked\", () => {\n theDialog.toggleOpen();\n });\n</script>\n```",
7665
7720
  "attribute": "full-screen"
7666
7721
  },
7667
7722
  {
@@ -8396,9 +8451,20 @@
8396
8451
  "text": "boolean"
8397
8452
  },
8398
8453
  "default": "false",
8399
- "description": "Whether the box should take up the full screen. Useful when\ndisplaying a dialog in full-screen mode.\n\n```html\n<esp-box>\n <h2>Box things</h2>\n <esp-box>\n <h2>Box in box</h2>\n <esp-button label=\"Toggle full screen\"></esp-button>\n </esp-box>\n</esp-box>\n<script>\n let fullScreen = false;\n const theBox = findByTagName(\"esp-box\")[0];\n const theButton = findByTagName(\"esp-button\")[0];\n theButton.addEventListener(\"esp-clicked\", () => {\n fullScreen = !fullScreen;\n\n if(fullScreen) {\n theBox.setAttribute(\"full-screen\", true);\n } else {\n theBox.removeAttribute(\"full-screen\");\n }\n });\n</script>\n```",
8454
+ "description": "Whether the box should take up the full screen. Useful when\ndisplaying a dialog in full-screen mode.\n\n```html\n<esp-box>\n <h2>Box things</h2>\n <esp-box>\n <h2>Box in box</h2>\n <esp-button label=\"Toggle full screen\"></esp-button>\n </esp-box>\n</esp-box>\n<script>\n let fullScreen = false;\n const theBox = findByTagName(\"esp-box\")[0];\n const theButton = findByTagName(\"esp-button\")[0];\n theButton.addEventListener(\"esp-clicked\", () => {\n fullScreen = !fullScreen;\n\n if(fullScreen) {\n theBox.setAttribute(\"full-screen\", true);\n } else {\n theBox.removeAttribute(\"full-screen\");\n }\n });\n</script>\n```\n\nA full-screen box fills the viewport. Inside a\n[bounded dialog](/components/dialog) it fills exactly the dialog's\nrectangle instead: it takes its position and size from the same\n`--esp-dialog-inset-*` custom properties, set on the `esp-dialog` or any\nancestor of the box.\n\n```html\n<style>\n esp-dialog.bounded-box {\n --esp-dialog-inset-block-start: 6rem;\n --esp-dialog-inset-inline-start: 3rem;\n --esp-dialog-inset-inline-end: 3rem;\n --esp-dialog-inset-block-end: 2rem;\n }\n</style>\n<esp-dialog class=\"bounded-box\" full-screen>\n <esp-box full-screen>\n <h2>Box in a bounded dialog</h2>\n <p>This box fills the rectangle the dialog is bounded to.</p>\n <esp-button intent=\"danger\" label=\"Close\"></esp-button>\n </esp-box>\n</esp-dialog>\n<esp-button label=\"Show Dialog\"></esp-button>\n<script>\n const theDialog = findByTagName(\"esp-dialog\")[0];\n const buttons = findByTagName(\"esp-button\");\n buttons[0].addEventListener(\"esp-clicked\", () => {\n theDialog.toggleOpen();\n });\n buttons[1].addEventListener(\"esp-clicked\", () => {\n theDialog.toggleOpen();\n });\n</script>\n```",
8400
8455
  "fieldName": "fullScreen"
8401
8456
  },
8457
+ {
8458
+ "type": {
8459
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
8460
+ },
8461
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
8462
+ "name": "surface-layer",
8463
+ "inheritedFrom": {
8464
+ "name": "EspalierElementBase",
8465
+ "module": "dist/shared/esp-element-base.js"
8466
+ }
8467
+ },
8402
8468
  {
8403
8469
  "name": "scheme",
8404
8470
  "type": {
@@ -8662,7 +8728,7 @@
8662
8728
  "name": "--esp-breadcrumb-separator-color"
8663
8729
  },
8664
8730
  {
8665
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
8731
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
8666
8732
  "name": "--esp-field-background",
8667
8733
  "inheritedFrom": {
8668
8734
  "name": "EspalierElementBase",
@@ -8739,7 +8805,7 @@
8739
8805
  "text": "void"
8740
8806
  }
8741
8807
  },
8742
- "description": "Point every crumb's arrow at the next one and mark the end of the\ntrail.\n\n`isLastElement` is set for every crumb, including one that supplies\nits own separator — that flag drives the current-crumb styling, not\njust the arrow, so skipping it left a custom-separator crumb unable\nto end a trail. Only the arrow itself defers to a custom separator.\n\nA crumb already carrying one of the two arrows below is one this\nelement wrote, so re-pointing it is safe; anything else is the\nconsumer's and is left alone, whenever they set it.\n\nThe emptiness test is a truthiness check rather than a length: Lit\nsets a `type: String` property to `null` when its attribute is\nremoved, and reading `.length` off that throws — reachable as soon\nas this runs again on a slot change."
8808
+ "description": "Keep the end-of-trail state current without replacing custom separators.\n\nEvery crumb needs `isLastElement`, even when it supplies a custom separator.\nOnly separators this component wrote may be repointed. Check emptiness by\ntruthiness: Lit sets a removed String attribute to `null`."
8743
8809
  },
8744
8810
  {
8745
8811
  "kind": "field",
@@ -9493,6 +9559,17 @@
9493
9559
  "description": ""
9494
9560
  },
9495
9561
  "attributes": [
9562
+ {
9563
+ "type": {
9564
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
9565
+ },
9566
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
9567
+ "name": "surface-layer",
9568
+ "inheritedFrom": {
9569
+ "name": "EspalierElementBase",
9570
+ "module": "dist/shared/esp-element-base.js"
9571
+ }
9572
+ },
9496
9573
  {
9497
9574
  "name": "scheme",
9498
9575
  "type": {
@@ -10010,7 +10087,7 @@
10010
10087
  "declarations": [
10011
10088
  {
10012
10089
  "kind": "class",
10013
- "description": "Used to initiate a user action or link to another page.\n\n```html\n<esp-button label=\"Hello there!\"></esp-button>\n<script>\n const theButton = findByTagName(\"esp-button\")[0];\n theButton.addEventListener(\"esp-clicked\", () => {\n showToast({\n message: `You clicked the button!`,\n icon: \"info-i\",\n duration: 2\n });\n });\n</script>\n```\n\nHere is each of the button intents:\n\n```html\n<div style=\"display: flex; flex-wrap: wrap; gap: var(--esp-size-padding);\">\n <esp-button label=\"Neutral\" intent=\"neutral\" collapsed></esp-button>\n <esp-button label=\"Success\" intent=\"success\" collapsed></esp-button>\n <esp-button label=\"Warning\" intent=\"warning\" collapsed></esp-button>\n <esp-button label=\"Danger\" intent=\"danger\" collapsed></esp-button>\n <esp-button label=\"Info\" intent=\"info\" collapsed></esp-button>\n</div>\n```\n\nUse `icon-position=\"left\"` for a leading icon. The default `\"right\"`\nposition keeps the icon trailing; both positions follow writing direction\nand therefore mirror in RTL.\n\n```html\n<div style=\"display: flex; gap: var(--esp-size-padding);\">\n <esp-button\n collapsed\n label=\"Save\"\n icon=\"save\"\n icon-position=\"left\">\n </esp-button>\n <esp-button\n collapsed\n label=\"Next\"\n icon=\"arrow-big-right\"\n icon-position=\"right\">\n </esp-button>\n</div>\n```",
10090
+ "description": "Triggers an action or links to another page.\n\n```html\n<esp-button label=\"Hello there!\"></esp-button>\n<script>\n const theButton = findByTagName(\"esp-button\")[0];\n theButton.addEventListener(\"esp-clicked\", () => {\n showToast({\n message: `You clicked the button!`,\n icon: \"info-i\",\n duration: 2\n });\n });\n</script>\n```\n\nChoose an intent to express the action's status:\n\n```html\n<div style=\"display: flex; flex-wrap: wrap; gap: var(--esp-size-padding);\">\n <esp-button label=\"Neutral\" intent=\"neutral\" collapsed></esp-button>\n <esp-button label=\"Success\" intent=\"success\" collapsed></esp-button>\n <esp-button label=\"Warning\" intent=\"warning\" collapsed></esp-button>\n <esp-button label=\"Danger\" intent=\"danger\" collapsed></esp-button>\n <esp-button label=\"Info\" intent=\"info\" collapsed></esp-button>\n</div>\n```\n\nUse `icon-position=\"left\"` for a leading icon. The default `\"right\"`\nposition keeps the icon trailing; both positions follow writing direction\nand therefore mirror in RTL.\n\n```html\n<div style=\"display: flex; gap: var(--esp-size-padding);\">\n <esp-button\n collapsed\n label=\"Save\"\n icon=\"save\"\n icon-position=\"left\">\n </esp-button>\n <esp-button\n collapsed\n label=\"Next\"\n icon=\"arrow-big-right\"\n icon-position=\"right\">\n </esp-button>\n</div>\n```",
10014
10091
  "name": "EspalierButton",
10015
10092
  "cssProperties": [
10016
10093
  {
@@ -10022,7 +10099,7 @@
10022
10099
  "name": "--esp-button-incognito-padding"
10023
10100
  },
10024
10101
  {
10025
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
10102
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
10026
10103
  "name": "--esp-field-background",
10027
10104
  "inheritedFrom": {
10028
10105
  "name": "EspalierElementBase",
@@ -10196,7 +10273,7 @@
10196
10273
  },
10197
10274
  "privacy": "public",
10198
10275
  "default": "false",
10199
- "description": "Controls whether or not the button is disabled.\n\n```html\n<esp-button disabled label=\"Disabled\"></esp-button>\n<script>\n const theButton = findByTagName(\"esp-button\")[0];\n theButton.addEventListener(\"esp-clicked\", () => {\n alert(\"We will not get here...\");\n});\n</script>\n```",
10276
+ "description": "Disables the button.\n\n```html\n<esp-button disabled label=\"Disabled\"></esp-button>\n<script>\n const theButton = findByTagName(\"esp-button\")[0];\n theButton.addEventListener(\"esp-clicked\", () => {\n alert(\"We will not get here...\");\n});\n</script>\n```",
10200
10277
  "attribute": "disabled",
10201
10278
  "reflects": true
10202
10279
  },
@@ -10275,7 +10352,7 @@
10275
10352
  },
10276
10353
  "privacy": "public",
10277
10354
  "default": "false",
10278
- "description": "Controls whether or not the button is in a loading state.\nThe button will be disabled and show a spinner while loading\nis true.\n\n```html\n<esp-button label=\"Click me...\" icon=\"save\"></esp-button>\n<script>\n const theButton = findByTagName(\"esp-button\")[0];\n theButton.addEventListener(\"esp-clicked\", async () => {\n theButton.loading = true;\n setTimeout(function() {\n theButton.loading = false;\n }, 5000);\n });\n</script>\n```",
10355
+ "description": "Shows the button's loading state.\nThe button will be disabled and show a spinner while loading\nis true.\n\n```html\n<esp-button label=\"Click me...\" icon=\"save\"></esp-button>\n<script>\n const theButton = findByTagName(\"esp-button\")[0];\n theButton.addEventListener(\"esp-clicked\", async () => {\n theButton.loading = true;\n setTimeout(function() {\n theButton.loading = false;\n }, 5000);\n });\n</script>\n```",
10279
10356
  "attribute": "loading",
10280
10357
  "reflects": true
10281
10358
  },
@@ -11105,7 +11182,7 @@
11105
11182
  "text": "boolean"
11106
11183
  },
11107
11184
  "default": "false",
11108
- "description": "Controls whether or not the button is disabled.\n\n```html\n<esp-button disabled label=\"Disabled\"></esp-button>\n<script>\n const theButton = findByTagName(\"esp-button\")[0];\n theButton.addEventListener(\"esp-clicked\", () => {\n alert(\"We will not get here...\");\n});\n</script>\n```",
11185
+ "description": "Disables the button.\n\n```html\n<esp-button disabled label=\"Disabled\"></esp-button>\n<script>\n const theButton = findByTagName(\"esp-button\")[0];\n theButton.addEventListener(\"esp-clicked\", () => {\n alert(\"We will not get here...\");\n});\n</script>\n```",
11109
11186
  "fieldName": "disabled"
11110
11187
  },
11111
11188
  {
@@ -11159,7 +11236,7 @@
11159
11236
  "text": "boolean"
11160
11237
  },
11161
11238
  "default": "false",
11162
- "description": "Controls whether or not the button is in a loading state.\nThe button will be disabled and show a spinner while loading\nis true.\n\n```html\n<esp-button label=\"Click me...\" icon=\"save\"></esp-button>\n<script>\n const theButton = findByTagName(\"esp-button\")[0];\n theButton.addEventListener(\"esp-clicked\", async () => {\n theButton.loading = true;\n setTimeout(function() {\n theButton.loading = false;\n }, 5000);\n });\n</script>\n```",
11239
+ "description": "Shows the button's loading state.\nThe button will be disabled and show a spinner while loading\nis true.\n\n```html\n<esp-button label=\"Click me...\" icon=\"save\"></esp-button>\n<script>\n const theButton = findByTagName(\"esp-button\")[0];\n theButton.addEventListener(\"esp-clicked\", async () => {\n theButton.loading = true;\n setTimeout(function() {\n theButton.loading = false;\n }, 5000);\n });\n</script>\n```",
11163
11240
  "fieldName": "loading"
11164
11241
  },
11165
11242
  {
@@ -11189,6 +11266,17 @@
11189
11266
  "description": "When true and `button-type` is `\"submit\"`, the enclosing\nform is submitted without running constraint validation.\n\n```html\n<esp-button button-type=\"submit\" formnovalidate label=\"Save Draft\"></esp-button>\n```",
11190
11267
  "fieldName": "formNoValidate"
11191
11268
  },
11269
+ {
11270
+ "type": {
11271
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
11272
+ },
11273
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
11274
+ "name": "surface-layer",
11275
+ "inheritedFrom": {
11276
+ "name": "EspalierElementBase",
11277
+ "module": "dist/shared/esp-element-base.js"
11278
+ }
11279
+ },
11192
11280
  {
11193
11281
  "name": "scheme",
11194
11282
  "type": {
@@ -12292,6 +12380,17 @@
12292
12380
  "description": "Disables all child checkboxes in the group.",
12293
12381
  "fieldName": "disabled"
12294
12382
  },
12383
+ {
12384
+ "type": {
12385
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
12386
+ },
12387
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
12388
+ "name": "surface-layer",
12389
+ "inheritedFrom": {
12390
+ "name": "EspalierElementBase",
12391
+ "module": "dist/shared/esp-element-base.js"
12392
+ }
12393
+ },
12295
12394
  {
12296
12395
  "name": "scheme",
12297
12396
  "type": {
@@ -12350,7 +12449,7 @@
12350
12449
  "customElement": true,
12351
12450
  "cssProperties": [
12352
12451
  {
12353
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
12452
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
12354
12453
  "name": "--esp-field-background",
12355
12454
  "inheritedFrom": {
12356
12455
  "name": "EspalierElementBase",
@@ -12449,7 +12548,7 @@
12449
12548
  "name": "--esp-checkbox-focus-shadow"
12450
12549
  },
12451
12550
  {
12452
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
12551
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
12453
12552
  "name": "--esp-field-background",
12454
12553
  "inheritedFrom": {
12455
12554
  "name": "EspalierElementBase",
@@ -13551,6 +13650,17 @@
13551
13650
  "description": "Displays the checkbox in an indeterminate (partial) state.\nClicking the checkbox clears the indeterminate state.",
13552
13651
  "fieldName": "indeterminate"
13553
13652
  },
13653
+ {
13654
+ "type": {
13655
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
13656
+ },
13657
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
13658
+ "name": "surface-layer",
13659
+ "inheritedFrom": {
13660
+ "name": "EspalierElementBase",
13661
+ "module": "dist/shared/esp-element-base.js"
13662
+ }
13663
+ },
13554
13664
  {
13555
13665
  "name": "scheme",
13556
13666
  "type": {
@@ -14651,6 +14761,17 @@
14651
14761
  "description": "Truncates both text lines to a single ellipsized line.",
14652
14762
  "fieldName": "truncate"
14653
14763
  },
14764
+ {
14765
+ "type": {
14766
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
14767
+ },
14768
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
14769
+ "name": "surface-layer",
14770
+ "inheritedFrom": {
14771
+ "name": "EspalierElementBase",
14772
+ "module": "dist/shared/esp-element-base.js"
14773
+ }
14774
+ },
14654
14775
  {
14655
14776
  "name": "scheme",
14656
14777
  "type": {
@@ -14717,7 +14838,7 @@
14717
14838
  "customElement": true,
14718
14839
  "cssProperties": [
14719
14840
  {
14720
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
14841
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
14721
14842
  "name": "--esp-field-background",
14722
14843
  "inheritedFrom": {
14723
14844
  "name": "EspalierElementBase",
@@ -17055,6 +17176,17 @@
17055
17176
  "description": "A custom message to display when the picker is required but no\ndate is selected. Defaults to `\"Please select a date.\"` when\nnot set.\n\n```html\n<esp-date-picker required required-message=\"A departure date is required.\"></esp-date-picker>\n```",
17056
17177
  "fieldName": "requiredMessage"
17057
17178
  },
17179
+ {
17180
+ "type": {
17181
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
17182
+ },
17183
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
17184
+ "name": "surface-layer",
17185
+ "inheritedFrom": {
17186
+ "name": "EspalierElementBase",
17187
+ "module": "dist/shared/esp-element-base.js"
17188
+ }
17189
+ },
17058
17190
  {
17059
17191
  "name": "scheme",
17060
17192
  "type": {
@@ -17121,7 +17253,7 @@
17121
17253
  },
17122
17254
  "cssProperties": [
17123
17255
  {
17124
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
17256
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
17125
17257
  "name": "--esp-field-background",
17126
17258
  "inheritedFrom": {
17127
17259
  "name": "EspalierElementBase",
@@ -17250,7 +17382,7 @@
17250
17382
  "name": "--esp-details-group-color-border"
17251
17383
  },
17252
17384
  {
17253
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
17385
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
17254
17386
  "name": "--esp-field-background",
17255
17387
  "inheritedFrom": {
17256
17388
  "name": "EspalierElementBase",
@@ -18121,6 +18253,17 @@
18121
18253
  "description": "When true (the default), only one item can be open at a\ntime. Set to false to allow multiple items open\nsimultaneously while keeping the grouped visual treatment.",
18122
18254
  "fieldName": "exclusive"
18123
18255
  },
18256
+ {
18257
+ "type": {
18258
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
18259
+ },
18260
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
18261
+ "name": "surface-layer",
18262
+ "inheritedFrom": {
18263
+ "name": "EspalierElementBase",
18264
+ "module": "dist/shared/esp-element-base.js"
18265
+ }
18266
+ },
18124
18267
  {
18125
18268
  "name": "scheme",
18126
18269
  "type": {
@@ -18208,7 +18351,7 @@
18208
18351
  "declarations": [
18209
18352
  {
18210
18353
  "kind": "class",
18211
- "description": "A disclosure widget that wraps the native `<details>` and\n`<summary>` elements. Provides themed styling with a custom\nopen/close indicator and smooth transitions. Use\n[esp-details-group](/components/details/group) to create an\naccordion where only one item is open at a time.\n\n```html\n<esp-details summary=\"What is Espalier?\">\n <p>Espalier is a design system and component library\n built on Web Components and Lit.</p>\n</esp-details>\n```\n\nDetails pick up zone theming from a `context` attribute on the group\nor any ancestor — see the [theming guide](/espalier-guides/):\n\n```html\n<esp-details-group context=\"quiet\">\n <esp-details summary=\"Inside the quiet zone\">\n <p>Zone-correct tokens, contrast enforced by the theme engine.</p>\n </esp-details>\n</esp-details-group>\n```\n\n### Pre-opened details\n\nSet the `open` attribute to render the details expanded by\ndefault:\n\n```html\n<esp-details summary=\"I start open\" open>\n <p>This content is visible from the start.</p>\n</esp-details>\n```\n\n### Disabled details\n\n```html\n<esp-details summary=\"Cannot toggle me\" disabled>\n <p>This content cannot be revealed.</p>\n</esp-details>\n```",
18354
+ "description": "A disclosure widget that wraps the native `<details>` and\n`<summary>` elements. Provides themed styling with a custom\nopen/close indicator and smooth transitions. Use\n[esp-details-group](/components/details/group) to create an\naccordion where only one item is open at a time.\n\nA field fills one layer above the surface it sits on.\nUse `surface-layer=\"1|2|3|4\"` to select the default painted layer.\n\n```html\n<esp-details summary=\"What is Espalier?\">\n <p>Espalier is a design system and component library\n built on Web Components and Lit.</p>\n</esp-details>\n```\n\nDetails pick up zone theming from a `context` attribute on the group\nor any ancestor — see the [theming guide](/espalier-guides/):\n\n```html\n<esp-details-group context=\"quiet\">\n <esp-details summary=\"Inside the quiet zone\">\n <p>Zone-correct tokens, contrast enforced by the theme engine.</p>\n </esp-details>\n</esp-details-group>\n```\n\n### Pre-opened details\n\nSet the `open` attribute to render the details expanded by\ndefault:\n\n```html\n<esp-details summary=\"I start open\" open>\n <p>This content is visible from the start.</p>\n</esp-details>\n```\n\n### Disabled details\n\n```html\n<esp-details summary=\"Cannot toggle me\" disabled>\n <p>This content cannot be revealed.</p>\n</esp-details>\n```",
18212
18355
  "name": "EspalierDetails",
18213
18356
  "cssProperties": [
18214
18357
  {
@@ -18240,7 +18383,7 @@
18240
18383
  "name": "--esp-details-size-padding"
18241
18384
  },
18242
18385
  {
18243
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
18386
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
18244
18387
  "name": "--esp-field-background",
18245
18388
  "inheritedFrom": {
18246
18389
  "name": "EspalierElementBase",
@@ -19168,6 +19311,17 @@
19168
19311
  "description": "Whether the details element is disabled (cannot be toggled).\n\n```html\n<esp-details summary=\"Locked\" disabled>\n <p>Cannot be toggled.</p>\n</esp-details>\n```",
19169
19312
  "fieldName": "disabled"
19170
19313
  },
19314
+ {
19315
+ "type": {
19316
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
19317
+ },
19318
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
19319
+ "name": "surface-layer",
19320
+ "inheritedFrom": {
19321
+ "name": "EspalierElementBase",
19322
+ "module": "dist/shared/esp-element-base.js"
19323
+ }
19324
+ },
19171
19325
  {
19172
19326
  "name": "scheme",
19173
19327
  "type": {
@@ -19263,7 +19417,7 @@
19263
19417
  "declarations": [
19264
19418
  {
19265
19419
  "kind": "class",
19266
- "description": "Used to focus content modally. By default, on large screens the\ndialog content is centered horizontally near the top of the\nscreen. On small screens, the content always tries to take up\nthe full screen width.\n\nWhen opened, the dialog:\n- Moves focus to the first `[autofocus]` element inside the slotted\n content, then the first focusable element (or the dialog container\n itself if none is found).\n- Traps focus so that `Tab` and `Shift+Tab` cycle only through\n elements inside the dialog.\n- Closes when the user presses `Escape`.\n- Marks all sibling content as `inert` to prevent screen reader\n and keyboard access to the page behind the overlay.\n- Restores focus to the element that was focused before the\n dialog was opened.",
19420
+ "description": "Shows content in a modal dialog. By default, on large screens the\ndialog content is centered horizontally near the top of the\nscreen. On small screens, the content always tries to take up\nthe full screen width.\n\nA field fills one layer above the surface it sits on.\nUse `surface-layer=\"1|2|3|4\"` to select the default painted layer.\n\nWhen opened, the dialog:\n- Moves focus to the first `[autofocus]` element inside the slotted\n content, then the first focusable element (or the dialog container\n itself if none is found).\n- Traps focus so that `Tab` and `Shift+Tab` cycle only through\n elements inside the dialog.\n- Closes when the user presses `Escape`.\n- Marks all sibling content as `inert` to prevent screen reader\n and keyboard access to the page behind the overlay.\n- Restores focus to the element that was focused before the\n dialog was opened.\n\n### Bounded dialog\n\nA dialog covers the whole viewport by default. Set the four inset custom\nproperties on the `esp-dialog` host to confine it to a rectangle and leave\nthe rest of the page visible, for example to keep a header or a live preview\non screen while a full-screen editor is open. The vellum (the dialog's\nscrolling surface) and its backdrop paint only inside the rectangle, and a\nslotted `esp-box full-screen` fills exactly that rectangle.\n\nThe insets are lengths (`0px`, `4rem`, `var(--header-height)`), not unitless\nnumbers, and they are measured from the viewport edges. They are logical, so\n\"inline start\" is the left edge in left-to-right text and the right edge in\nright-to-left text. Because custom properties inherit, a dialog nested inside\na bounded dialog's markup is bounded the same way unless it sets its own\ninsets.\n\nBounds change only where the dialog paints. What stays reachable is decided\nby the same rules as an unbounded dialog:\n\n- The dialog has no click-outside dismissal, bounded or not. It closes on\n `Escape`, an `esp-form-dialog-close-requested` event, or `toggleOpen()`, so\n a click on the uncovered region never counts as an \"outside\" click.\n- The dialog marks `inert` the siblings of its anchor element, not the\n regions its bounds leave uncovered. The anchor is `esp-page`'s dialog drop\n zone when the dialog sits in the page's markup, so there the page's\n internal tracks (header, menus, preview, help, footer) are inert and not\n clickable even where the dialog does not cover them. A dialog that is a\n direct child of another component's shadow root is anchored on the nearest\n ancestor host that has a light-DOM parent, and only that host's siblings\n go inert; the page around it stays live. A dialog wrapped in an element\n inside that shadow root is anchored on itself, so its siblings in the\n wrapper go inert instead.\n- A region that is not inert receives clicks and keyboard focus. The focus\n trap wraps `Tab` and `Shift+Tab` at the dialog's first and last focusable\n elements, and `Escape` is handled on the dialog, both only while focus is\n inside it. A click on an uncovered control moves focus there, and from then\n on `Tab` continues through the page and `Escape` no longer closes the\n dialog. Set `inert` on the uncovered regions that must not be reachable\n while the dialog is open (an inert region is also skipped by screen\n readers).\n- Bounds assume a horizontal writing mode: the insets are logical, but the\n dialog's width and height are physical.\n\n```html\n<style>\n esp-dialog.bounded {\n --esp-dialog-inset-block-start: 6rem;\n --esp-dialog-inset-inline-start: 3rem;\n --esp-dialog-inset-inline-end: 3rem;\n --esp-dialog-inset-block-end: 2rem;\n }\n</style>\n<esp-dialog class=\"bounded\" full-screen>\n <esp-box full-screen>\n <h2>Bounded dialog</h2>\n <p>The page above and around this box stays visible.</p>\n <esp-button intent=\"danger\" label=\"Close\"></esp-button>\n </esp-box>\n</esp-dialog>\n<esp-button label=\"Show Dialog\"></esp-button>\n<script>\n const theDialog = findByTagName(\"esp-dialog\")[0];\n const buttons = findByTagName(\"esp-button\");\n buttons[0].addEventListener(\"esp-clicked\", () => {\n theDialog.toggleOpen();\n });\n buttons[1].addEventListener(\"esp-clicked\", () => {\n theDialog.toggleOpen();\n });\n</script>\n```",
19267
19421
  "name": "EspalierDialog",
19268
19422
  "cssProperties": [
19269
19423
  {
@@ -19273,6 +19427,22 @@
19273
19427
  {
19274
19428
  "description": "The opacity of the dialog overlay background.",
19275
19429
  "name": "--esp-dialog-bg-opacity"
19430
+ },
19431
+ {
19432
+ "description": "The top edge of the area the dialog covers, as a length from the viewport top. Defaults to `0px`.",
19433
+ "name": "--esp-dialog-inset-block-start"
19434
+ },
19435
+ {
19436
+ "description": "The leading edge (left in left-to-right text) of the area the dialog covers, as a length from the viewport edge. Defaults to `0px`.",
19437
+ "name": "--esp-dialog-inset-inline-start"
19438
+ },
19439
+ {
19440
+ "description": "The trailing edge (right in left-to-right text) of the area the dialog covers, as a length from the viewport edge. Defaults to `0px`.",
19441
+ "name": "--esp-dialog-inset-inline-end"
19442
+ },
19443
+ {
19444
+ "description": "The bottom edge of the area the dialog covers, as a length from the viewport bottom. Defaults to `0px`.",
19445
+ "name": "--esp-dialog-inset-block-end"
19276
19446
  }
19277
19447
  ],
19278
19448
  "slots": [
@@ -19398,6 +19568,13 @@
19398
19568
  }
19399
19569
  ],
19400
19570
  "attributes": [
19571
+ {
19572
+ "type": {
19573
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
19574
+ },
19575
+ "description": "Declares the painted surface depth; defaults to layer 3.",
19576
+ "name": "surface-layer"
19577
+ },
19401
19578
  {
19402
19579
  "name": "full-screen",
19403
19580
  "type": {
@@ -19461,71 +19638,87 @@
19461
19638
  },
19462
19639
  {
19463
19640
  "kind": "javascript-module",
19464
- "path": "dist/file-upload/esp-file-upload.js",
19641
+ "path": "dist/empty-state/esp-empty-state.js",
19465
19642
  "declarations": [
19466
19643
  {
19467
19644
  "kind": "class",
19468
- "description": "Used to upload files.\n\n```html\n<esp-file-upload choose-text=\"Upload Document\"></esp-file-upload>\n<script>\n const theFileUpload = findByTagName(\"esp-file-upload\")[0];\n theFileUpload.addEventListener(\"esp-file-upload-files-selected\", () => {\n showToast({\n message: `You selected a file!`,\n icon: \"info-i\",\n duration: 2,\n onClosed: () => {\n theFileUpload.finishedProcessingFiles();\n },\n });\n });\n</script>\n```",
19469
- "name": "EspalierFileUpload",
19645
+ "description": "A centered empty-state layout for list, grid, and page-level\nzero-item states.\n\n```html\n<esp-empty-state icon=\"photo-up\">\n <span slot=\"heading\">No images yet</span>\n Upload the first image to start building this library.\n <esp-button slot=\"action\" label=\"Upload Image\"></esp-button>\n</esp-empty-state>\n\n<esp-empty-state size=\"large\" icon=\"users\">\n <span slot=\"heading\">No members yet</span>\n Invite teammates when you are ready to collaborate.\n</esp-empty-state>\n```",
19646
+ "name": "EspalierEmptyState",
19647
+ "slots": [
19648
+ {
19649
+ "description": "Primary empty-state message.",
19650
+ "name": "heading"
19651
+ },
19652
+ {
19653
+ "description": "Supporting body text.",
19654
+ "name": ""
19655
+ },
19656
+ {
19657
+ "description": "Optional custom icon. When provided, it overrides the generated SVG from the `icon` attribute.",
19658
+ "name": "icon"
19659
+ },
19660
+ {
19661
+ "description": "Optional call-to-action button or link.",
19662
+ "name": "action"
19663
+ }
19664
+ ],
19470
19665
  "members": [
19471
19666
  {
19472
19667
  "kind": "field",
19473
- "name": "uploadInput",
19668
+ "name": "iconSlot",
19669
+ "privacy": "private",
19670
+ "default": "new SlottedIconController(this)"
19671
+ },
19672
+ {
19673
+ "kind": "field",
19674
+ "name": "actionSlot",
19474
19675
  "privacy": "private"
19475
19676
  },
19476
19677
  {
19477
19678
  "kind": "field",
19478
- "name": "processingFiles",
19679
+ "name": "sizeBacker",
19479
19680
  "type": {
19480
- "text": "boolean"
19681
+ "text": "EspalierEmptyStateSize"
19481
19682
  },
19482
19683
  "privacy": "private",
19483
- "default": "false"
19684
+ "default": "\"medium\""
19484
19685
  },
19485
19686
  {
19486
19687
  "kind": "field",
19487
- "name": "accept",
19688
+ "name": "hasSlottedAction",
19488
19689
  "type": {
19489
- "text": "string"
19690
+ "text": "boolean"
19490
19691
  },
19491
- "privacy": "public",
19492
- "default": "\"*\"",
19493
- "description": "The accepted file types. For example, to allow JPEG, PNG, and WebP images:\n\n`image/jpeg, image/png, image/webp`",
19494
- "attribute": "accept"
19692
+ "privacy": "private",
19693
+ "default": "false"
19495
19694
  },
19496
19695
  {
19497
19696
  "kind": "field",
19498
- "name": "chooseText",
19697
+ "name": "icon",
19499
19698
  "type": {
19500
19699
  "text": "string"
19501
19700
  },
19502
19701
  "privacy": "public",
19503
- "default": "\"Choose File\"",
19504
- "description": "The text to display on the button.",
19505
- "attribute": "choose-text"
19702
+ "default": "\"\"",
19703
+ "description": "Optional icon name from the Espalier SVG sprite.",
19704
+ "attribute": "icon"
19506
19705
  },
19507
19706
  {
19508
19707
  "kind": "field",
19509
- "name": "multiple",
19708
+ "name": "size",
19510
19709
  "type": {
19511
- "text": "boolean"
19710
+ "text": "EspalierEmptyStateSize"
19512
19711
  },
19513
19712
  "privacy": "public",
19514
- "default": "false",
19515
- "description": "Allow multiple files to be selected.",
19516
- "attribute": "multiple"
19713
+ "description": "Empty-state scale.",
19714
+ "attribute": "size",
19715
+ "reflects": true
19517
19716
  },
19518
19717
  {
19519
19718
  "kind": "field",
19520
- "name": "filesSelected",
19719
+ "name": "handleActionSlotChange",
19521
19720
  "privacy": "private"
19522
19721
  },
19523
- {
19524
- "kind": "method",
19525
- "name": "finishedProcessingFiles",
19526
- "privacy": "public",
19527
- "description": "The `esp-file-upload-files-selected` event sets the file upload to a processing state.\nCall this method when you have finished processing the selected files\nto reset the file upload."
19528
- },
19529
19722
  {
19530
19723
  "kind": "field",
19531
19724
  "name": "seedColorBacker",
@@ -20251,42 +20444,34 @@
20251
20444
  }
20252
20445
  }
20253
20446
  ],
20254
- "events": [
20255
- {
20256
- "type": {
20257
- "text": "CustomEvent<FileList>"
20258
- },
20259
- "description": "Fired after the user selects one or more files.",
20260
- "name": "esp-file-upload-files-selected"
20261
- }
20262
- ],
20263
20447
  "attributes": [
20264
20448
  {
20265
- "name": "accept",
20449
+ "name": "icon",
20266
20450
  "type": {
20267
20451
  "text": "string"
20268
20452
  },
20269
- "default": "\"*\"",
20270
- "description": "The accepted file types. For example, to allow JPEG, PNG, and WebP images:\n\n`image/jpeg, image/png, image/webp`",
20271
- "fieldName": "accept"
20453
+ "default": "\"\"",
20454
+ "description": "Optional icon name from the Espalier SVG sprite.",
20455
+ "fieldName": "icon"
20272
20456
  },
20273
20457
  {
20274
- "name": "choose-text",
20458
+ "name": "size",
20275
20459
  "type": {
20276
- "text": "string"
20460
+ "text": "EspalierEmptyStateSize"
20277
20461
  },
20278
- "default": "\"Choose File\"",
20279
- "description": "The text to display on the button.",
20280
- "fieldName": "chooseText"
20462
+ "description": "Empty-state scale.",
20463
+ "fieldName": "size"
20281
20464
  },
20282
20465
  {
20283
- "name": "multiple",
20284
20466
  "type": {
20285
- "text": "boolean"
20467
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
20286
20468
  },
20287
- "default": "false",
20288
- "description": "Allow multiple files to be selected.",
20289
- "fieldName": "multiple"
20469
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
20470
+ "name": "surface-layer",
20471
+ "inheritedFrom": {
20472
+ "name": "EspalierElementBase",
20473
+ "module": "dist/shared/esp-element-base.js"
20474
+ }
20290
20475
  },
20291
20476
  {
20292
20477
  "name": "scheme",
@@ -20330,31 +20515,31 @@
20330
20515
  "name": "EspalierElementBase",
20331
20516
  "module": "dist/shared/esp-element-base.js"
20332
20517
  },
20333
- "tagName": "esp-file-upload",
20334
- "customElement": true,
20335
20518
  "docPageTitle": {
20336
- "name": "File",
20337
- "description": "Upload"
20519
+ "name": "Empty",
20520
+ "description": "State"
20338
20521
  },
20339
20522
  "docUrl": {
20340
- "name": "/components/file-upload",
20523
+ "name": "/components/empty-state",
20341
20524
  "description": ""
20342
20525
  },
20343
20526
  "menuGroup": {
20344
- "name": "Form",
20345
- "description": "Controls"
20527
+ "name": "Feedback",
20528
+ "description": ""
20346
20529
  },
20347
20530
  "menuLabel": {
20348
- "name": "File",
20349
- "description": "Upload"
20531
+ "name": "Empty",
20532
+ "description": "State"
20350
20533
  },
20351
20534
  "menuIcon": {
20352
- "name": "upload",
20535
+ "name": "box",
20353
20536
  "description": ""
20354
20537
  },
20538
+ "tagName": "esp-empty-state",
20539
+ "customElement": true,
20355
20540
  "cssProperties": [
20356
20541
  {
20357
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
20542
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
20358
20543
  "name": "--esp-field-background",
20359
20544
  "inheritedFrom": {
20360
20545
  "name": "EspalierElementBase",
@@ -20409,111 +20594,96 @@
20409
20594
  "module": "dist/shared/esp-element-base.js"
20410
20595
  }
20411
20596
  }
20412
- ]
20597
+ ],
20598
+ "events": []
20413
20599
  }
20414
20600
  ],
20415
20601
  "exports": [
20416
20602
  {
20417
20603
  "kind": "js",
20418
- "name": "EspalierFileUpload",
20604
+ "name": "EspalierEmptyState",
20419
20605
  "declaration": {
20420
- "name": "EspalierFileUpload",
20421
- "module": "dist/file-upload/esp-file-upload.js"
20606
+ "name": "EspalierEmptyState",
20607
+ "module": "dist/empty-state/esp-empty-state.js"
20422
20608
  }
20423
20609
  },
20424
20610
  {
20425
20611
  "kind": "custom-element-definition",
20426
- "name": "esp-file-upload",
20612
+ "name": "esp-empty-state",
20427
20613
  "declaration": {
20428
- "name": "EspalierFileUpload",
20429
- "module": "dist/file-upload/esp-file-upload.js"
20614
+ "name": "EspalierEmptyState",
20615
+ "module": "dist/empty-state/esp-empty-state.js"
20430
20616
  }
20431
20617
  }
20432
20618
  ]
20433
20619
  },
20434
20620
  {
20435
20621
  "kind": "javascript-module",
20436
- "path": "dist/empty-state/esp-empty-state.js",
20622
+ "path": "dist/file-upload/esp-file-upload.js",
20437
20623
  "declarations": [
20438
20624
  {
20439
20625
  "kind": "class",
20440
- "description": "A centered empty-state layout for list, grid, and page-level\nzero-item states.\n\n```html\n<esp-empty-state icon=\"photo-up\">\n <span slot=\"heading\">No images yet</span>\n Upload the first image to start building this library.\n <esp-button slot=\"action\" label=\"Upload Image\"></esp-button>\n</esp-empty-state>\n\n<esp-empty-state size=\"large\" icon=\"users\">\n <span slot=\"heading\">No members yet</span>\n Invite teammates when you are ready to collaborate.\n</esp-empty-state>\n```",
20441
- "name": "EspalierEmptyState",
20442
- "slots": [
20443
- {
20444
- "description": "Primary empty-state message.",
20445
- "name": "heading"
20446
- },
20447
- {
20448
- "description": "Supporting body text.",
20449
- "name": ""
20450
- },
20451
- {
20452
- "description": "Optional custom icon. When provided, it overrides the generated SVG from the `icon` attribute.",
20453
- "name": "icon"
20454
- },
20455
- {
20456
- "description": "Optional call-to-action button or link.",
20457
- "name": "action"
20458
- }
20459
- ],
20626
+ "description": "Used to upload files.\n\n```html\n<esp-file-upload choose-text=\"Upload Document\"></esp-file-upload>\n<script>\n const theFileUpload = findByTagName(\"esp-file-upload\")[0];\n theFileUpload.addEventListener(\"esp-file-upload-files-selected\", () => {\n showToast({\n message: `You selected a file!`,\n icon: \"info-i\",\n duration: 2,\n onClosed: () => {\n theFileUpload.finishedProcessingFiles();\n },\n });\n });\n</script>\n```",
20627
+ "name": "EspalierFileUpload",
20460
20628
  "members": [
20461
20629
  {
20462
20630
  "kind": "field",
20463
- "name": "iconSlot",
20464
- "privacy": "private",
20465
- "default": "new SlottedIconController(this)"
20466
- },
20467
- {
20468
- "kind": "field",
20469
- "name": "actionSlot",
20631
+ "name": "uploadInput",
20470
20632
  "privacy": "private"
20471
20633
  },
20472
20634
  {
20473
20635
  "kind": "field",
20474
- "name": "sizeBacker",
20636
+ "name": "processingFiles",
20475
20637
  "type": {
20476
- "text": "EspalierEmptyStateSize"
20638
+ "text": "boolean"
20477
20639
  },
20478
20640
  "privacy": "private",
20479
- "default": "\"medium\""
20641
+ "default": "false"
20480
20642
  },
20481
20643
  {
20482
20644
  "kind": "field",
20483
- "name": "hasSlottedAction",
20645
+ "name": "accept",
20484
20646
  "type": {
20485
- "text": "boolean"
20647
+ "text": "string"
20486
20648
  },
20487
- "privacy": "private",
20488
- "default": "false"
20649
+ "privacy": "public",
20650
+ "default": "\"*\"",
20651
+ "description": "The accepted file types. For example, to allow JPEG, PNG, and WebP images:\n\n`image/jpeg, image/png, image/webp`",
20652
+ "attribute": "accept"
20489
20653
  },
20490
20654
  {
20491
20655
  "kind": "field",
20492
- "name": "icon",
20656
+ "name": "chooseText",
20493
20657
  "type": {
20494
20658
  "text": "string"
20495
20659
  },
20496
20660
  "privacy": "public",
20497
- "default": "\"\"",
20498
- "description": "Optional icon name from the Espalier SVG sprite.",
20499
- "attribute": "icon"
20661
+ "default": "\"Choose File\"",
20662
+ "description": "The text to display on the button.",
20663
+ "attribute": "choose-text"
20500
20664
  },
20501
20665
  {
20502
20666
  "kind": "field",
20503
- "name": "size",
20667
+ "name": "multiple",
20504
20668
  "type": {
20505
- "text": "EspalierEmptyStateSize"
20669
+ "text": "boolean"
20506
20670
  },
20507
20671
  "privacy": "public",
20508
- "description": "Empty-state scale.",
20509
- "attribute": "size",
20510
- "reflects": true
20672
+ "default": "false",
20673
+ "description": "Allow multiple files to be selected.",
20674
+ "attribute": "multiple"
20511
20675
  },
20512
20676
  {
20513
20677
  "kind": "field",
20514
- "name": "handleActionSlotChange",
20678
+ "name": "filesSelected",
20515
20679
  "privacy": "private"
20516
20680
  },
20681
+ {
20682
+ "kind": "method",
20683
+ "name": "finishedProcessingFiles",
20684
+ "privacy": "public",
20685
+ "description": "The `esp-file-upload-files-selected` event sets the file upload to a processing state.\nCall this method when you have finished processing the selected files\nto reset the file upload."
20686
+ },
20517
20687
  {
20518
20688
  "kind": "field",
20519
20689
  "name": "seedColorBacker",
@@ -21239,23 +21409,53 @@
21239
21409
  }
21240
21410
  }
21241
21411
  ],
21412
+ "events": [
21413
+ {
21414
+ "type": {
21415
+ "text": "CustomEvent<FileList>"
21416
+ },
21417
+ "description": "Fired after the user selects one or more files.",
21418
+ "name": "esp-file-upload-files-selected"
21419
+ }
21420
+ ],
21242
21421
  "attributes": [
21243
21422
  {
21244
- "name": "icon",
21423
+ "name": "accept",
21245
21424
  "type": {
21246
21425
  "text": "string"
21247
21426
  },
21248
- "default": "\"\"",
21249
- "description": "Optional icon name from the Espalier SVG sprite.",
21250
- "fieldName": "icon"
21427
+ "default": "\"*\"",
21428
+ "description": "The accepted file types. For example, to allow JPEG, PNG, and WebP images:\n\n`image/jpeg, image/png, image/webp`",
21429
+ "fieldName": "accept"
21251
21430
  },
21252
21431
  {
21253
- "name": "size",
21432
+ "name": "choose-text",
21254
21433
  "type": {
21255
- "text": "EspalierEmptyStateSize"
21434
+ "text": "string"
21256
21435
  },
21257
- "description": "Empty-state scale.",
21258
- "fieldName": "size"
21436
+ "default": "\"Choose File\"",
21437
+ "description": "The text to display on the button.",
21438
+ "fieldName": "chooseText"
21439
+ },
21440
+ {
21441
+ "name": "multiple",
21442
+ "type": {
21443
+ "text": "boolean"
21444
+ },
21445
+ "default": "false",
21446
+ "description": "Allow multiple files to be selected.",
21447
+ "fieldName": "multiple"
21448
+ },
21449
+ {
21450
+ "type": {
21451
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
21452
+ },
21453
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
21454
+ "name": "surface-layer",
21455
+ "inheritedFrom": {
21456
+ "name": "EspalierElementBase",
21457
+ "module": "dist/shared/esp-element-base.js"
21458
+ }
21259
21459
  },
21260
21460
  {
21261
21461
  "name": "scheme",
@@ -21299,31 +21499,31 @@
21299
21499
  "name": "EspalierElementBase",
21300
21500
  "module": "dist/shared/esp-element-base.js"
21301
21501
  },
21502
+ "tagName": "esp-file-upload",
21503
+ "customElement": true,
21302
21504
  "docPageTitle": {
21303
- "name": "Empty",
21304
- "description": "State"
21505
+ "name": "File",
21506
+ "description": "Upload"
21305
21507
  },
21306
21508
  "docUrl": {
21307
- "name": "/components/empty-state",
21509
+ "name": "/components/file-upload",
21308
21510
  "description": ""
21309
21511
  },
21310
21512
  "menuGroup": {
21311
- "name": "Feedback",
21312
- "description": ""
21513
+ "name": "Form",
21514
+ "description": "Controls"
21313
21515
  },
21314
21516
  "menuLabel": {
21315
- "name": "Empty",
21316
- "description": "State"
21517
+ "name": "File",
21518
+ "description": "Upload"
21317
21519
  },
21318
21520
  "menuIcon": {
21319
- "name": "box",
21521
+ "name": "upload",
21320
21522
  "description": ""
21321
21523
  },
21322
- "tagName": "esp-empty-state",
21323
- "customElement": true,
21324
21524
  "cssProperties": [
21325
21525
  {
21326
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
21526
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
21327
21527
  "name": "--esp-field-background",
21328
21528
  "inheritedFrom": {
21329
21529
  "name": "EspalierElementBase",
@@ -21378,25 +21578,24 @@
21378
21578
  "module": "dist/shared/esp-element-base.js"
21379
21579
  }
21380
21580
  }
21381
- ],
21382
- "events": []
21581
+ ]
21383
21582
  }
21384
21583
  ],
21385
21584
  "exports": [
21386
21585
  {
21387
21586
  "kind": "js",
21388
- "name": "EspalierEmptyState",
21587
+ "name": "EspalierFileUpload",
21389
21588
  "declaration": {
21390
- "name": "EspalierEmptyState",
21391
- "module": "dist/empty-state/esp-empty-state.js"
21589
+ "name": "EspalierFileUpload",
21590
+ "module": "dist/file-upload/esp-file-upload.js"
21392
21591
  }
21393
21592
  },
21394
21593
  {
21395
21594
  "kind": "custom-element-definition",
21396
- "name": "esp-empty-state",
21595
+ "name": "esp-file-upload",
21397
21596
  "declaration": {
21398
- "name": "EspalierEmptyState",
21399
- "module": "dist/empty-state/esp-empty-state.js"
21597
+ "name": "EspalierFileUpload",
21598
+ "module": "dist/file-upload/esp-file-upload.js"
21400
21599
  }
21401
21600
  }
21402
21601
  ]
@@ -21407,7 +21606,7 @@
21407
21606
  "declarations": [
21408
21607
  {
21409
21608
  "kind": "class",
21410
- "description": "A transient panel for help and more-info content that claims designed spare\nwidth before ever covering the page.\n\nPlace one `esp-flyout` in an `esp-page`'s `flyout` slot. The panel\nlives on the canvas after any persistent preview, outside the content\nsurface. It negotiates between `--esp-page-flyout-min-width` and\n`--esp-page-flyout-max-width`; preview receives flexible room first. The page\ndecides where help goes, from widest viewport to narrowest:\n\n1. **Gutter** — the panel occupies the right canvas gutter. When the\n gutter already has room, it claims existing spare canvas and the\n main content does not move at all, on any alignment.\n2. **Shift** — when the gutter is not quite wide enough, the surface\n slides toward `start` by exactly the shortfall (no more) to free\n the difference.\n3. **Docked sidebar** — once the gutter is gone, the panel competes\n for width with the main well; content narrows but is never\n covered.\n4. **Overlay drawer** — when the complete sidebar + main + help\n minimums do not fit, or below `COMPACT_VIEWPORT_MEDIA_QUERY` (the\n same breakpoint the `esp-menu` drawer uses), the panel becomes a fixed\n drawer over a vellum backdrop. Set `mode=\"overlay\"` to get this\n presentation at every width.\n\nThe panel is styled as a tear-off piece: a dotted perforation on\nits leading edge, rounded trailing corners, and no shadow — all\noverridable through the `--esp-flyout-*` tokens below.\nAn anchored panel shown with preview keeps its caret at the main/preview\nedge and receives a non-interactive dotted bridge across preview from the\npage. A short, thick terminus on the preview/help seam completes that\nconnection; no consumer positioning is required.\n\nA11y follows the presentation. In the in-grid modes (1–3) the\nflyout is a non-modal `complementary` landmark named by its\n`heading`, opening it never steals focus, and boundary scrolling chains\nback to the main document. In the overlay-drawer mode (4) it is a true\nmodal: `role=\"dialog\"` + `aria-modal`, the\nbackground goes inert, scroll is locked, focus moves into the drawer\nand is trapped, and focus is restored on close. `Escape` closes it\nin every mode.\n\n```html\n<style>\nesp-page.flyout-demo {\n --esp-page-main-max-width: 420px;\n --esp-page-canvas-background: var(--esp-color-layer-1);\n &::part(wrapper) {\n min-height: 320px;\n height: 320px;\n }\n > div {\n padding: var(--esp-size-padding);\n }\n}\n</style>\n<esp-page class=\"flyout-demo\" align=\"center\">\n <esp-flyout slot=\"flyout\" heading=\"More info\" id=\"demo-flyout\" standalone>\n <p>Spare width claimed; the well shifted only enough to make room.</p>\n </esp-flyout>\n <div>\n <esp-button id=\"toggle-flyout\" label=\"Toggle flyout\" collapsed></esp-button>\n </div>\n</esp-page>\n<script>\n const flyout = findById(\"demo-flyout\");\n findById(\"toggle-flyout\").addEventListener(\"esp-clicked\", () => flyout.toggle());\n</script>\n```\n\nBy default the panel is a tear-off — a dotted perforation on its\nleading edge, rounded trailing corners, no shadow. Add\n`match-surface` to lift the trailing edge with the page's surface\nedge shadow, so the flyout reads as a raised peer of the content it\nflies out from while staying attached at the leading perforation:\n\n```html\n<style>\nesp-page.flyout-match-demo {\n --esp-page-main-max-width: 420px;\n &::part(wrapper) {\n min-height: 320px;\n height: 320px;\n }\n > div {\n padding: var(--esp-size-padding);\n }\n}\n</style>\n<esp-page class=\"flyout-match-demo\" align=\"center\">\n <esp-flyout slot=\"flyout\" heading=\"More info\" id=\"match-flyout\" match-surface standalone>\n <p>Attached at the perforation, trailing edge raised like the surface.</p>\n </esp-flyout>\n <div>\n <esp-button id=\"toggle-match\" label=\"Toggle flyout\" collapsed></esp-button>\n </div>\n</esp-page>\n<script>\n const matchFlyout = findById(\"match-flyout\");\n findById(\"toggle-match\").addEventListener(\"esp-clicked\", () => matchFlyout.toggle());\n</script>\n```\n\nWhen the page has no spare width to claim — a `kind=\"full\"` page,\nor a capped page on a viewport at the cap — the open flyout docks\nas a right sidebar and the content makes room:\n\n```html\n<style>\nesp-page.flyout-docked-demo {\n &::part(wrapper) {\n min-height: 320px;\n height: 320px;\n }\n > div {\n padding: var(--esp-size-padding);\n }\n}\n</style>\n<esp-page class=\"flyout-docked-demo\" kind=\"full\">\n <esp-flyout slot=\"flyout\" heading=\"Docked\" id=\"docked-flyout\" standalone>\n <p>No gutter to claim, so the content narrows instead.</p>\n </esp-flyout>\n <div>\n <p>A <code>kind=\"full\"</code> page has no canvas gutters at any\n width — the flyout squeezes the main well and never covers it.</p>\n <esp-button id=\"toggle-docked\" label=\"Toggle flyout\" collapsed></esp-button>\n </div>\n</esp-page>\n<script>\n const dockedFlyout = findById(\"docked-flyout\");\n findById(\"toggle-docked\").addEventListener(\"esp-clicked\", () => dockedFlyout.toggle());\n</script>\n```\n\nBelow `COMPACT_VIEWPORT_MEDIA_QUERY` every flyout becomes a fixed\noverlay drawer over a vellum — there is no room for a side-by-side\nsplit on a phone. `mode=\"overlay\"` forces that presentation at any\nwidth (try it here; `Escape` or the vellum closes it):\n\n```html\n<style>\nesp-page.flyout-overlay-demo {\n &::part(wrapper) {\n min-height: 160px;\n height: 160px;\n }\n > div {\n padding: var(--esp-size-padding);\n }\n}\n</style>\n<esp-page class=\"flyout-overlay-demo\">\n <esp-flyout slot=\"flyout\" heading=\"Overlay drawer\" mode=\"overlay\" id=\"overlay-flyout\" standalone>\n <p>A fixed drawer over a vellum — what every flyout becomes on\n small viewports.</p>\n </esp-flyout>\n <div>\n <esp-button id=\"open-overlay\" label=\"Open overlay flyout\" collapsed></esp-button>\n </div>\n</esp-page>\n<script>\n const overlayFlyout = findById(\"overlay-flyout\");\n findById(\"open-overlay\").addEventListener(\"esp-clicked\", () => overlayFlyout.toggle());\n</script>\n```\n\nComponents that cannot know where the flyout lives can request it\nover the bus with `showFlyout()` — the same pattern `showToast()`\nuses. A second request swaps the content in place; it never\nstacks. Content passed as a string renders as plain text; pass a\n`Node` for rich content. Pass the triggering element as `anchor` to\nalign the in-grid panel with that control and keep both in the same\nscroll flow. If the panel would cross the visible viewport's bottom,\nit shifts up only far enough to fit and returns to its natural trigger\nalignment as the page scrolls. Content taller than the viewport\nscrolls inside the panel. Pass `fullHeight: true` (or set the\n`full-height` attribute) when a reading surface should fill the visible\nblock-size even when its content is short. Overlay drawers stay\nviewport-fixed.\n\nBus requests anchored inside nested pages are presented by the\noutermost containing page's flyout by default. This keeps previews,\nembedded tools, and nested layouts from trapping application-level\nhelp or detail panels. Set `scope=\"nearest\"` on a nested flyout when\nthat page should explicitly own requests originating inside it.\nThe demos above are each driven by their own toggle button, so they\nare marked `standalone` to opt out of the bus entirely.\n\n```html\n<style>\nesp-page.flyout-bus-demo {\n --esp-page-main-max-width: 420px;\n &::part(wrapper) {\n min-height: 320px;\n }\n > div {\n padding: var(--esp-size-padding);\n }\n .flyout-demo-copy {\n max-inline-size: 42ch;\n }\n}\n</style>\n<esp-page class=\"flyout-bus-demo\">\n <esp-flyout slot=\"flyout\"></esp-flyout>\n <div>\n <esp-button id=\"show-apples\" label=\"Apples\" collapsed></esp-button>\n <div class=\"flyout-demo-copy\">\n <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod\n tempor incididunt ut labore et dolore magna aliqua.</p>\n <p>Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi\n ut aliquip ex ea commodo consequat.</p>\n <p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum\n dolore eu fugiat nulla pariatur.</p>\n </div>\n <esp-button id=\"show-pears\" label=\"Pears\" collapsed></esp-button>\n <template id=\"pears-description\">\n <p>Ripen pears at room temperature until the neck yields gently to\n pressure, then refrigerate them to slow further ripening.</p>\n <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer\n nec odio. Praesent libero. Sed cursus ante dapibus diam.</p>\n <p>Sed nisi. Nulla quis sem at nibh elementum imperdiet. Duis\n sagittis ipsum. Praesent mauris. Fusce nec tellus sed augue semper\n porta.</p>\n <p>Mauris massa. Vestibulum lacinia arcu eget nulla. Class aptent\n taciti sociosqu ad litora torquent per conubia nostra.</p>\n <p>Curabitur sodales ligula in libero. Sed dignissim lacinia nunc.\n Curabitur tortor. Pellentesque nibh. Aenean quam.</p>\n <p>In scelerisque sem at dolor. Maecenas mattis. Sed convallis\n tristique sem. Proin ut ligula vel nunc egestas porttitor.</p>\n <p>Morbi lectus risus, iaculis vel, suscipit quis, luctus non,\n massa. Fusce ac turpis quis ligula lacinia aliquet.</p>\n <p>Mauris ipsum. Nulla metus metus, ullamcorper vel, tincidunt sed,\n euismod in, nibh. Quisque volutpat condimentum velit.</p>\n </template>\n <div class=\"flyout-demo-copy\">\n <p>Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia\n deserunt mollit anim id est laborum.</p>\n <p>Curabitur pretium tincidunt lacus. Nulla gravida orci a odio, nullam\n varius, turpis et commodo pharetra.</p>\n </div>\n </div>\n</esp-page>\n<script>\n const applesButton = findById(\"show-apples\");\n applesButton.addEventListener(\"esp-clicked\", () => {\n showFlyout({ heading: \"Apples\", content: \"Crisp. Store cold.\", anchor: applesButton });\n });\n const pearsButton = findById(\"show-pears\");\n const pearsDescription = findById(\"pears-description\");\n pearsButton.addEventListener(\"esp-clicked\", () => {\n showFlyout({\n heading: \"Pears\",\n content: pearsDescription.content.cloneNode(true),\n anchor: pearsButton,\n });\n });\n</script>\n```",
21609
+ "description": "A panel for help and additional information. Uses spare page width when\navailable and switches to an overlay when the workspace cannot fit it.\n\nPlace one `esp-flyout` in an `esp-page`'s `flyout` slot. The panel\nlives on the canvas after any persistent preview, outside the content\nsurface. It negotiates between `--esp-page-flyout-min-width` and\n`--esp-page-flyout-max-width`; preview receives flexible room first. The page\ndecides where help goes, from widest viewport to narrowest:\n\n1. **Gutter** — the panel occupies the right canvas gutter. When the\n gutter already has room, it claims existing spare canvas and the\n main content does not move at all, on any alignment.\n2. **Shift** — when the gutter is not quite wide enough, the surface\n slides toward `start` by exactly the shortfall (no more) to free\n the difference.\n3. **Docked sidebar** — once the gutter is gone, the panel competes\n for width with the main well; content narrows but is never\n covered.\n4. **Overlay drawer** — when the complete sidebar + main + help\n minimums do not fit, or below `COMPACT_VIEWPORT_MEDIA_QUERY` (the\n same breakpoint the `esp-menu` drawer uses), the panel becomes a fixed\n drawer over a vellum backdrop. Set `mode=\"overlay\"` to get this\n presentation at every width.\n\nThe panel is styled as a tear-off piece: a dotted perforation on\nits leading edge, rounded trailing corners, and no shadow — all\noverridable through the `--esp-flyout-*` tokens below.\nAn anchored panel shown with preview keeps its caret at the main/preview\nedge and receives a non-interactive dotted bridge across preview from the\npage. A short, thick terminus on the preview/help seam completes that\nconnection; no consumer positioning is required.\n\nA11y follows the presentation. In the in-grid modes (1–3) the\nflyout is a non-modal `complementary` landmark named by its\n`heading`, opening it never steals focus, and boundary scrolling chains\nback to the main document. In the overlay-drawer mode (4) it is a true\nmodal: `role=\"dialog\"` + `aria-modal`, the\nbackground goes inert, scroll is locked, focus moves into the drawer\nand is trapped, and focus is restored on close. `Escape` closes it\nin every mode.\n\n```html\n<style>\nesp-page.flyout-demo {\n --esp-page-main-max-width: 420px;\n --esp-page-canvas-background: var(--esp-color-layer-1);\n &::part(wrapper) {\n min-height: 320px;\n height: 320px;\n }\n > div {\n padding: var(--esp-size-padding);\n }\n}\n</style>\n<esp-page class=\"flyout-demo\" align=\"center\">\n <esp-flyout slot=\"flyout\" heading=\"More info\" id=\"demo-flyout\" standalone>\n <p>Spare width claimed; the well shifted only enough to make room.</p>\n </esp-flyout>\n <div>\n <esp-button id=\"toggle-flyout\" label=\"Toggle flyout\" collapsed></esp-button>\n </div>\n</esp-page>\n<script>\n const flyout = findById(\"demo-flyout\");\n findById(\"toggle-flyout\").addEventListener(\"esp-clicked\", () => flyout.toggle());\n</script>\n```\n\nBy default the panel is a tear-off — a dotted perforation on its\nleading edge, rounded trailing corners, no shadow. Add\n`match-surface` to lift the trailing edge with the page's surface\nedge shadow, so the flyout reads as a raised peer of the content it\nflies out from while staying attached at the leading perforation:\n\n```html\n<style>\nesp-page.flyout-match-demo {\n --esp-page-main-max-width: 420px;\n &::part(wrapper) {\n min-height: 320px;\n height: 320px;\n }\n > div {\n padding: var(--esp-size-padding);\n }\n}\n</style>\n<esp-page class=\"flyout-match-demo\" align=\"center\">\n <esp-flyout slot=\"flyout\" heading=\"More info\" id=\"match-flyout\" match-surface standalone>\n <p>Attached at the perforation, trailing edge raised like the surface.</p>\n </esp-flyout>\n <div>\n <esp-button id=\"toggle-match\" label=\"Toggle flyout\" collapsed></esp-button>\n </div>\n</esp-page>\n<script>\n const matchFlyout = findById(\"match-flyout\");\n findById(\"toggle-match\").addEventListener(\"esp-clicked\", () => matchFlyout.toggle());\n</script>\n```\n\nWhen the page has no spare width to claim — a `kind=\"full\"` page,\nor a capped page on a viewport at the cap — the open flyout docks\nas a right sidebar and the content makes room:\n\n```html\n<style>\nesp-page.flyout-docked-demo {\n &::part(wrapper) {\n min-height: 320px;\n height: 320px;\n }\n > div {\n padding: var(--esp-size-padding);\n }\n}\n</style>\n<esp-page class=\"flyout-docked-demo\" kind=\"full\">\n <esp-flyout slot=\"flyout\" heading=\"Docked\" id=\"docked-flyout\" standalone>\n <p>No gutter to claim, so the content narrows instead.</p>\n </esp-flyout>\n <div>\n <p>A <code>kind=\"full\"</code> page has no canvas gutters at any\n width — the flyout squeezes the main well and never covers it.</p>\n <esp-button id=\"toggle-docked\" label=\"Toggle flyout\" collapsed></esp-button>\n </div>\n</esp-page>\n<script>\n const dockedFlyout = findById(\"docked-flyout\");\n findById(\"toggle-docked\").addEventListener(\"esp-clicked\", () => dockedFlyout.toggle());\n</script>\n```\n\nBelow `COMPACT_VIEWPORT_MEDIA_QUERY` every flyout becomes a fixed\noverlay drawer over a vellum — there is no room for a side-by-side\nsplit on a phone. `mode=\"overlay\"` forces that presentation at any\nwidth (try it here; `Escape` or the vellum closes it):\n\n```html\n<style>\nesp-page.flyout-overlay-demo {\n &::part(wrapper) {\n min-height: 160px;\n height: 160px;\n }\n > div {\n padding: var(--esp-size-padding);\n }\n}\n</style>\n<esp-page class=\"flyout-overlay-demo\">\n <esp-flyout slot=\"flyout\" heading=\"Overlay drawer\" mode=\"overlay\" id=\"overlay-flyout\" standalone>\n <p>A fixed drawer over a vellum — what every flyout becomes on\n small viewports.</p>\n </esp-flyout>\n <div>\n <esp-button id=\"open-overlay\" label=\"Open overlay flyout\" collapsed></esp-button>\n </div>\n</esp-page>\n<script>\n const overlayFlyout = findById(\"overlay-flyout\");\n findById(\"open-overlay\").addEventListener(\"esp-clicked\", () => overlayFlyout.toggle());\n</script>\n```\n\nComponents that cannot know where the flyout lives can request it\nover the bus with `showFlyout()` — the same pattern `showToast()`\nuses. A second request swaps the content in place; it never\nstacks. Content passed as a string renders as plain text; pass a\n`Node` for rich content. Pass the triggering element as `anchor` to\nalign the in-grid panel with that control and keep both in the same\nscroll flow. If the panel would cross the visible viewport's bottom,\nit shifts up only far enough to fit and returns to its natural trigger\nalignment as the page scrolls. Content taller than the viewport\nscrolls inside the panel. Pass `fullHeight: true` (or set the\n`full-height` attribute) when a reading surface should fill the visible\nblock-size even when its content is short. Overlay drawers stay\nviewport-fixed.\n\nBus requests anchored inside nested pages are presented by the\noutermost containing page's flyout by default. This keeps previews,\nembedded tools, and nested layouts from trapping application-level\nhelp or detail panels. Set `scope=\"nearest\"` on a nested flyout when\nthat page should explicitly own requests originating inside it.\nThe demos above are each driven by their own toggle button, so they\nare marked `standalone` to opt out of the bus entirely.\n\n```html\n<style>\nesp-page.flyout-bus-demo {\n --esp-page-main-max-width: 420px;\n &::part(wrapper) {\n min-height: 320px;\n }\n > div {\n padding: var(--esp-size-padding);\n }\n .flyout-demo-copy {\n max-inline-size: 42ch;\n }\n}\n</style>\n<esp-page class=\"flyout-bus-demo\">\n <esp-flyout slot=\"flyout\"></esp-flyout>\n <div>\n <esp-button id=\"show-apples\" label=\"Apples\" collapsed></esp-button>\n <div class=\"flyout-demo-copy\">\n <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod\n tempor incididunt ut labore et dolore magna aliqua.</p>\n <p>Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi\n ut aliquip ex ea commodo consequat.</p>\n <p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum\n dolore eu fugiat nulla pariatur.</p>\n </div>\n <esp-button id=\"show-pears\" label=\"Pears\" collapsed></esp-button>\n <template id=\"pears-description\">\n <p>Ripen pears at room temperature until the neck yields gently to\n pressure, then refrigerate them to slow further ripening.</p>\n <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer\n nec odio. Praesent libero. Sed cursus ante dapibus diam.</p>\n <p>Sed nisi. Nulla quis sem at nibh elementum imperdiet. Duis\n sagittis ipsum. Praesent mauris. Fusce nec tellus sed augue semper\n porta.</p>\n <p>Mauris massa. Vestibulum lacinia arcu eget nulla. Class aptent\n taciti sociosqu ad litora torquent per conubia nostra.</p>\n <p>Curabitur sodales ligula in libero. Sed dignissim lacinia nunc.\n Curabitur tortor. Pellentesque nibh. Aenean quam.</p>\n <p>In scelerisque sem at dolor. Maecenas mattis. Sed convallis\n tristique sem. Proin ut ligula vel nunc egestas porttitor.</p>\n <p>Morbi lectus risus, iaculis vel, suscipit quis, luctus non,\n massa. Fusce ac turpis quis ligula lacinia aliquet.</p>\n <p>Mauris ipsum. Nulla metus metus, ullamcorper vel, tincidunt sed,\n euismod in, nibh. Quisque volutpat condimentum velit.</p>\n </template>\n <div class=\"flyout-demo-copy\">\n <p>Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia\n deserunt mollit anim id est laborum.</p>\n <p>Curabitur pretium tincidunt lacus. Nulla gravida orci a odio, nullam\n varius, turpis et commodo pharetra.</p>\n </div>\n </div>\n</esp-page>\n<script>\n const applesButton = findById(\"show-apples\");\n applesButton.addEventListener(\"esp-clicked\", () => {\n showFlyout({ heading: \"Apples\", content: \"Crisp. Store cold.\", anchor: applesButton });\n });\n const pearsButton = findById(\"show-pears\");\n const pearsDescription = findById(\"pears-description\");\n pearsButton.addEventListener(\"esp-clicked\", () => {\n showFlyout({\n heading: \"Pears\",\n content: pearsDescription.content.cloneNode(true),\n anchor: pearsButton,\n });\n });\n</script>\n```",
21411
21610
  "name": "EspalierFlyout",
21412
21611
  "cssProperties": [
21413
21612
  {
@@ -21435,7 +21634,7 @@
21435
21634
  "name": "--esp-flyout-z-index"
21436
21635
  },
21437
21636
  {
21438
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
21637
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
21439
21638
  "name": "--esp-field-background",
21440
21639
  "inheritedFrom": {
21441
21640
  "name": "EspalierElementBase",
@@ -22866,6 +23065,17 @@
22866
23065
  "description": "Which containing page owns anchored bus requests.\n\n- `\"outermost\"` (default) routes to the flyout in the outermost\n `esp-page` containing the request trigger.\n- `\"nearest\"` explicitly gives this flyout precedence for requests\n originating in its own page or nested descendants.\n\nA `scope=\"nearest\"` flyout must be a light-DOM descendant of the\n`esp-page` whose requests it owns so that page can discover it.\n\nRequests without an `anchor` or `returnFocusTo` have no local context,\nso only outermost page flyouts service them. Use the instance API for\na directly controlled nested flyout.",
22867
23066
  "fieldName": "scope"
22868
23067
  },
23068
+ {
23069
+ "type": {
23070
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
23071
+ },
23072
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
23073
+ "name": "surface-layer",
23074
+ "inheritedFrom": {
23075
+ "name": "EspalierElementBase",
23076
+ "module": "dist/shared/esp-element-base.js"
23077
+ }
23078
+ },
22869
23079
  {
22870
23080
  "name": "scheme",
22871
23081
  "type": {
@@ -22997,7 +23207,7 @@
22997
23207
  "name": "--esp-focus-picker-focus-outline"
22998
23208
  },
22999
23209
  {
23000
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
23210
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
23001
23211
  "name": "--esp-field-background",
23002
23212
  "inheritedFrom": {
23003
23213
  "name": "EspalierElementBase",
@@ -23981,6 +24191,17 @@
23981
24191
  "description": "Aspect ratio for the compact live preview.",
23982
24192
  "fieldName": "compactRatio"
23983
24193
  },
24194
+ {
24195
+ "type": {
24196
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
24197
+ },
24198
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
24199
+ "name": "surface-layer",
24200
+ "inheritedFrom": {
24201
+ "name": "EspalierElementBase",
24202
+ "module": "dist/shared/esp-element-base.js"
24203
+ }
24204
+ },
23984
24205
  {
23985
24206
  "name": "scheme",
23986
24207
  "type": {
@@ -25108,6 +25329,17 @@
25108
25329
  "description": "The placeholder text for the picker input.",
25109
25330
  "fieldName": "placeholder"
25110
25331
  },
25332
+ {
25333
+ "type": {
25334
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
25335
+ },
25336
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
25337
+ "name": "surface-layer",
25338
+ "inheritedFrom": {
25339
+ "name": "EspalierElementBase",
25340
+ "module": "dist/shared/esp-element-base.js"
25341
+ }
25342
+ },
25111
25343
  {
25112
25344
  "name": "scheme",
25113
25345
  "type": {
@@ -25174,7 +25406,7 @@
25174
25406
  },
25175
25407
  "cssProperties": [
25176
25408
  {
25177
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
25409
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
25178
25410
  "name": "--esp-field-background",
25179
25411
  "inheritedFrom": {
25180
25412
  "name": "EspalierElementBase",
@@ -25472,7 +25704,7 @@
25472
25704
  "name": "--esp-footer-row-gap"
25473
25705
  },
25474
25706
  {
25475
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
25707
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
25476
25708
  "name": "--esp-field-background",
25477
25709
  "inheritedFrom": {
25478
25710
  "name": "EspalierElementBase",
@@ -26287,6 +26519,17 @@
26287
26519
  "description": ""
26288
26520
  },
26289
26521
  "attributes": [
26522
+ {
26523
+ "type": {
26524
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
26525
+ },
26526
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
26527
+ "name": "surface-layer",
26528
+ "inheritedFrom": {
26529
+ "name": "EspalierElementBase",
26530
+ "module": "dist/shared/esp-element-base.js"
26531
+ }
26532
+ },
26290
26533
  {
26291
26534
  "name": "scheme",
26292
26535
  "type": {
@@ -27122,6 +27365,17 @@
27122
27365
  "description": "Visible heading for the link group. It also labels the navigation\nlandmark. When omitted, the landmark receives the fallback label\n“Footer links” without rendering an empty heading.",
27123
27366
  "fieldName": "heading"
27124
27367
  },
27368
+ {
27369
+ "type": {
27370
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
27371
+ },
27372
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
27373
+ "name": "surface-layer",
27374
+ "inheritedFrom": {
27375
+ "name": "EspalierElementBase",
27376
+ "module": "dist/shared/esp-element-base.js"
27377
+ }
27378
+ },
27125
27379
  {
27126
27380
  "name": "scheme",
27127
27381
  "type": {
@@ -27184,7 +27438,7 @@
27184
27438
  },
27185
27439
  "cssProperties": [
27186
27440
  {
27187
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
27441
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
27188
27442
  "name": "--esp-field-background",
27189
27443
  "inheritedFrom": {
27190
27444
  "name": "EspalierElementBase",
@@ -27268,7 +27522,7 @@
27268
27522
  "declarations": [
27269
27523
  {
27270
27524
  "kind": "class",
27271
- "description": "A responsive site footer with structured spaces for branding, feature\nartwork, link groups, supporting actions, and legal or utility content.\nIts background remains full-bleed while its content automatically aligns\nto the surface of a containing [`<esp-page>`](/components/page/).\nThe decorative background uses the same internal visual-layer primitive as\n`esp-image` banners; the footer's public background tokens are unchanged.\n\nUse bare [`<esp-footer-link-group>`](/components/footer/link-group/) children\nwhen navigation groups should auto-flow independently through the grid.\n`columns` is a maximum: the grid always drops columns when they no longer fit.\n\n```html\n<esp-footer\n light-brand-logo=\"/assets/logo-light.svg\"\n dark-brand-logo=\"/assets/logo-dark.svg\"\n brand-text=\"Espalier\"\n brand-href=\"/\"\n brand-alt=\"Espalier logo\"\n light-brand-color=\"oklch(0.35 0.12 216)\"\n dark-brand-color=\"oklch(0.9 0.08 216)\"\n columns=\"3\">\n <esp-footer-link-group heading=\"Explore\">\n <a href=\"/components\">Components</a>\n <a href=\"/api\">API</a>\n </esp-footer-link-group>\n <esp-footer-link-group heading=\"Learn\">\n <a href=\"/guides/getting-started\">Getting started</a>\n <a href=\"/guides/styling\">Styling</a>\n </esp-footer-link-group>\n <p slot=\"aside\">Themeable web components built on web standards.</p>\n <div slot=\"bottom\">Copyright 2026 Example Organization</div>\n</esp-footer>\n```\n\nFor creator-controlled columns, wrap each ordered stack of groups in\n[`<esp-footer-column>`](/components/footer/column/). Each wrapper is one grid\nitem at wide sizes; below the footer's compact container breakpoint, the\ncolumns become full-width rows in their slotted order. `columns` remains the\nmaximum number of grid items per row:\n\n```html\n<esp-footer columns=\"3\">\n <esp-footer-column>\n <esp-footer-link-group heading=\"Product\">\n <a href=\"/features\">Features</a>\n <a href=\"/pricing\">Pricing</a>\n </esp-footer-link-group>\n <esp-footer-link-group heading=\"Company\">\n <a href=\"/about\">About</a>\n <a href=\"/careers\">Careers</a>\n </esp-footer-link-group>\n </esp-footer-column>\n <esp-footer-column>\n <esp-footer-link-group heading=\"Resources\">\n <a href=\"/docs\">Documentation</a>\n <a href=\"/support\">Support</a>\n </esp-footer-link-group>\n </esp-footer-column>\n</esp-footer>\n```\n\nA slotted brand replaces configured branding, while the media slot accepts\na responsive `picture`, native image, or `esp-image`. Media spans the\nsurface-aligned content well; use the decorative background layer for\nedge-to-edge artwork:\n\n```html\n<esp-footer columns=\"4\">\n <picture slot=\"media\">\n <source media=\"(prefers-color-scheme: dark)\" srcset=\"/assets/canvas-bg-dark.jpg\" />\n <img\n src=\"/assets/canvas-bg.jpg\"\n alt=\"A colorful floral pattern\"\n style=\"display: block; width: 100%; height: auto;\" />\n </picture>\n <a slot=\"brand\" href=\"/\" aria-label=\"Espalier home\">\n <img src=\"/assets/espalier-logo.svg\" alt=\"Espalier\" />\n </a>\n <esp-footer-link-group heading=\"Product\">\n <a href=\"/components\">Components</a>\n </esp-footer-link-group>\n</esp-footer>\n```\n\nDecorative art uses an image layer independent of the base color. The same\nAPI supports a repeating landscape band or one large cover image:\n\n```html\n<style>\n esp-footer.mountains {\n --esp-footer-background: oklch(0.48 0.12 145);\n --esp-footer-background-image-opacity: 0.55;\n --esp-footer-background-image:\n linear-gradient(to bottom, transparent 8rem, oklch(0.48 0.12 145)),\n url(\"/assets/espalier-geometric-texture.png\");\n --esp-footer-background-repeat: no-repeat, repeat-x;\n --esp-footer-background-position: 0 0, top;\n --esp-footer-background-size: 100% 100%, auto 10rem;\n --esp-footer-padding-block: 6rem;\n }\n\n esp-footer.photo {\n --esp-footer-background: oklch(0.2 0.04 216);\n --esp-footer-heading-color: white;\n --esp-footer-background-image:\n linear-gradient(oklch(0.1 0 0 / 0.55), oklch(0.1 0 0 / 0.55)),\n url(\"/assets/canvas-bg.jpg\");\n --esp-footer-background-repeat: no-repeat;\n --esp-footer-background-position: center;\n --esp-footer-background-size: cover;\n }\n</style>\n<esp-footer class=\"mountains\" brand-text=\"Mountain Studio\"></esp-footer>\n<esp-footer class=\"photo\" brand-text=\"Photo Studio\"></esp-footer>\n```\n\nSurface alignment is automatic inside `esp-page`; opt out only when the\nfooter's content should also span the entire bar:\n\n```html\n<esp-page align=\"center\" style=\"--esp-page-main-max-width: 36rem;\">\n <main>Surface-aligned page content</main>\n <esp-footer slot=\"footer\" brand-text=\"Aligned Studio\" columns=\"3\">\n <esp-footer-link-group heading=\"Explore\">\n <a href=\"/work\">Work</a>\n <a href=\"/about\">About</a>\n </esp-footer-link-group>\n </esp-footer>\n</esp-page>\n\n<esp-footer full-bleed-content brand-text=\"Full-width Studio\"></esp-footer>\n```",
27525
+ "description": "A responsive site footer with structured spaces for branding, feature\nartwork, link groups, supporting actions, and legal or utility content.\nIts background remains full-bleed while its content automatically aligns\nto the surface of a containing [`<esp-page>`](/components/page/).\nUse bare [`<esp-footer-link-group>`](/components/footer/link-group/) children\nwhen navigation groups should auto-flow independently through the grid.\n`columns` is a maximum: the grid always drops columns when they no longer fit.\n\nA field fills one layer above the surface it sits on.\nUse `surface-layer=\"1|2|3|4\"` to select the default painted layer.\n\n```html\n<esp-footer\n light-brand-logo=\"/assets/logo-light.svg\"\n dark-brand-logo=\"/assets/logo-dark.svg\"\n brand-text=\"Espalier\"\n brand-href=\"/\"\n brand-alt=\"Espalier logo\"\n light-brand-color=\"oklch(0.35 0.12 216)\"\n dark-brand-color=\"oklch(0.9 0.08 216)\"\n columns=\"3\">\n <esp-footer-link-group heading=\"Explore\">\n <a href=\"/components\">Components</a>\n <a href=\"/api\">API</a>\n </esp-footer-link-group>\n <esp-footer-link-group heading=\"Learn\">\n <a href=\"/guides/getting-started\">Getting started</a>\n <a href=\"/guides/styling\">Styling</a>\n </esp-footer-link-group>\n <p slot=\"aside\">Themeable web components built on web standards.</p>\n <div slot=\"bottom\">Copyright 2026 Example Organization</div>\n</esp-footer>\n```\n\nFor creator-controlled columns, wrap each ordered stack of groups in\n[`<esp-footer-column>`](/components/footer/column/). Each wrapper is one grid\nitem at wide sizes; below the footer's compact container breakpoint, the\ncolumns become full-width rows in their slotted order. `columns` remains the\nmaximum number of grid items per row:\n\n```html\n<esp-footer columns=\"3\">\n <esp-footer-column>\n <esp-footer-link-group heading=\"Product\">\n <a href=\"/features\">Features</a>\n <a href=\"/pricing\">Pricing</a>\n </esp-footer-link-group>\n <esp-footer-link-group heading=\"Company\">\n <a href=\"/about\">About</a>\n <a href=\"/careers\">Careers</a>\n </esp-footer-link-group>\n </esp-footer-column>\n <esp-footer-column>\n <esp-footer-link-group heading=\"Resources\">\n <a href=\"/docs\">Documentation</a>\n <a href=\"/support\">Support</a>\n </esp-footer-link-group>\n </esp-footer-column>\n</esp-footer>\n```\n\nA slotted brand replaces configured branding, while the media slot accepts\na responsive `picture`, native image, or `esp-image`. Media spans the\nsurface-aligned content well; use the decorative background layer for\nedge-to-edge artwork:\n\n```html\n<esp-footer columns=\"4\">\n <picture slot=\"media\">\n <source media=\"(prefers-color-scheme: dark)\" srcset=\"/assets/canvas-bg-dark.jpg\" />\n <img\n src=\"/assets/canvas-bg.jpg\"\n alt=\"A colorful floral pattern\"\n style=\"display: block; width: 100%; height: auto;\" />\n </picture>\n <a slot=\"brand\" href=\"/\" aria-label=\"Espalier home\">\n <img src=\"/assets/espalier-logo.svg\" alt=\"Espalier\" />\n </a>\n <esp-footer-link-group heading=\"Product\">\n <a href=\"/components\">Components</a>\n </esp-footer-link-group>\n</esp-footer>\n```\n\nDecorative art uses an image layer independent of the base color. The same\nAPI supports a repeating landscape band or one large cover image:\n\n```html\n<style>\n esp-footer.mountains {\n --esp-footer-background: oklch(0.48 0.12 145);\n --esp-footer-background-image-opacity: 0.55;\n --esp-footer-background-image:\n linear-gradient(to bottom, transparent 8rem, oklch(0.48 0.12 145)),\n url(\"/assets/espalier-geometric-texture.png\");\n --esp-footer-background-repeat: no-repeat, repeat-x;\n --esp-footer-background-position: 0 0, top;\n --esp-footer-background-size: 100% 100%, auto 10rem;\n --esp-footer-padding-block: 6rem;\n }\n\n esp-footer.photo {\n --esp-footer-background: oklch(0.2 0.04 216);\n --esp-footer-heading-color: white;\n --esp-footer-background-image:\n linear-gradient(oklch(0.1 0 0 / 0.55), oklch(0.1 0 0 / 0.55)),\n url(\"/assets/canvas-bg.jpg\");\n --esp-footer-background-repeat: no-repeat;\n --esp-footer-background-position: center;\n --esp-footer-background-size: cover;\n }\n</style>\n<esp-footer class=\"mountains\" brand-text=\"Mountain Studio\"></esp-footer>\n<esp-footer class=\"photo\" brand-text=\"Photo Studio\"></esp-footer>\n```\n\nSurface alignment is automatic inside `esp-page`; opt out only when the\nfooter's content should also span the entire bar:\n\n```html\n<esp-page align=\"center\" style=\"--esp-page-main-max-width: 36rem;\">\n <main>Surface-aligned page content</main>\n <esp-footer slot=\"footer\" brand-text=\"Aligned Studio\" columns=\"3\">\n <esp-footer-link-group heading=\"Explore\">\n <a href=\"/work\">Work</a>\n <a href=\"/about\">About</a>\n </esp-footer-link-group>\n </esp-footer>\n</esp-page>\n\n<esp-footer full-bleed-content brand-text=\"Full-width Studio\"></esp-footer>\n```",
27272
27526
  "name": "EspalierFooter",
27273
27527
  "cssProperties": [
27274
27528
  {
@@ -27372,7 +27626,7 @@
27372
27626
  "name": "--esp-footer-content-lead"
27373
27627
  },
27374
27628
  {
27375
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
27629
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
27376
27630
  "name": "--esp-field-background",
27377
27631
  "inheritedFrom": {
27378
27632
  "name": "EspalierElementBase",
@@ -28489,6 +28743,17 @@
28489
28743
  "description": "Optional accessible label for pages containing multiple footer landmarks.",
28490
28744
  "fieldName": "landmarkLabel"
28491
28745
  },
28746
+ {
28747
+ "type": {
28748
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
28749
+ },
28750
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
28751
+ "name": "surface-layer",
28752
+ "inheritedFrom": {
28753
+ "name": "EspalierElementBase",
28754
+ "module": "dist/shared/esp-element-base.js"
28755
+ }
28756
+ },
28492
28757
  {
28493
28758
  "name": "scheme",
28494
28759
  "type": {
@@ -30146,7 +30411,7 @@
30146
30411
  },
30147
30412
  {
30148
30413
  "kind": "class",
30149
- "description": "Renders data in a table. Specify grid column definitions by\nadding [esp-grid-column](/components/grid/grid-column) elements to\nthe default slot.",
30414
+ "description": "Renders data in a table. Specify grid column definitions by\nadding [esp-grid-column](/components/grid/grid-column) elements to\nthe default slot.\n\nA field fills one layer above the surface it sits on.\nUse `surface-layer=\"1|2|3|4\"` to select the default painted layer.",
30150
30415
  "name": "EspalierGrid",
30151
30416
  "cssProperties": [
30152
30417
  {
@@ -30202,7 +30467,7 @@
30202
30467
  "name": "--esp-grid-link-hover-color"
30203
30468
  },
30204
30469
  {
30205
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
30470
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
30206
30471
  "name": "--esp-field-background",
30207
30472
  "inheritedFrom": {
30208
30473
  "name": "EspalierElementBase",
@@ -31930,6 +32195,17 @@
31930
32195
  "description": "The fields to include when filtering results. It defaults to\nan empty array, which includes all fields. The test dataset\nincludes an address field that is not displayed, so to only\nfilter results by fields displayed in the grid, specify those\nfields in the `search-fields` attribute.\n\n> **Warning:** When specifying arrays in plain HTML, the attributes\n> should be wrapped in single quotes.\n\n```html\n<esp-grid\n data-url=\"/assets/data/grid-data.json\"\n search-fields='[\"first_name\", \"last_name\"]'>\n <esp-grid-column header=\"Id\" field-name=\"id\" text-align=\"right\" sortable sort-type=\"number\"></esp-grid-column>\n <esp-grid-column header=\"Name\" sort-field=\"first_name\" sortable>\n <a href=\"mailto:${email}\">${first_name} ${last_name}</a>\n </esp-grid-column>\n</esp-grid>\n```",
31931
32196
  "fieldName": "searchFields"
31932
32197
  },
32198
+ {
32199
+ "type": {
32200
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
32201
+ },
32202
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
32203
+ "name": "surface-layer",
32204
+ "inheritedFrom": {
32205
+ "name": "EspalierElementBase",
32206
+ "module": "dist/shared/esp-element-base.js"
32207
+ }
32208
+ },
31933
32209
  {
31934
32210
  "name": "scheme",
31935
32211
  "type": {
@@ -32124,7 +32400,7 @@
32124
32400
  },
32125
32401
  "privacy": "public",
32126
32402
  "default": "null",
32127
- "description": "Id of the region this button controls, forwarded to the inner\n`<button>`.\n\nThe host is not the control — it has no tabindex and no key handling, so\nthe native button inside the shadow root is what assistive technology and\nrole-based locators resolve to. State set on the host has no path to it\nunless the component carries it across, which is what this does.\n\nForwarding the string is not the same as forwarding the relationship: an\nid reference resolves only within the referring element's own tree, so a\ntarget outside this button's shadow root stays unresolved. ESP0222 tracks\ncarrying a resolved element reference instead.\n\n`ariaControls` is deliberately not an `override`: `ARIAMixin` carries\n`ariaControlsElements`, not a string reflection, so there is no base\nmember to override. If a future lib adds one, `noImplicitOverride` will\nsay so here.",
32403
+ "description": "Id of the controlled region, forwarded to the inner native button.\n\nAssistive technology and role locators target that button, so host state must\nbe forwarded. A string id reference still cannot resolve a target outside\nits shadow root; ESP0222 tracks forwarding a resolved element reference.\n\n`ariaControls` has no base string member in the current DOM types. Keep it\nwithout `override`; `noImplicitOverride` will flag a future base declaration.",
32128
32404
  "attribute": "aria-controls"
32129
32405
  },
32130
32406
  {
@@ -32187,7 +32463,7 @@
32187
32463
  "text": "string | null"
32188
32464
  },
32189
32465
  "default": "null",
32190
- "description": "Id of the region this button controls, forwarded to the inner\n`<button>`.\n\nThe host is not the control — it has no tabindex and no key handling, so\nthe native button inside the shadow root is what assistive technology and\nrole-based locators resolve to. State set on the host has no path to it\nunless the component carries it across, which is what this does.\n\nForwarding the string is not the same as forwarding the relationship: an\nid reference resolves only within the referring element's own tree, so a\ntarget outside this button's shadow root stays unresolved. ESP0222 tracks\ncarrying a resolved element reference instead.\n\n`ariaControls` is deliberately not an `override`: `ARIAMixin` carries\n`ariaControlsElements`, not a string reflection, so there is no base\nmember to override. If a future lib adds one, `noImplicitOverride` will\nsay so here.",
32466
+ "description": "Id of the controlled region, forwarded to the inner native button.\n\nAssistive technology and role locators target that button, so host state must\nbe forwarded. A string id reference still cannot resolve a target outside\nits shadow root; ESP0222 tracks forwarding a resolved element reference.\n\n`ariaControls` has no base string member in the current DOM types. Keep it\nwithout `override`; `noImplicitOverride` will flag a future base declaration.",
32191
32467
  "fieldName": "ariaControls"
32192
32468
  },
32193
32469
  {
@@ -32254,7 +32530,7 @@
32254
32530
  "declarations": [
32255
32531
  {
32256
32532
  "kind": "class",
32257
- "description": "Used to build the top navigation for a site. An empty header\ndisplays a bar. It has three slots for populating header content:\n`topbar`, `brand`, `menu`, and `buttons`.\n\nThe header works together with several companion components:\n[`<esp-menu>`](/components/menu/) provides the navigation\ncontainer, [`<esp-menu-item>`](/components/menu/item/)\nrenders individual links, and\n[`<esp-header-button>`](/components/header/button/) adds icon\nbuttons for global actions.\n\n```html\n<esp-header>\n <div slot=\"brand\" style=\"background-color: red;\" class=\"header-example\">brand</div>\n <esp-menu slot=\"menu\" mode=\"horizontal\" style=\"background-color: green;\">\n <esp-menu-item label=\"menu\" url=\"#\"></esp-menu-item>\n </esp-menu>\n <div slot=\"buttons\" style=\"background-color: yellow;\" class=\"header-example\">buttons</div>\n</esp-header>\n```",
32533
+ "description": "Builds a site's top navigation bar. Use its four slots to add content:\n`topbar`, `brand`, `menu`, and `buttons`.\n\nA field fills one layer above the surface it sits on.\nUse `surface-layer=\"1|2|3|4\"` to select the default painted layer.\n\nThe header works together with several companion components:\n[`<esp-menu>`](/components/menu/) provides the navigation\ncontainer, [`<esp-menu-item>`](/components/menu/item/)\nrenders individual links, and\n[`<esp-header-button>`](/components/header/button/) adds icon\nbuttons for global actions.\n\n```html\n<esp-header>\n <div slot=\"brand\" style=\"background-color: red;\" class=\"header-example\">brand</div>\n <esp-menu slot=\"menu\" mode=\"horizontal\" style=\"background-color: green;\">\n <esp-menu-item label=\"menu\" url=\"#\"></esp-menu-item>\n </esp-menu>\n <div slot=\"buttons\" style=\"background-color: yellow;\" class=\"header-example\">buttons</div>\n</esp-header>\n```",
32258
32534
  "name": "EspalierHeader",
32259
32535
  "cssProperties": [
32260
32536
  {
@@ -32418,7 +32694,7 @@
32418
32694
  "name": "--esp-header-button-stroke-width"
32419
32695
  },
32420
32696
  {
32421
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
32697
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
32422
32698
  "name": "--esp-field-background",
32423
32699
  "inheritedFrom": {
32424
32700
  "name": "EspalierElementBase",
@@ -34245,6 +34521,17 @@
34245
34521
  "description": "Opt out of surface alignment. When an `esp-header` is slotted into an\n`esp-page`, it constrains its bar content to the page's content\nsurface by default (the bar background stays full-bleed). Set this to\nlet the content span the full width instead, as it did before the\npage grew alignment modes. Standalone headers (outside `esp-page`) are\nalready full-bleed and unaffected.",
34246
34522
  "fieldName": "fullBleedContent"
34247
34523
  },
34524
+ {
34525
+ "type": {
34526
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
34527
+ },
34528
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
34529
+ "name": "surface-layer",
34530
+ "inheritedFrom": {
34531
+ "name": "EspalierElementBase",
34532
+ "module": "dist/shared/esp-element-base.js"
34533
+ }
34534
+ },
34248
34535
  {
34249
34536
  "name": "scheme",
34250
34537
  "type": {
@@ -35252,6 +35539,17 @@
35252
35539
  "description": "Whether this button's exact help topic is currently showing.",
35253
35540
  "fieldName": "active"
35254
35541
  },
35542
+ {
35543
+ "type": {
35544
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
35545
+ },
35546
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
35547
+ "name": "surface-layer",
35548
+ "inheritedFrom": {
35549
+ "name": "EspalierElementBase",
35550
+ "module": "dist/shared/esp-element-base.js"
35551
+ }
35552
+ },
35255
35553
  {
35256
35554
  "name": "scheme",
35257
35555
  "type": {
@@ -35318,7 +35616,7 @@
35318
35616
  "customElement": true,
35319
35617
  "cssProperties": [
35320
35618
  {
35321
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
35619
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
35322
35620
  "name": "--esp-field-background",
35323
35621
  "inheritedFrom": {
35324
35622
  "name": "EspalierElementBase",
@@ -36400,7 +36698,7 @@
36400
36698
  "customElement": true,
36401
36699
  "cssProperties": [
36402
36700
  {
36403
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
36701
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
36404
36702
  "name": "--esp-field-background",
36405
36703
  "inheritedFrom": {
36406
36704
  "name": "EspalierElementBase",
@@ -36457,6 +36755,17 @@
36457
36755
  }
36458
36756
  ],
36459
36757
  "attributes": [
36758
+ {
36759
+ "type": {
36760
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
36761
+ },
36762
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
36763
+ "name": "surface-layer",
36764
+ "inheritedFrom": {
36765
+ "name": "EspalierElementBase",
36766
+ "module": "dist/shared/esp-element-base.js"
36767
+ }
36768
+ },
36460
36769
  {
36461
36770
  "name": "scheme",
36462
36771
  "type": {
@@ -36851,7 +37160,7 @@
36851
37160
  "name": "--esp-image-overlay-text-shadow"
36852
37161
  },
36853
37162
  {
36854
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
37163
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
36855
37164
  "name": "--esp-field-background",
36856
37165
  "inheritedFrom": {
36857
37166
  "name": "EspalierElementBase",
@@ -37612,7 +37921,7 @@
37612
37921
  }
37613
37922
  }
37614
37923
  ],
37615
- "description": "Grow the frame past its ratio when its copy would be clipped, and give\nthe ratio back once the copy fits again with its spacing (padding and\nmargins). Between the two the banner keeps its current state, so it\ncannot oscillate. A\nbanner only grows from a frame its ratio sizes: an explicit, capped, or\nstretched block size wins, as it always has.\n\nGrowth must take effect, and keep taking effect:\n- The first measurement after growing checks that the frame did not\n widen. A banner sized by its content would widen, fit, shrink back,\n and loop. (A frame that narrows, as when growing adds a classic\n scrollbar, is not that loop; it simply stays grown.)\n- Every measurement while grown (including on a viewport resize, see\n handleWindowResize) checks that the host grew with the frame. A\n max-block-size cap would clip the end of the copy and crop the image\n over a box nobody sees.\nEither failure returns the banner to its ratio and blocks growth until\nthe frame width or the host's max-block-size changes. The rollback\nwaits a frame, so a failed growth never loops inside one delivery.\n\nGrowing itself happens inside the observer callback, so the grown\nlayout paints in the same frame (no flash of clipped copy, no shift\nafter a static first paint). The host's own width observer then sees\nits height change in that delivery, and Chrome may log one benign\n\"ResizeObserver loop completed with undelivered notifications\" per\ngrowth; deferring growth a frame would trade that for a visible shift.\n\nReturns whether the state changed now (a re-render follows)."
37924
+ "description": "Grow a ratio-sized banner when its text would be clipped; restore the ratio\nwhen the copy and its spacing fit again. Use separate grow and shrink checks\nto avoid oscillation. Explicit, capped, or stretched block sizes take precedence.\n\nAfter growth, verify that the frame did not widen and that the host grew\nwith it. Content-sized width can create a grow/shrink loop, while a host cap\ncan leave copy clipped outside the visible image. Roll back either failure\non the next frame, then retry after the width or cap changes.\n\nGrow in the observer callback to avoid a painted frame with clipped copy.\nChrome may report one benign ResizeObserver notification warning per growth;\ndeferring growth would introduce a visible shift.\n\nReturns whether the state changed and needs a render."
37616
37925
  },
37617
37926
  {
37618
37927
  "kind": "method",
@@ -38685,6 +38994,17 @@
38685
38994
  "description": "Aspect ratio used below the component-relative compact threshold.",
38686
38995
  "fieldName": "compactRatio"
38687
38996
  },
38997
+ {
38998
+ "type": {
38999
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
39000
+ },
39001
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
39002
+ "name": "surface-layer",
39003
+ "inheritedFrom": {
39004
+ "name": "EspalierElementBase",
39005
+ "module": "dist/shared/esp-element-base.js"
39006
+ }
39007
+ },
38688
39008
  {
38689
39009
  "name": "scheme",
38690
39010
  "type": {
@@ -39179,7 +39499,7 @@
39179
39499
  "name": "--esp-image-picker-option-height"
39180
39500
  },
39181
39501
  {
39182
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
39502
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
39183
39503
  "name": "--esp-field-background",
39184
39504
  "inheritedFrom": {
39185
39505
  "name": "EspalierElementBase",
@@ -40357,6 +40677,17 @@
40357
40677
  "description": "The name used when this picker participates in a `<form>`.",
40358
40678
  "fieldName": "name"
40359
40679
  },
40680
+ {
40681
+ "type": {
40682
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
40683
+ },
40684
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
40685
+ "name": "surface-layer",
40686
+ "inheritedFrom": {
40687
+ "name": "EspalierElementBase",
40688
+ "module": "dist/shared/esp-element-base.js"
40689
+ }
40690
+ },
40360
40691
  {
40361
40692
  "name": "scheme",
40362
40693
  "type": {
@@ -40460,7 +40791,7 @@
40460
40791
  "name": "--esp-image-preview-overlay-text"
40461
40792
  },
40462
40793
  {
40463
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
40794
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
40464
40795
  "name": "--esp-field-background",
40465
40796
  "inheritedFrom": {
40466
40797
  "name": "EspalierElementBase",
@@ -41620,6 +41951,17 @@
41620
41951
  "description": "Whether the upload for this image has failed.\nShows an error overlay with a retry button.",
41621
41952
  "fieldName": "failed"
41622
41953
  },
41954
+ {
41955
+ "type": {
41956
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
41957
+ },
41958
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
41959
+ "name": "surface-layer",
41960
+ "inheritedFrom": {
41961
+ "name": "EspalierElementBase",
41962
+ "module": "dist/shared/esp-element-base.js"
41963
+ }
41964
+ },
41623
41965
  {
41624
41966
  "name": "scheme",
41625
41967
  "type": {
@@ -41699,7 +42041,7 @@
41699
42041
  "name": "--esp-image-upload-preview-background"
41700
42042
  },
41701
42043
  {
41702
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
42044
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
41703
42045
  "name": "--esp-field-background",
41704
42046
  "inheritedFrom": {
41705
42047
  "name": "EspalierElementBase",
@@ -43079,6 +43421,17 @@
43079
43421
  },
43080
43422
  "fieldName": "maxRowHeightVh"
43081
43423
  },
43424
+ {
43425
+ "type": {
43426
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
43427
+ },
43428
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
43429
+ "name": "surface-layer",
43430
+ "inheritedFrom": {
43431
+ "name": "EspalierElementBase",
43432
+ "module": "dist/shared/esp-element-base.js"
43433
+ }
43434
+ },
43082
43435
  {
43083
43436
  "name": "scheme",
43084
43437
  "type": {
@@ -43259,7 +43612,7 @@
43259
43612
  "declarations": [
43260
43613
  {
43261
43614
  "kind": "class",
43262
- "description": "Used to show an informational message.\n\n```html\n<esp-info>\n Info defaults to intent=\"info\" and its informational surface treatment.\n</esp-info>\n```",
43615
+ "description": "Shows an informational or status message.\n\nA field fills one layer above the surface it sits on.\n\n```html\n<esp-info>\n Info defaults to intent=\"info\" and its informational surface treatment.\n</esp-info>\n```",
43263
43616
  "name": "EspalierInfo",
43264
43617
  "cssProperties": [
43265
43618
  {
@@ -43275,7 +43628,7 @@
43275
43628
  "name": "--esp-info-color-text"
43276
43629
  },
43277
43630
  {
43278
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
43631
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
43279
43632
  "name": "--esp-field-background",
43280
43633
  "inheritedFrom": {
43281
43634
  "name": "EspalierElementBase",
@@ -44123,6 +44476,17 @@
44123
44476
  "description": "If an info box is destroyable, it has a button that will fire the\n`esp-info-destroy` event when clicked.\n\n```html\n<esp-info destroyable>\n I am destroyable.\n</esp-info>\n```",
44124
44477
  "fieldName": "destroyable"
44125
44478
  },
44479
+ {
44480
+ "type": {
44481
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
44482
+ },
44483
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
44484
+ "name": "surface-layer",
44485
+ "inheritedFrom": {
44486
+ "name": "EspalierElementBase",
44487
+ "module": "dist/shared/esp-element-base.js"
44488
+ }
44489
+ },
44126
44490
  {
44127
44491
  "name": "scheme",
44128
44492
  "type": {
@@ -45893,6 +46257,17 @@
45893
46257
  "description": "Space-separated list of region codes that defines which\nphone localities are accepted for `input-type=\"tel\"`. The\nfirst locality whose format matches wins. When omitted,\ndefaults to a set of 10 common regions.\n\n```html\n<esp-input input-type=\"tel\" tel-localities=\"US CA GB\"></esp-input>\n```",
45894
46258
  "fieldName": "telLocalities"
45895
46259
  },
46260
+ {
46261
+ "type": {
46262
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
46263
+ },
46264
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
46265
+ "name": "surface-layer",
46266
+ "inheritedFrom": {
46267
+ "name": "EspalierElementBase",
46268
+ "module": "dist/shared/esp-element-base.js"
46269
+ }
46270
+ },
45896
46271
  {
45897
46272
  "name": "scheme",
45898
46273
  "type": {
@@ -45959,7 +46334,7 @@
45959
46334
  },
45960
46335
  "cssProperties": [
45961
46336
  {
45962
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
46337
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
45963
46338
  "name": "--esp-field-background",
45964
46339
  "inheritedFrom": {
45965
46340
  "name": "EspalierElementBase",
@@ -46158,7 +46533,7 @@
46158
46533
  "declarations": [
46159
46534
  {
46160
46535
  "kind": "class",
46161
- "description": "A full-screen lightbox for viewing high-res versions of\n[esp-image](/components/image) elements. Point the `for` attribute\nat a CSS selector for the container that holds the images. Clicking\nany image in that container opens the lightbox at that image.\n\nOn mobile, swipe left or right to navigate. The gallery wraps\naround — swiping past the last image goes back to the first.\n\nA subtle caret at the bottom of the screen reveals a drawer\noverlay containing the image caption and a `comments` slot.\n\nBoth halves of that panel are optional. `hide-comments` drops the\ncomment count and the `comments` slot; `hide-caption` drops the\nimage caption. With only the caption left, the bar shows it on one\nline and slides up only when the line is actually clipped — a\ncaption that already fits has no caret and no tab stop. With both\nhidden there is no bar, though navigation dots remain for a\nmulti-image gallery.\n\n```html\n<esp-lightbox for=\".gallery\" hide-comments></esp-lightbox>\n```\n\n```html\n<div class=\"gallery\">\n <esp-image caption=\"A mountain lake\" original-width=\"1200\" original-height=\"800\" low-res=\"https://picsum.photos/id/29/300/200\">\n <esp-image-option width=\"400\" url=\"https://picsum.photos/id/29/600/400\"></esp-image-option>\n <esp-image-option width=\"800\" url=\"https://picsum.photos/id/29/900/600\"></esp-image-option>\n <esp-image-option width=\"1024\" url=\"https://picsum.photos/id/29/1200/800\"></esp-image-option>\n </esp-image>\n <esp-image caption=\"A forest path\" original-width=\"1200\" original-height=\"800\" low-res=\"https://picsum.photos/id/15/300/200\">\n <esp-image-option width=\"400\" url=\"https://picsum.photos/id/15/600/400\"></esp-image-option>\n <esp-image-option width=\"800\" url=\"https://picsum.photos/id/15/900/600\"></esp-image-option>\n <esp-image-option width=\"1024\" url=\"https://picsum.photos/id/15/1200/800\"></esp-image-option>\n </esp-image>\n <esp-image caption=\"Ocean waves\" original-width=\"1200\" original-height=\"800\" low-res=\"https://picsum.photos/id/65/300/200\">\n <esp-image-option width=\"400\" url=\"https://picsum.photos/id/65/600/400\"></esp-image-option>\n <esp-image-option width=\"800\" url=\"https://picsum.photos/id/65/900/600\"></esp-image-option>\n <esp-image-option width=\"1024\" url=\"https://picsum.photos/id/65/1200/800\"></esp-image-option>\n </esp-image>\n</div>\n<esp-lightbox for=\".gallery\"></esp-lightbox>\n```",
46536
+ "description": "A full-screen lightbox for viewing high-res versions of\n[esp-image](/components/image) elements. Point the `for` attribute\nat a CSS selector for the container that holds the images. Clicking\nany image in that container opens the lightbox at that image.\n\nA field fills one layer above the surface it sits on.\nUse `surface-layer=\"1|2|3|4\"` to select the default painted layer.\n\nOn mobile, swipe left or right to navigate. The gallery wraps\naround — swiping past the last image goes back to the first.\n\nA subtle caret at the bottom of the screen reveals a drawer\noverlay containing the image caption and a `comments` slot.\n\nBoth halves of that panel are optional. `hide-comments` drops the\ncomment count and the `comments` slot; `hide-caption` drops the\nimage caption. With only the caption left, the bar shows it on one\nline and slides up only when the line is actually clipped — a\ncaption that already fits has no caret and no tab stop. With both\nhidden there is no bar, though navigation dots remain for a\nmulti-image gallery.\n\n```html\n<esp-lightbox for=\".gallery\" hide-comments></esp-lightbox>\n```\n\n```html\n<div class=\"gallery\">\n <esp-image caption=\"A mountain lake\" original-width=\"1200\" original-height=\"800\" low-res=\"https://picsum.photos/id/29/300/200\">\n <esp-image-option width=\"400\" url=\"https://picsum.photos/id/29/600/400\"></esp-image-option>\n <esp-image-option width=\"800\" url=\"https://picsum.photos/id/29/900/600\"></esp-image-option>\n <esp-image-option width=\"1024\" url=\"https://picsum.photos/id/29/1200/800\"></esp-image-option>\n </esp-image>\n <esp-image caption=\"A forest path\" original-width=\"1200\" original-height=\"800\" low-res=\"https://picsum.photos/id/15/300/200\">\n <esp-image-option width=\"400\" url=\"https://picsum.photos/id/15/600/400\"></esp-image-option>\n <esp-image-option width=\"800\" url=\"https://picsum.photos/id/15/900/600\"></esp-image-option>\n <esp-image-option width=\"1024\" url=\"https://picsum.photos/id/15/1200/800\"></esp-image-option>\n </esp-image>\n <esp-image caption=\"Ocean waves\" original-width=\"1200\" original-height=\"800\" low-res=\"https://picsum.photos/id/65/300/200\">\n <esp-image-option width=\"400\" url=\"https://picsum.photos/id/65/600/400\"></esp-image-option>\n <esp-image-option width=\"800\" url=\"https://picsum.photos/id/65/900/600\"></esp-image-option>\n <esp-image-option width=\"1024\" url=\"https://picsum.photos/id/65/1200/800\"></esp-image-option>\n </esp-image>\n</div>\n<esp-lightbox for=\".gallery\"></esp-lightbox>\n```",
46162
46537
  "name": "EspalierLightbox",
46163
46538
  "cssProperties": [
46164
46539
  {
@@ -46178,7 +46553,7 @@
46178
46553
  "name": "--esp-vellum-background-image-opacity"
46179
46554
  },
46180
46555
  {
46181
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
46556
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
46182
46557
  "name": "--esp-field-background",
46183
46558
  "inheritedFrom": {
46184
46559
  "name": "EspalierElementBase",
@@ -47993,6 +48368,17 @@
47993
48368
  "description": "Remove the image caption from the bar and the drawer.",
47994
48369
  "fieldName": "hideCaption"
47995
48370
  },
48371
+ {
48372
+ "type": {
48373
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
48374
+ },
48375
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
48376
+ "name": "surface-layer",
48377
+ "inheritedFrom": {
48378
+ "name": "EspalierElementBase",
48379
+ "module": "dist/shared/esp-element-base.js"
48380
+ }
48381
+ },
47996
48382
  {
47997
48383
  "name": "scheme",
47998
48384
  "type": {
@@ -48170,7 +48556,7 @@
48170
48556
  "name": "--esp-menu-item-font-family"
48171
48557
  },
48172
48558
  {
48173
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
48559
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
48174
48560
  "name": "--esp-field-background",
48175
48561
  "inheritedFrom": {
48176
48562
  "name": "EspalierElementBase",
@@ -49394,6 +49780,17 @@
49394
49780
  "description": "URL prefix for automatic expansion. If `location.pathname`\nstarts with this value, the group opens on connect.",
49395
49781
  "fieldName": "urlPrefix"
49396
49782
  },
49783
+ {
49784
+ "type": {
49785
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
49786
+ },
49787
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
49788
+ "name": "surface-layer",
49789
+ "inheritedFrom": {
49790
+ "name": "EspalierElementBase",
49791
+ "module": "dist/shared/esp-element-base.js"
49792
+ }
49793
+ },
49397
49794
  {
49398
49795
  "name": "scheme",
49399
49796
  "type": {
@@ -49565,7 +49962,7 @@
49565
49962
  "name": "--esp-menu-item-icon-size"
49566
49963
  },
49567
49964
  {
49568
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
49965
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
49569
49966
  "name": "--esp-field-background",
49570
49967
  "inheritedFrom": {
49571
49968
  "name": "EspalierElementBase",
@@ -50488,6 +50885,17 @@
50488
50885
  "description": "Optional icon name from the configured Espalier SVG sprite.",
50489
50886
  "fieldName": "icon"
50490
50887
  },
50888
+ {
50889
+ "type": {
50890
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
50891
+ },
50892
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
50893
+ "name": "surface-layer",
50894
+ "inheritedFrom": {
50895
+ "name": "EspalierElementBase",
50896
+ "module": "dist/shared/esp-element-base.js"
50897
+ }
50898
+ },
50491
50899
  {
50492
50900
  "name": "scheme",
50493
50901
  "type": {
@@ -50631,7 +51039,7 @@
50631
51039
  "name": "--esp-menu-top-offset"
50632
51040
  },
50633
51041
  {
50634
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
51042
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
50635
51043
  "name": "--esp-field-background",
50636
51044
  "inheritedFrom": {
50637
51045
  "name": "EspalierElementBase",
@@ -52472,6 +52880,17 @@
52472
52880
  "description": "Compatibility hook used by `esp-header` when inline navigation\nbecomes too wide. Prefer `mode`/`overflow` in application code.",
52473
52881
  "fieldName": "sliding"
52474
52882
  },
52883
+ {
52884
+ "type": {
52885
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
52886
+ },
52887
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
52888
+ "name": "surface-layer",
52889
+ "inheritedFrom": {
52890
+ "name": "EspalierElementBase",
52891
+ "module": "dist/shared/esp-element-base.js"
52892
+ }
52893
+ },
52475
52894
  {
52476
52895
  "name": "scheme",
52477
52896
  "type": {
@@ -53602,7 +54021,7 @@
53602
54021
  "type": {
53603
54022
  "text": "array"
53604
54023
  },
53605
- "default": "[ ...EspalierElementBase.styles, css` :host { --_esp-page-resolved-max-width: var(--esp-page-main-max-width, 1536px); --_esp-page-main-min-width: var(--esp-page-main-min-width, 30rem); --_esp-page-preview-resolved-min-width: var(--esp-page-preview-min-width, 22.5rem); --_esp-page-preview-resolved-max-width: var(--esp-page-preview-max-width, 48rem); --_esp-page-flyout-resolved-min-width: var(--esp-page-flyout-min-width, 20rem); --_esp-page-flyout-resolved-max-width: var(--esp-page-flyout-max-width, 30rem); --_esp-page-preview-used-width: 0px; --_esp-page-flyout-used-width: 0px; --_esp-page-workspace-track: minmax( calc(var(--_esp-page-flyout-min) + var(--_esp-page-preview-min)), calc(var(--_esp-page-preview-used-width) + var(--_esp-page-flyout-used-width)) ); /* The cap sizes the main content *well*: the main track maxes at the cap and the surplus beyond the surface becomes canvas gutters. (Contract preserved from v2.1.0.) */ --_esp-page-main-track: minmax(0, var(--_esp-page-resolved-max-width)); /* Canvas gutter weights (physical left/right in LTR). The flexible gutters claim only the space left once the main track reaches its cap, so they appear exactly when the surface exceeds the cap — at any cap, with no viewport breakpoint (ADR-011). The start default keeps no leading gutter and lets all surplus collect trailing. Empty asides collapse to zero because the header and footer regions are inline-size-contained and no longer leak their min-content into the aside tracks (see the .esp-page-top / .esp-page-footer rules). The fr weights are written as 0fr/1fr (never bare 0) so the track list stays pairwise-interpolable and the flyout open/close can animate via grid track interpolation where supported. */ --_esp-page-gutter-left: 0fr; --_esp-page-gutter-right: 1fr; /* Help and preview share an exact auxiliary workspace track before the outer trailing gutter. Opening help raises that track's safe minimum; the allocator supplies its concrete used width. */ --_esp-page-flyout-min: 0px; /* Preview starts at the surface edge before help. Reclaiming raises this minimum so the auxiliary track reserves both pane floors while the main track retains its own floor. */ --_esp-page-preview-min: 0px; /* A raised surface edge's drop shadow, cast in the trailing (inline-end) direction. The surface mirrors it onto both inline edges; a match-surface flyout casts only this trailing edge, so it lifts off the canvas while staying attached at its leading perforation (no shadow bleeding over the content it flies out from). */ --_esp-page-surface-edge-shadow: 0.75rem 0 1.5rem -0.75rem var(--esp-color-shadow); /* Resolve the surface shadow/border once (public override plus the built-in default) so the value is readable downstream — the surface layer and the match-surface flyout contract both consume it, and it stays a single source of truth. */ --_esp-page-surface-shadow: var( --esp-page-surface-shadow, -0.75rem 0 1.5rem -0.75rem var(--esp-color-shadow), var(--_esp-page-surface-edge-shadow) ); --_esp-page-surface-border: var(--esp-page-surface-border, none); --_esp-page-fixed-header-offset: var( --esp-page-fixed-header-offset, var(--esp-header-height, calc(4.5 * var(--esp-size-small))) ); display: block; } :host([kind=\"narrow\"]) { --_esp-page-resolved-max-width: var(--esp-page-main-max-width, 768px); } :host([align=\"center\"]) { --_esp-page-gutter-left: 1fr; --_esp-page-gutter-right: 1fr; } :host([align=\"end\"]) { --_esp-page-gutter-left: 1fr; --_esp-page-gutter-right: 0fr; } /* Full is unbounded only while the consumer has not supplied the explicit main maximum. The internal marker is synchronized from the computed public token before workspace allocation, allowing a finite full-kind authoring surface to retain normal alignment gutters. */ :host([kind=\"full\"]) { --_esp-page-resolved-max-width: var(--esp-page-main-max-width, none); } :host([kind=\"full\"]:not([data-main-max-configured])) { --_esp-page-main-track: 1fr; --_esp-page-gutter-left: 0fr; --_esp-page-gutter-right: 0fr; } /* Site: the marketing shell (ESP0174). The main region is always unbounded — slotted esp-section bands own their centered wells — so this is the unconfigured-full geometry, unconditionally, and align's gutter weights do not apply. Declared after the align rules so it wins on order. */ :host([kind=\"site\"]) { --_esp-page-resolved-max-width: none; --_esp-page-main-track: 1fr; --_esp-page-gutter-left: 0fr; --_esp-page-gutter-right: 0fr; } /* Flyout placement ladder (ESP0111). Preview and help occupy an exact auxiliary track between the main surface and the outer gutter: 1. spare canvas is converted into the allocated auxiliary track while main remains at its natural maximum 2. help and preview contract to their floors 3. an eligible linked navigation rail yields before the main track narrows toward its minimum 4. if the complete main + preview + help minimums do not fit, JS requests the flyout's existing modal overlay presentation 5. mobile → the media query below releases the minimum and esp-flyout itself becomes a fixed overlay drawer. The flyout-open host attribute is maintained by syncFlyoutState (Blink cannot match :has() inside :host(), so the state is mirrored instead). Declared after the align/kind rules so the open state wins. */ :host([flyout-open]) { --_esp-page-flyout-min: var(--_esp-page-flyout-resolved-min-width); } :host([preview-reclaiming]) { --_esp-page-preview-min: var(--_esp-page-preview-resolved-min-width); } /* Configured defaults and retained drag/keyboard choices participate in the same grid minima as automatic allocation. This lets a target that must reclaim main do so instead of overflowing the gutter, while the allocator still clamps private used widths before they reach Grid. */ :host([flyout-open][data-workspace-targeted]) { --_esp-page-flyout-min: var(--_esp-page-flyout-used-width); } :host([preview-reclaiming][data-workspace-targeted]) { --_esp-page-preview-min: var(--_esp-page-preview-used-width); } :host([flyout-open]), :host([preview-reclaiming]) { --_esp-page-main-track: minmax( var(--_esp-page-main-min-width), var(--_esp-page-resolved-max-width) ); } :host([kind=\"full\"]:not([data-main-max-configured])[flyout-open]), :host([kind=\"full\"]:not([data-main-max-configured])[preview-reclaiming]), :host([kind=\"site\"][flyout-open]), :host([kind=\"site\"][preview-reclaiming]) { --_esp-page-main-track: minmax(var(--_esp-page-main-min-width), 1fr); } /* Deliberate ADR-011 exception: like the esp-menu drawer (same 50em threshold), the flyout's overlay rung is a genuinely viewport-level decision — below it the flyout leaves the grid for a fixed drawer, so the gutter must release the minimum it was reserving. */ @media ${unsafeCSS(COMPACT_VIEWPORT_MEDIA_QUERY)} { :host([flyout-open]) { --_esp-page-flyout-min: 0px; } :host([preview-reclaiming]) { --_esp-page-preview-min: 0px; } :host([flyout-open]), :host([preview-reclaiming]) { --_esp-page-main-track: minmax(0, var(--_esp-page-resolved-max-width)); } :host([kind=\"full\"]:not([data-main-max-configured])[flyout-open]), :host([kind=\"full\"]:not([data-main-max-configured])[preview-reclaiming]), :host([kind=\"site\"][flyout-open]), :host([kind=\"site\"][preview-reclaiming]) { --_esp-page-main-track: 1fr; } .esp-page > .esp-page-preview-space, .esp-page > .esp-page-workspace-resize-handles { display: none; } } /* A forced mode=\"overlay\" flyout is a fixed drawer at any viewport width, so its aside needs the same above-the-header hoist the sub-50em media rule gives the auto-mode drawer — without this the desktop drawer paints beneath the header (aside z 5 vs header z 20). Keyed off the flyout-overlay-open state attribute maintained by syncFlyoutState. */ :host([flyout-overlay-open]) .esp-page > div.esp-page-flyout { z-index: var(--esp-flyout-z-index, 3000); } /* A nested page's fixed drawer still participates in this page's main stacking context. Raise that complete context while the descendant modal is open so the drawer and vellum paint above this page's header instead of trapping their close control beneath it. */ :host([descendant-flyout-overlay-open]) .esp-page > div.esp-page-main { z-index: var(--esp-flyout-z-index, 3000); overflow: visible; } /* Trigger-anchored panels belong to the same document flow as their initiator. BiDirectionalStickyController can keep updating the wrapper's inline top, but position: static deliberately makes it inert until the anchored panel closes. */ :host([flyout-anchored]:not([flyout-full-height])) .esp-page > div.esp-page-flyout > .sticky-wrapper { position: static; } /* Full-document help owns the visible block-size rather than following one field. Span header through footer so the page footer cannot push a viewport-height panel out of view at the document boundary; the sticky controller pins this presentation to the viewport top. */ :host([flyout-full-height]) .esp-page > div.esp-page-flyout { grid-row: top-start / footer-end; z-index: calc(var(--esp-page-header-z-index, 20) + 2); } /* Anchored help connects back to its trigger across a visible preview. Raise only the sized help wrapper above the preview surface; the bridge itself is non-interactive inside esp-flyout. */ :host([flyout-anchored][preview-visible]) .esp-page > div.esp-page-flyout { z-index: calc(var(--esp-page-header-z-index, 20) + 2); } slot[name=\"flyout\"]::slotted(esp-flyout) { --_esp-flyout-used-width: var(--_esp-page-flyout-used-width); --_esp-flyout-preview-bridge-width: 0px; --_esp-flyout-preview-anchor-display: none; } :host([preview-visible]) slot[name=\"flyout\"]::slotted(esp-flyout) { --_esp-flyout-preview-bridge-width: var(--_esp-page-preview-used-width); --_esp-flyout-preview-anchor-display: block; } /* Flyout surface-match contract. A slotted esp-flyout with match-surface lifts its trailing edge with the surface's edge shadow, so the panel reads as a raised peer of the content surface rather than the shadowless tear-off. The leading edge keeps its perforation (the border is not overridden) and casts no shadow, so it stays attached at the tear line instead of floating over the content it flies out from. Opt-in and page-scoped: a standalone flyout has no surface to match and is unaffected. */ slot[name=\"flyout\"]::slotted(esp-flyout[match-surface]) { --esp-flyout-shadow: var(--_esp-page-surface-edge-shadow); } /* Header alignment contract. Published on the slotted esp-header (not :host) so it never shadows an inherited or author override and only the header-slot header adopts it — see the host-token-defaults architecture rule and ADR-005. max-width is the resolved cap; lead is the fraction of surplus placed before the content (0 = start, .5 = center, 1 = end). */ slot[name=\"header\"]::slotted(esp-header) { --esp-header-content-max-width: var(--_esp-page-resolved-max-width); --esp-header-content-lead: 0; } :host([align=\"center\"]) slot[name=\"header\"]::slotted(esp-header) { --esp-header-content-lead: 0.5; } :host([align=\"end\"]) slot[name=\"header\"]::slotted(esp-header) { --esp-header-content-lead: 1; } /* full has no cap; publish 100% (not none) so the header contract calc resolves to zero surplus and leaves the header full-bleed. */ :host([kind=\"full\"]:not([data-main-max-configured])) slot[name=\"header\"]::slotted(esp-header) { --esp-header-content-max-width: 100%; } /* Footer alignment mirrors the header behavior with footer-owned contract names. The full-bleed footer paints its own background; only its internal content adopts the page's cap and lead fraction. */ slot[name=\"footer\"]::slotted(esp-footer) { --esp-footer-content-max-width: var(--_esp-page-resolved-max-width); --esp-footer-content-lead: 0; } :host([align=\"center\"]) slot[name=\"footer\"]::slotted(esp-footer) { --esp-footer-content-lead: 0.5; } :host([align=\"end\"]) slot[name=\"footer\"]::slotted(esp-footer) { --esp-footer-content-lead: 1; } :host([kind=\"full\"]:not([data-main-max-configured])) slot[name=\"footer\"]::slotted(esp-footer) { --esp-footer-content-max-width: 100%; } /* Site: bar content aligns with the section wells, not a main cap. One public token governs the header, the footer, and every esp-section well, and the well is centered — declared after the align lead rules so the centered site column wins on order. */ :host([kind=\"site\"]) slot[name=\"header\"]::slotted(esp-header) { --esp-header-content-max-width: var(--esp-page-well-max-width, 72rem); --esp-header-content-lead: 0.5; } :host([kind=\"site\"]) slot[name=\"footer\"]::slotted(esp-footer) { --esp-footer-content-max-width: var(--esp-page-well-max-width, 72rem); --esp-footer-content-lead: 0.5; } /* Contained shell: the header and footer ride the surface instead of the full viewport, so the whole page reads as one band on the canvas. Only their column placement changes — the full-height canvas and surface layers already do the rest. */ :host([contained]) .esp-page > div.esp-page-top, :host([contained]) .esp-page > div.esp-page-footer { grid-column: surface; } /* On the surface the bar already spans the content width, so the header fills it (no content padding) and drops its own shadow — the continuous surface edge shadow does the framing. */ :host([contained]) slot[name=\"header\"]::slotted(esp-header) { --esp-header-shadow: none; } /* The 100% contract belongs to the capped kinds, where the surface column IS the content width. A contained site keeps the shared well column (ESP0174) — sections and chrome must not split columns just because the shell is contained. */ :host([contained]:not([kind=\"site\"])) slot[name=\"header\"]::slotted(esp-header) { --esp-header-content-max-width: 100%; } :host([contained]:not([kind=\"site\"])) slot[name=\"footer\"]::slotted(esp-footer) { --esp-footer-content-max-width: 100%; } :host([kind=\"narrow\"]) .esp-page > div.esp-page-main { /* The golden reading measure, from the shared token (ESP0174); 66ch remains the fallback and the default emission. */ > ::slotted(*) { max-inline-size: var(--esp-measure, 66ch); } } #dialog-drop-zone { z-index: 4000; position: absolute; } :host([preview-visible]) .esp-page > div.esp-page-preview-space > .esp-page-preview { display: block; } :host([workspace-resizable][preview-visible]:not([flyout-overlay-open])) .esp-page > div.esp-page-workspace-resize-handles > .esp-page-main-preview-resize-handle, :host([workspace-resizable][flyout-open]:not([flyout-overlay-open])) .esp-page > div.esp-page-workspace-resize-handles > .esp-page-preview-flyout-resize-handle { display: block; } :host([workspace-resizable]:not([data-workspace-resize-ready])) .esp-page > .esp-page-workspace-resize-handles { display: none; } :host([workspace-resizable][data-workspace-trailing-edge-exposed]:not([flyout-overlay-open])) .esp-page > div.esp-page-workspace-resize-handles > .esp-page-workspace-end-resize-handle, :host([workspace-resizable][data-workspace-position-held]:not([flyout-overlay-open])) .esp-page > div.esp-page-workspace-resize-handles > .esp-page-workspace-end-resize-handle { display: block; } /* Pointer-owned resizing controls the logical leading gutter. It stays fixed for zero-sum pane trades and yields as an outward drag consumes centered outside canvas, keeping the active separator under the pointer in both cases. On release JS moves this pixel track to the aligned target; the normal grid transition supplies the glide before the intrinsic fr gutter contract resumes. */ :host([data-workspace-position-held]) { --_esp-page-gutter-left: var(--_esp-page-resize-leading-gutter-width, 0px); --_esp-page-gutter-right: 1fr; } :host([data-workspace-resizing]) { cursor: col-resize; user-select: none; } /* The grid-track glide that makes programmatic open/close feel animated reads as cursor lag while a pointer owns the seam, and a mid-glide grid feeds stale geometry into the clamp ranges of the next keyboard step. Track changes must be immediate for the whole resize session, pointer or keyboard. */ :host([data-workspace-resizing]) .esp-page, :host([data-workspace-keyboard-adjusting]) .esp-page { transition: none; } /* The linked menu switches its rail to a fixed drawer, but light-DOM menu content can still contribute an intrinsic width to the page's sidebar track. Collapse the owning aside only after the validated header/menu handshake is active, so unmatched navigation remains visible and reachable. */ :host([data-preview-navigation-collapsed]) .esp-page > aside.esp-page-left { inline-size: 0; min-inline-size: 0; } :host([data-preview-measuring]) .esp-page { transition: none; } /* New preview layouts are validated against visible geometry before presentation. Keep that candidate surface non-visible so a rejected measurement can never flash on screen. */ :host([data-preview-validating]) .esp-page-preview { visibility: hidden; } .esp-page { /* dvh keeps the shell within the visible mobile viewport; 100vh is the fallback for browsers without dvh (dvh === vh on desktop). */ min-height: 100vh; min-height: 100dvh; display: grid; grid-template-columns: [full-start canvas-left-start] var(--_esp-page-gutter-left) [canvas-left-end surface-start left-start] min-content [left-end main-start] var(--_esp-page-main-track) [main-end right-start] min-content [right-end surface-end canvas-right-start flyout-start] var(--_esp-page-workspace-track) [flyout-end] var(--_esp-page-gutter-right) [canvas-right-end full-end]; /* Animate the flyout claiming/releasing its track (and the gutter rebalance) via grid track interpolation where supported; elsewhere the modes still apply, just without the glide. Keep this 0.25s duration synchronized with WORKSPACE_RECENTER_DURATION in workspace-resize-session.ts. */ transition: grid-template-columns 0.25s ease; grid-template-rows: [top-start] min-content [top-end content-start] 1fr [content-end footer-start] min-content [footer-end]; overflow-x: clip; background: var(--esp-page-background, var(--esp-color-background)); line-height: 1.5; font-family: var( --_esp-font-body-effective, var(--esp-font-body, var(--_esp-font-body-fallback)) ); font-size: var(--esp-type-normal); color: var(--esp-color-text); position: relative; &:before { content: \" \"; display: block; position: absolute; left: 0; top: 0; right: 0; bottom: 0; opacity: var(--esp-page-background-image-opacity, 1); background-image: var(--esp-page-background-image, none); z-index: 1; } > div.esp-page-top { z-index: var(--esp-page-header-z-index, 20); grid-column: full; grid-row: top; position: relative; /* The header spans every column, so without inline-size containment its content's min-content would be distributed into the aside tracks and inflate them below the cap (the asides would never collapse). Contain it like the main well so it takes its width from the grid, not the grid from it. */ contain: inline-size; } /* Sidebar containers — act as the \"track\" for sticky positioning */ aside.esp-page-left, aside.esp-page-right { z-index: 5; grid-row: content; position: relative; /* No explicit height — grid stretch fills the 1fr row, and the aside's bottom edge constrains sticky children. */ } > aside.esp-page-left { grid-column: left; } > div.esp-page-main { z-index: 5; grid-column: main; grid-row: content; position: relative; /* Clip oversized content without becoming a scroll container. A hidden-overflow wrapper traps descendant view timelines (and sticky positioning) on this non-scrolling main well. */ overflow: clip; /* The main well's own background. Defaults to transparent, so the well shows the page background exactly as before; set it to give the content well a distinct card color, or leave it transparent (with --esp-page-surface-shadow: none) for content that floats directly on the page/canvas. */ background: var(--esp-page-main-background, transparent); /* The cap now lives in the main track (minmax(0, cap)); the element fills its column. contain: inline-size still holds because the track gives a definite inline size. */ contain: inline-size; } > aside.esp-page-right { grid-column: right; } /* A neutral div, unlike the left/right asides: the slotted esp-flyout is itself the complementary landmark, and an aside wrapper would nest a second, empty landmark around it. */ > div.esp-page-flyout { /* Sits after preview inside the exact auxiliary track and sizes to the allocated help width. Outer surplus remains in the following gutter rather than under an empty stretched wrapper. */ grid-column: flyout; justify-self: start; grid-row: content; z-index: 5; position: relative; inline-size: var(--_esp-page-flyout-used-width); min-inline-size: 0; margin-inline-start: var(--_esp-page-preview-used-width); /* In the drawer rung the fixed esp-flyout paints inside this wrapper's stacking context, so the wrapper itself must rise above the header (z 20) while staying below the dialog drop zone (4000). Uses the same token as the drawer — set it on the page (or higher) to move both together. */ @media ${unsafeCSS(COMPACT_VIEWPORT_MEDIA_QUERY)} { z-index: var(--esp-flyout-z-index, 3000); } } /* Preview begins at the surface edge and help is offset after its resolved width, producing the logical order main → preview → help inside their exact shared auxiliary track. */ > div.esp-page-preview-space { grid-column: flyout; grid-row: content; z-index: 4; position: relative; min-inline-size: 0; > .esp-page-preview { display: none; box-sizing: border-box; inline-size: var(--_esp-page-preview-used-width); position: relative; background: var( --esp-page-preview-background, var(--esp-page-main-background, var(--esp-page-background, var(--esp-color-background))) ); border-inline-start: var(--esp-page-preview-border, 1px dotted var(--esp-color-border)); box-shadow: var(--esp-page-preview-shadow, var(--_esp-page-surface-edge-shadow)); } } /* A transparent interaction layer keeps the complete hit target above main, preview, and anchored help without raising either painted surface (which would hide the help bridge). */ > div.esp-page-workspace-resize-handles { grid-column: flyout; grid-row: top-start / footer-end; z-index: calc(var(--esp-page-header-z-index, 20) + 3); position: relative; min-inline-size: 0; pointer-events: none; } > div.esp-page-workspace-resize-handles > .esp-page-workspace-resize-handle { display: none; box-sizing: border-box; position: absolute; inset-block: 0; z-index: 3; inline-size: var(--esp-page-resize-handle-hit-size, 2.75rem); margin-inline-start: calc(-0.5 * var(--esp-page-resize-handle-hit-size, 2.75rem)); border: 0; outline: 0; background: transparent; cursor: col-resize; pointer-events: auto; touch-action: none; /* The target spans the seam so it remains reachable during document scroll. Focus draws a dashed line along the seam itself — the visible height of the dotted tear-off edge, capped at one viewport — kept in view by sticky positioning while the page scrolls. */ &::after { content: \"\"; display: block; position: sticky; inset-block-start: 0; box-sizing: border-box; inline-size: 0; margin-inline: auto; block-size: 100dvh; max-block-size: 100%; pointer-events: none; } &:focus-visible::after { border-inline-start: var( --esp-page-resize-focus-outline, 2px dashed var(--esp-color-link) ); /* Glow in the line's own color so the focused seam reads as one deliberate highlight rather than a neutral drop shadow. */ box-shadow: var(--esp-page-resize-focus-shadow, 0 0 0.75rem var(--esp-color-link)); } } > div.esp-page-workspace-resize-handles > .esp-page-main-preview-resize-handle { inset-inline-start: 0; } > div.esp-page-workspace-resize-handles > .esp-page-preview-flyout-resize-handle { inset-inline-start: var(--_esp-page-preview-used-width); } > div.esp-page-workspace-resize-handles > .esp-page-workspace-end-resize-handle { inset-inline-start: calc( var(--_esp-page-preview-used-width) + var(--_esp-page-flyout-used-width) ); } /* Keep token-sized probes outside the width-constrained gutter. If they lived inside a zero-width preview space, layout would clamp their rectangles to zero before JS could decide whether the reclaim rung should be activated. */ > .esp-page-preview-min-width-probe, > .esp-page-preview-default-width-probe, > .esp-page-preview-max-width-probe, > .esp-page-flyout-min-width-probe, > .esp-page-flyout-default-width-probe, > .esp-page-flyout-max-width-probe, > .esp-page-main-min-width-probe, > .esp-page-main-configured-max-width-probe, > .esp-page-main-max-width-probe, > .esp-page-resize-step-probe, > .esp-page-resize-large-step-probe { position: absolute; block-size: 0; visibility: hidden; pointer-events: none; contain: strict; } > .esp-page-preview-min-width-probe { inline-size: var(--_esp-page-preview-resolved-min-width); } > .esp-page-preview-max-width-probe { inline-size: var(--_esp-page-preview-resolved-max-width); } > .esp-page-preview-default-width-probe { inline-size: var(--esp-page-preview-default-width, 0px); } > .esp-page-flyout-min-width-probe { inline-size: var(--_esp-page-flyout-resolved-min-width); } > .esp-page-flyout-max-width-probe { inline-size: var(--_esp-page-flyout-resolved-max-width); } > .esp-page-flyout-default-width-probe { inline-size: var(--esp-page-flyout-default-width, 0px); } > .esp-page-main-min-width-probe { inline-size: var(--_esp-page-main-min-width); } > .esp-page-main-configured-max-width-probe { inline-size: var(--esp-page-main-max-width, 0px); } > .esp-page-main-max-width-probe { inline-size: var(--_esp-page-resolved-max-width); } > .esp-page-resize-step-probe { inline-size: var(--esp-page-resize-step, 1rem); } > .esp-page-resize-large-step-probe { inline-size: var(--esp-page-resize-large-step, 4rem); } /* Canvas: the outer gutter regions promoted to a styleable layer. They only have width once the viewport exceeds the cap, so with the transparent defaults they are invisible and narrow pages are unchanged. Painted below the content (z-index 5) and below the surface shadow. */ > .esp-page-canvas { /* Full height: the canvas runs top-to-bottom. In the default full-bleed chrome the header/footer span the gutters and cover it there, so only the content-band gutters show; in contained mode the header/footer sit on the surface and the canvas shows full height beside them. */ grid-row: top-start / footer-end; z-index: 1; position: relative; pointer-events: none; background-color: var(--esp-page-canvas-background, transparent); /* Image layer mirrors the page background-image knobs: a separate layer so its opacity does not fade the color. */ &::before { content: \"\"; position: absolute; inset: 0; background-image: var(--esp-page-canvas-background-image, none); opacity: var(--esp-page-canvas-background-image-opacity, 1); } } > .esp-page-canvas--left { grid-column: canvas-left; } > .esp-page-canvas--right { /* The canvas spans both the auxiliary workspace and the independent outer gutter beneath it. */ grid-column: canvas-right; } /* Surface: a transparent backdrop spanning the surface cluster (left aside + main + right aside). It carries the surface edge shadow (on by default) and an optional hairline border. The shadow casts outward into the gutters; when there are no gutters the surface fills the viewport and overflow-x: clip on .esp-page trims the shadow away — so it is visible only above the cap with no container query needed. */ > .esp-page-surface { grid-column: surface; /* Full height so the edge shadow is one continuous line from the header through the content to the footer — no fade at the region joins. */ grid-row: top-start / footer-end; z-index: 2; pointer-events: none; box-shadow: var(--_esp-page-surface-shadow); border-inline: var(--_esp-page-surface-border); } /* Internal wrapper manipulated by BiDirectionalStickyController */ .sticky-wrapper { will-change: top; display: block; width: 100%; position: sticky; top: 0; } > div.esp-page-footer { grid-column: full; grid-row: footer; z-index: 10; background: var(--esp-page-background, var(--esp-color-background)); /* Spans every column; contain it so its content does not inflate the aside tracks (see the header note above). */ contain: inline-size; } &.sticky-header { slot[name=\"header\"]::slotted(esp-header), slot[name=\"sidebar\"]::slotted(esp-menu), slot[name=\"right\"]::slotted(esp-menu) { --esp-menu-top-offset: var(--_esp-page-fixed-header-offset); } > div.esp-page-top { position: sticky; top: var(--esp-page-sticky-header-top, 0); } } &.fixed-header { slot[name=\"header\"]::slotted(esp-header), slot[name=\"sidebar\"]::slotted(esp-menu), slot[name=\"right\"]::slotted(esp-menu) { --esp-menu-top-offset: var(--_esp-page-fixed-header-offset); } > div.esp-page-top { position: fixed; top: 0; left: 0; right: 0; } > div.esp-page-main, > aside.esp-page-left, > aside.esp-page-right, > div.esp-page-flyout, > .esp-page-surface, > .esp-page-canvas { margin-top: var(--_esp-page-fixed-header-offset); } } /* Inherit fonts for inputs and buttons */ input, button, textarea, select { font-family: inherit; font-size: inherit; } /* Make sure text areas without a rows attribute are not tiny */ textarea:not([rows]) { min-height: 10em; } /* Anything that has been anchored to should have extra scroll margin */ :target { scroll-margin-block: 5ex; } } .esp-page:has(esp-dialog[is-open=\"true\"]) { overflow: hidden; } /* Main and preview are additive surfaces. Once preview owns the trailing edge, move elevation there so no shadow appears at the internal join; the surface resumes its normal frame when preview is hidden. */ :host([preview-visible]) .esp-page > .esp-page-surface { box-shadow: none; } /* The preview's outer grid item and landmark span the complete page so the sticky wrapper's containing block has enough travel to remain pinned throughout a tall document. Its dynamic-viewport block size and vertical overflow keep its scroll position independent from the main document. It paints above full-bleed header chrome only in the preview's negotiated gutter track; overlay flyouts still take precedence by hiding preview presentation. */ .esp-page > div.esp-page-preview-space { grid-row: top-start / footer-end; z-index: calc(var(--esp-page-header-z-index, 20) + 1); pointer-events: none; > .esp-page-preview { position: absolute; inset-block: 0; inset-inline-start: 0; pointer-events: auto; > .sticky-wrapper { block-size: 100vh; block-size: 100dvh; overflow-y: auto; /* Scroll preview first, then hand wheel/trackpad motion back to the document at either boundary (or when no overflow exists). */ overscroll-behavior-y: auto; } } } @media (prefers-reduced-motion: reduce) { .esp-page { transition: none; } } `, ]"
54024
+ "default": "[ ...EspalierElementBase.styles, css` :host { --_esp-surface-layer: initial; --_esp-page-resolved-max-width: var(--esp-page-main-max-width, 1536px); --_esp-page-main-min-width: var(--esp-page-main-min-width, 30rem); --_esp-page-preview-resolved-min-width: var(--esp-page-preview-min-width, 22.5rem); --_esp-page-preview-resolved-max-width: var(--esp-page-preview-max-width, 48rem); --_esp-page-flyout-resolved-min-width: var(--esp-page-flyout-min-width, 20rem); --_esp-page-flyout-resolved-max-width: var(--esp-page-flyout-max-width, 30rem); --_esp-page-preview-used-width: 0px; --_esp-page-flyout-used-width: 0px; --_esp-page-workspace-track: minmax( calc(var(--_esp-page-flyout-min) + var(--_esp-page-preview-min)), calc(var(--_esp-page-preview-used-width) + var(--_esp-page-flyout-used-width)) ); /* The cap sizes the main content *well*: the main track maxes at the cap and the surplus beyond the surface becomes canvas gutters. (Contract preserved from v2.1.0.) */ --_esp-page-main-track: minmax(0, var(--_esp-page-resolved-max-width)); /* Canvas gutter weights (physical left/right in LTR). The flexible gutters claim only the space left once the main track reaches its cap, so they appear exactly when the surface exceeds the cap — at any cap, with no viewport breakpoint (ADR-011). The start default keeps no leading gutter and lets all surplus collect trailing. Empty asides collapse to zero because the header and footer regions are inline-size-contained and no longer leak their min-content into the aside tracks (see the .esp-page-top / .esp-page-footer rules). The fr weights are written as 0fr/1fr (never bare 0) so the track list stays pairwise-interpolable and the flyout open/close can animate via grid track interpolation where supported. */ --_esp-page-gutter-left: 0fr; --_esp-page-gutter-right: 1fr; /* Help and preview share an exact auxiliary workspace track before the outer trailing gutter. Opening help raises that track's safe minimum; the allocator supplies its concrete used width. */ --_esp-page-flyout-min: 0px; /* Preview starts at the surface edge before help. Reclaiming raises this minimum so the auxiliary track reserves both pane floors while the main track retains its own floor. */ --_esp-page-preview-min: 0px; /* A raised surface edge's drop shadow, cast in the trailing (inline-end) direction. The surface mirrors it onto both inline edges; a match-surface flyout casts only this trailing edge, so it lifts off the canvas while staying attached at its leading perforation (no shadow bleeding over the content it flies out from). */ --_esp-page-surface-edge-shadow: 0.75rem 0 1.5rem -0.75rem var(--esp-color-shadow); /* Resolve the surface shadow/border once (public override plus the built-in default) so the value is readable downstream — the surface layer and the match-surface flyout contract both consume it, and it stays a single source of truth. */ --_esp-page-surface-shadow: var( --esp-page-surface-shadow, -0.75rem 0 1.5rem -0.75rem var(--esp-color-shadow), var(--_esp-page-surface-edge-shadow) ); --_esp-page-surface-border: var(--esp-page-surface-border, none); --_esp-page-fixed-header-offset: var( --esp-page-fixed-header-offset, var(--esp-header-height, calc(4.5 * var(--esp-size-small))) ); display: block; } :host([kind=\"narrow\"]) { --_esp-page-resolved-max-width: var(--esp-page-main-max-width, 768px); } :host([align=\"center\"]) { --_esp-page-gutter-left: 1fr; --_esp-page-gutter-right: 1fr; } :host([align=\"end\"]) { --_esp-page-gutter-left: 1fr; --_esp-page-gutter-right: 0fr; } /* Full is unbounded only while the consumer has not supplied the explicit main maximum. The internal marker is synchronized from the computed public token before workspace allocation, allowing a finite full-kind authoring surface to retain normal alignment gutters. */ :host([kind=\"full\"]) { --_esp-page-resolved-max-width: var(--esp-page-main-max-width, none); } :host([kind=\"full\"]:not([data-main-max-configured])) { --_esp-page-main-track: 1fr; --_esp-page-gutter-left: 0fr; --_esp-page-gutter-right: 0fr; } /* Site: the marketing shell (ESP0174). The main region is always unbounded — slotted esp-section bands own their centered wells — so this is the unconfigured-full geometry, unconditionally, and align's gutter weights do not apply. Declared after the align rules so it wins on order. */ :host([kind=\"site\"]) { --_esp-page-resolved-max-width: none; --_esp-page-main-track: 1fr; --_esp-page-gutter-left: 0fr; --_esp-page-gutter-right: 0fr; } /* Flyout placement ladder (ESP0111). Preview and help occupy an exact auxiliary track between the main surface and the outer gutter: 1. spare canvas is converted into the allocated auxiliary track while main remains at its natural maximum 2. help and preview contract to their floors 3. an eligible linked navigation rail yields before the main track narrows toward its minimum 4. if the complete main + preview + help minimums do not fit, JS requests the flyout's existing modal overlay presentation 5. mobile → the media query below releases the minimum and esp-flyout itself becomes a fixed overlay drawer. The flyout-open host attribute is maintained by syncFlyoutState (Blink cannot match :has() inside :host(), so the state is mirrored instead). Declared after the align/kind rules so the open state wins. */ :host([flyout-open]) { --_esp-page-flyout-min: var(--_esp-page-flyout-resolved-min-width); } :host([preview-reclaiming]) { --_esp-page-preview-min: var(--_esp-page-preview-resolved-min-width); } /* Configured defaults and retained drag/keyboard choices participate in the same grid minima as automatic allocation. This lets a target that must reclaim main do so instead of overflowing the gutter, while the allocator still clamps private used widths before they reach Grid. */ :host([flyout-open][data-workspace-targeted]) { --_esp-page-flyout-min: var(--_esp-page-flyout-used-width); } :host([preview-reclaiming][data-workspace-targeted]) { --_esp-page-preview-min: var(--_esp-page-preview-used-width); } :host([flyout-open]), :host([preview-reclaiming]) { --_esp-page-main-track: minmax( var(--_esp-page-main-min-width), var(--_esp-page-resolved-max-width) ); } :host([kind=\"full\"]:not([data-main-max-configured])[flyout-open]), :host([kind=\"full\"]:not([data-main-max-configured])[preview-reclaiming]), :host([kind=\"site\"][flyout-open]), :host([kind=\"site\"][preview-reclaiming]) { --_esp-page-main-track: minmax(var(--_esp-page-main-min-width), 1fr); } /* Deliberate ADR-011 exception: like the esp-menu drawer (same 50em threshold), the flyout's overlay rung is a genuinely viewport-level decision — below it the flyout leaves the grid for a fixed drawer, so the gutter must release the minimum it was reserving. */ @media ${unsafeCSS(COMPACT_VIEWPORT_MEDIA_QUERY)} { :host([flyout-open]) { --_esp-page-flyout-min: 0px; } :host([preview-reclaiming]) { --_esp-page-preview-min: 0px; } :host([flyout-open]), :host([preview-reclaiming]) { --_esp-page-main-track: minmax(0, var(--_esp-page-resolved-max-width)); } :host([kind=\"full\"]:not([data-main-max-configured])[flyout-open]), :host([kind=\"full\"]:not([data-main-max-configured])[preview-reclaiming]), :host([kind=\"site\"][flyout-open]), :host([kind=\"site\"][preview-reclaiming]) { --_esp-page-main-track: 1fr; } .esp-page > .esp-page-preview-space, .esp-page > .esp-page-workspace-resize-handles { display: none; } } /* A forced mode=\"overlay\" flyout is a fixed drawer at any viewport width, so its aside needs the same above-the-header hoist the sub-50em media rule gives the auto-mode drawer — without this the desktop drawer paints beneath the header (aside z 5 vs header z 20). Keyed off the flyout-overlay-open state attribute maintained by syncFlyoutState. */ :host([flyout-overlay-open]) .esp-page > div.esp-page-flyout { z-index: var(--esp-flyout-z-index, 3000); } /* A nested page's fixed drawer still participates in this page's main stacking context. Raise that complete context while the descendant modal is open so the drawer and vellum paint above this page's header instead of trapping their close control beneath it. */ :host([descendant-flyout-overlay-open]) .esp-page > div.esp-page-main { z-index: var(--esp-flyout-z-index, 3000); overflow: visible; } /* Trigger-anchored panels belong to the same document flow as their initiator. BiDirectionalStickyController can keep updating the wrapper's inline top, but position: static deliberately makes it inert until the anchored panel closes. */ :host([flyout-anchored]:not([flyout-full-height])) .esp-page > div.esp-page-flyout > .sticky-wrapper { position: static; } /* Full-document help owns the visible block-size rather than following one field. Span header through footer so the page footer cannot push a viewport-height panel out of view at the document boundary; the sticky controller pins this presentation to the viewport top. */ :host([flyout-full-height]) .esp-page > div.esp-page-flyout { grid-row: top-start / footer-end; z-index: calc(var(--esp-page-header-z-index, 20) + 2); } /* Anchored help connects back to its trigger across a visible preview. Raise only the sized help wrapper above the preview surface; the bridge itself is non-interactive inside esp-flyout. */ :host([flyout-anchored][preview-visible]) .esp-page > div.esp-page-flyout { z-index: calc(var(--esp-page-header-z-index, 20) + 2); } slot[name=\"flyout\"]::slotted(esp-flyout) { --_esp-flyout-used-width: var(--_esp-page-flyout-used-width); --_esp-flyout-preview-bridge-width: 0px; --_esp-flyout-preview-anchor-display: none; } :host([preview-visible]) slot[name=\"flyout\"]::slotted(esp-flyout) { --_esp-flyout-preview-bridge-width: var(--_esp-page-preview-used-width); --_esp-flyout-preview-anchor-display: block; } /* Flyout surface-match contract. A slotted esp-flyout with match-surface lifts its trailing edge with the surface's edge shadow, so the panel reads as a raised peer of the content surface rather than the shadowless tear-off. The leading edge keeps its perforation (the border is not overridden) and casts no shadow, so it stays attached at the tear line instead of floating over the content it flies out from. Opt-in and page-scoped: a standalone flyout has no surface to match and is unaffected. */ slot[name=\"flyout\"]::slotted(esp-flyout[match-surface]) { --esp-flyout-shadow: var(--_esp-page-surface-edge-shadow); } /* Header alignment contract. Published on the slotted esp-header (not :host) so it never shadows an inherited or author override and only the header-slot header adopts it — see the host-token-defaults architecture rule and ADR-005. max-width is the resolved cap; lead is the fraction of surplus placed before the content (0 = start, .5 = center, 1 = end). */ slot[name=\"header\"]::slotted(esp-header) { --esp-header-content-max-width: var(--_esp-page-resolved-max-width); --esp-header-content-lead: 0; } :host([align=\"center\"]) slot[name=\"header\"]::slotted(esp-header) { --esp-header-content-lead: 0.5; } :host([align=\"end\"]) slot[name=\"header\"]::slotted(esp-header) { --esp-header-content-lead: 1; } /* full has no cap; publish 100% (not none) so the header contract calc resolves to zero surplus and leaves the header full-bleed. */ :host([kind=\"full\"]:not([data-main-max-configured])) slot[name=\"header\"]::slotted(esp-header) { --esp-header-content-max-width: 100%; } /* Footer alignment mirrors the header behavior with footer-owned contract names. The full-bleed footer paints its own background; only its internal content adopts the page's cap and lead fraction. */ slot[name=\"footer\"]::slotted(esp-footer) { --esp-footer-content-max-width: var(--_esp-page-resolved-max-width); --esp-footer-content-lead: 0; } :host([align=\"center\"]) slot[name=\"footer\"]::slotted(esp-footer) { --esp-footer-content-lead: 0.5; } :host([align=\"end\"]) slot[name=\"footer\"]::slotted(esp-footer) { --esp-footer-content-lead: 1; } :host([kind=\"full\"]:not([data-main-max-configured])) slot[name=\"footer\"]::slotted(esp-footer) { --esp-footer-content-max-width: 100%; } /* Site: bar content aligns with the section wells, not a main cap. One public token governs the header, the footer, and every esp-section well, and the well is centered — declared after the align lead rules so the centered site column wins on order. */ :host([kind=\"site\"]) slot[name=\"header\"]::slotted(esp-header) { --esp-header-content-max-width: var(--esp-page-well-max-width, 72rem); --esp-header-content-lead: 0.5; } :host([kind=\"site\"]) slot[name=\"footer\"]::slotted(esp-footer) { --esp-footer-content-max-width: var(--esp-page-well-max-width, 72rem); --esp-footer-content-lead: 0.5; } /* Contained shell: the header and footer ride the surface instead of the full viewport, so the whole page reads as one band on the canvas. Only their column placement changes — the full-height canvas and surface layers already do the rest. */ :host([contained]) .esp-page > div.esp-page-top, :host([contained]) .esp-page > div.esp-page-footer { grid-column: surface; } /* On the surface the bar already spans the content width, so the header fills it (no content padding) and drops its own shadow — the continuous surface edge shadow does the framing. */ :host([contained]) slot[name=\"header\"]::slotted(esp-header) { --esp-header-shadow: none; } /* The 100% contract belongs to the capped kinds, where the surface column IS the content width. A contained site keeps the shared well column (ESP0174) — sections and chrome must not split columns just because the shell is contained. */ :host([contained]:not([kind=\"site\"])) slot[name=\"header\"]::slotted(esp-header) { --esp-header-content-max-width: 100%; } :host([contained]:not([kind=\"site\"])) slot[name=\"footer\"]::slotted(esp-footer) { --esp-footer-content-max-width: 100%; } :host([kind=\"narrow\"]) .esp-page > div.esp-page-main { /* The golden reading measure, from the shared token (ESP0174); 66ch remains the fallback and the default emission. */ > ::slotted(*) { max-inline-size: var(--esp-measure, 66ch); } } #dialog-drop-zone { z-index: 4000; position: absolute; } :host([preview-visible]) .esp-page > div.esp-page-preview-space > .esp-page-preview { display: block; } :host([workspace-resizable][preview-visible]:not([flyout-overlay-open])) .esp-page > div.esp-page-workspace-resize-handles > .esp-page-main-preview-resize-handle, :host([workspace-resizable][flyout-open]:not([flyout-overlay-open])) .esp-page > div.esp-page-workspace-resize-handles > .esp-page-preview-flyout-resize-handle { display: block; } :host([workspace-resizable]:not([data-workspace-resize-ready])) .esp-page > .esp-page-workspace-resize-handles { display: none; } :host([workspace-resizable][data-workspace-trailing-edge-exposed]:not([flyout-overlay-open])) .esp-page > div.esp-page-workspace-resize-handles > .esp-page-workspace-end-resize-handle, :host([workspace-resizable][data-workspace-position-held]:not([flyout-overlay-open])) .esp-page > div.esp-page-workspace-resize-handles > .esp-page-workspace-end-resize-handle { display: block; } /* Pointer-owned resizing controls the logical leading gutter. It stays fixed for zero-sum pane trades and yields as an outward drag consumes centered outside canvas, keeping the active separator under the pointer in both cases. On release JS moves this pixel track to the aligned target; the normal grid transition supplies the glide before the intrinsic fr gutter contract resumes. */ :host([data-workspace-position-held]) { --_esp-page-gutter-left: var(--_esp-page-resize-leading-gutter-width, 0px); --_esp-page-gutter-right: 1fr; } :host([data-workspace-resizing]) { cursor: col-resize; user-select: none; } /* The grid-track glide that makes programmatic open/close feel animated reads as cursor lag while a pointer owns the seam, and a mid-glide grid feeds stale geometry into the clamp ranges of the next keyboard step. Track changes must be immediate for the whole resize session, pointer or keyboard. */ :host([data-workspace-resizing]) .esp-page, :host([data-workspace-keyboard-adjusting]) .esp-page { transition: none; } /* The linked menu switches its rail to a fixed drawer, but light-DOM menu content can still contribute an intrinsic width to the page's sidebar track. Collapse the owning aside only after the validated header/menu handshake is active, so unmatched navigation remains visible and reachable. */ :host([data-preview-navigation-collapsed]) .esp-page > aside.esp-page-left { inline-size: 0; min-inline-size: 0; } :host([data-preview-measuring]) .esp-page { transition: none; } /* New preview layouts are validated against visible geometry before presentation. Keep that candidate surface non-visible so a rejected measurement can never flash on screen. */ :host([data-preview-validating]) .esp-page-preview { visibility: hidden; } .esp-page { /* dvh keeps the shell within the visible mobile viewport; 100vh is the fallback for browsers without dvh (dvh === vh on desktop). */ min-height: 100vh; min-height: 100dvh; display: grid; grid-template-columns: [full-start canvas-left-start] var(--_esp-page-gutter-left) [canvas-left-end surface-start left-start] min-content [left-end main-start] var(--_esp-page-main-track) [main-end right-start] min-content [right-end surface-end canvas-right-start flyout-start] var(--_esp-page-workspace-track) [flyout-end] var(--_esp-page-gutter-right) [canvas-right-end full-end]; /* Animate the flyout claiming/releasing its track (and the gutter rebalance) via grid track interpolation where supported; elsewhere the modes still apply, just without the glide. Keep this 0.25s duration synchronized with WORKSPACE_RECENTER_DURATION in workspace-resize-session.ts. */ transition: grid-template-columns 0.25s ease; grid-template-rows: [top-start] min-content [top-end content-start] 1fr [content-end footer-start] min-content [footer-end]; overflow-x: clip; background: var(--esp-page-background, var(--esp-color-background)); line-height: 1.5; font-family: var( --_esp-font-body-effective, var(--esp-font-body, var(--_esp-font-body-fallback)) ); font-size: var(--esp-type-normal); color: var(--esp-color-text); position: relative; &:before { content: \" \"; display: block; position: absolute; left: 0; top: 0; right: 0; bottom: 0; opacity: var(--esp-page-background-image-opacity, 1); background-image: var(--esp-page-background-image, none); z-index: 1; } > div.esp-page-top { z-index: var(--esp-page-header-z-index, 20); grid-column: full; grid-row: top; position: relative; /* The header spans every column, so without inline-size containment its content's min-content would be distributed into the aside tracks and inflate them below the cap (the asides would never collapse). Contain it like the main well so it takes its width from the grid, not the grid from it. */ contain: inline-size; } /* Sidebar containers — act as the \"track\" for sticky positioning */ aside.esp-page-left, aside.esp-page-right { z-index: 5; grid-row: content; position: relative; /* No explicit height — grid stretch fills the 1fr row, and the aside's bottom edge constrains sticky children. */ } > aside.esp-page-left { grid-column: left; } > div.esp-page-main { z-index: 5; grid-column: main; grid-row: content; position: relative; /* Clip oversized content without becoming a scroll container. A hidden-overflow wrapper traps descendant view timelines (and sticky positioning) on this non-scrolling main well. */ overflow: clip; /* The main well's own background. Defaults to transparent, so the well shows the page background exactly as before; set it to give the content well a distinct card color, or leave it transparent (with --esp-page-surface-shadow: none) for content that floats directly on the page/canvas. */ background: var(--esp-page-main-background, transparent); /* The cap now lives in the main track (minmax(0, cap)); the element fills its column. contain: inline-size still holds because the track gives a definite inline size. */ contain: inline-size; } > aside.esp-page-right { grid-column: right; } /* A neutral div, unlike the left/right asides: the slotted esp-flyout is itself the complementary landmark, and an aside wrapper would nest a second, empty landmark around it. */ > div.esp-page-flyout { /* Sits after preview inside the exact auxiliary track and sizes to the allocated help width. Outer surplus remains in the following gutter rather than under an empty stretched wrapper. */ grid-column: flyout; justify-self: start; grid-row: content; z-index: 5; position: relative; inline-size: var(--_esp-page-flyout-used-width); min-inline-size: 0; margin-inline-start: var(--_esp-page-preview-used-width); /* In the drawer rung the fixed esp-flyout paints inside this wrapper's stacking context, so the wrapper itself must rise above the header (z 20) while staying below the dialog drop zone (4000). Uses the same token as the drawer — set it on the page (or higher) to move both together. */ @media ${unsafeCSS(COMPACT_VIEWPORT_MEDIA_QUERY)} { z-index: var(--esp-flyout-z-index, 3000); } } /* Preview begins at the surface edge and help is offset after its resolved width, producing the logical order main → preview → help inside their exact shared auxiliary track. */ > div.esp-page-preview-space { grid-column: flyout; grid-row: content; z-index: 4; position: relative; min-inline-size: 0; > .esp-page-preview { display: none; box-sizing: border-box; inline-size: var(--_esp-page-preview-used-width); position: relative; background: var( --esp-page-preview-background, var(--esp-page-main-background, var(--esp-page-background, var(--esp-color-background))) ); border-inline-start: var(--esp-page-preview-border, 1px dotted var(--esp-color-border)); box-shadow: var(--esp-page-preview-shadow, var(--_esp-page-surface-edge-shadow)); } } /* A transparent interaction layer keeps the complete hit target above main, preview, and anchored help without raising either painted surface (which would hide the help bridge). */ > div.esp-page-workspace-resize-handles { grid-column: flyout; grid-row: top-start / footer-end; z-index: calc(var(--esp-page-header-z-index, 20) + 3); position: relative; min-inline-size: 0; pointer-events: none; } > div.esp-page-workspace-resize-handles > .esp-page-workspace-resize-handle { display: none; box-sizing: border-box; position: absolute; inset-block: 0; z-index: 3; inline-size: var(--esp-page-resize-handle-hit-size, 2.75rem); margin-inline-start: calc(-0.5 * var(--esp-page-resize-handle-hit-size, 2.75rem)); border: 0; outline: 0; background: transparent; cursor: col-resize; pointer-events: auto; touch-action: none; /* The target spans the seam so it remains reachable during document scroll. Focus draws a dashed line along the seam itself — the visible height of the dotted tear-off edge, capped at one viewport — kept in view by sticky positioning while the page scrolls. */ &::after { content: \"\"; display: block; position: sticky; inset-block-start: 0; box-sizing: border-box; inline-size: 0; margin-inline: auto; block-size: 100dvh; max-block-size: 100%; pointer-events: none; } &:focus-visible::after { border-inline-start: var( --esp-page-resize-focus-outline, 2px dashed var(--esp-color-link) ); /* Glow in the line's own color so the focused seam reads as one deliberate highlight rather than a neutral drop shadow. */ box-shadow: var(--esp-page-resize-focus-shadow, 0 0 0.75rem var(--esp-color-link)); } } > div.esp-page-workspace-resize-handles > .esp-page-main-preview-resize-handle { inset-inline-start: 0; } > div.esp-page-workspace-resize-handles > .esp-page-preview-flyout-resize-handle { inset-inline-start: var(--_esp-page-preview-used-width); } > div.esp-page-workspace-resize-handles > .esp-page-workspace-end-resize-handle { inset-inline-start: calc( var(--_esp-page-preview-used-width) + var(--_esp-page-flyout-used-width) ); } /* Keep token-sized probes outside the width-constrained gutter. If they lived inside a zero-width preview space, layout would clamp their rectangles to zero before JS could decide whether the reclaim rung should be activated. */ > .esp-page-preview-min-width-probe, > .esp-page-preview-default-width-probe, > .esp-page-preview-max-width-probe, > .esp-page-flyout-min-width-probe, > .esp-page-flyout-default-width-probe, > .esp-page-flyout-max-width-probe, > .esp-page-main-min-width-probe, > .esp-page-main-configured-max-width-probe, > .esp-page-main-max-width-probe, > .esp-page-resize-step-probe, > .esp-page-resize-large-step-probe { position: absolute; block-size: 0; visibility: hidden; pointer-events: none; contain: strict; } > .esp-page-preview-min-width-probe { inline-size: var(--_esp-page-preview-resolved-min-width); } > .esp-page-preview-max-width-probe { inline-size: var(--_esp-page-preview-resolved-max-width); } > .esp-page-preview-default-width-probe { inline-size: var(--esp-page-preview-default-width, 0px); } > .esp-page-flyout-min-width-probe { inline-size: var(--_esp-page-flyout-resolved-min-width); } > .esp-page-flyout-max-width-probe { inline-size: var(--_esp-page-flyout-resolved-max-width); } > .esp-page-flyout-default-width-probe { inline-size: var(--esp-page-flyout-default-width, 0px); } > .esp-page-main-min-width-probe { inline-size: var(--_esp-page-main-min-width); } > .esp-page-main-configured-max-width-probe { inline-size: var(--esp-page-main-max-width, 0px); } > .esp-page-main-max-width-probe { inline-size: var(--_esp-page-resolved-max-width); } > .esp-page-resize-step-probe { inline-size: var(--esp-page-resize-step, 1rem); } > .esp-page-resize-large-step-probe { inline-size: var(--esp-page-resize-large-step, 4rem); } /* Canvas: the outer gutter regions promoted to a styleable layer. They only have width once the viewport exceeds the cap, so with the transparent defaults they are invisible and narrow pages are unchanged. Painted below the content (z-index 5) and below the surface shadow. */ > .esp-page-canvas { /* Full height: the canvas runs top-to-bottom. In the default full-bleed chrome the header/footer span the gutters and cover it there, so only the content-band gutters show; in contained mode the header/footer sit on the surface and the canvas shows full height beside them. */ grid-row: top-start / footer-end; z-index: 1; position: relative; pointer-events: none; background-color: var(--esp-page-canvas-background, transparent); /* Image layer mirrors the page background-image knobs: a separate layer so its opacity does not fade the color. */ &::before { content: \"\"; position: absolute; inset: 0; background-image: var(--esp-page-canvas-background-image, none); opacity: var(--esp-page-canvas-background-image-opacity, 1); } } > .esp-page-canvas--left { grid-column: canvas-left; } > .esp-page-canvas--right { /* The canvas spans both the auxiliary workspace and the independent outer gutter beneath it. */ grid-column: canvas-right; } /* Surface: a transparent backdrop spanning the surface cluster (left aside + main + right aside). It carries the surface edge shadow (on by default) and an optional hairline border. The shadow casts outward into the gutters; when there are no gutters the surface fills the viewport and overflow-x: clip on .esp-page trims the shadow away — so it is visible only above the cap with no container query needed. */ > .esp-page-surface { grid-column: surface; /* Full height so the edge shadow is one continuous line from the header through the content to the footer — no fade at the region joins. */ grid-row: top-start / footer-end; z-index: 2; pointer-events: none; box-shadow: var(--_esp-page-surface-shadow); border-inline: var(--_esp-page-surface-border); } /* Internal wrapper manipulated by BiDirectionalStickyController */ .sticky-wrapper { will-change: top; display: block; width: 100%; position: sticky; top: 0; } > div.esp-page-footer { grid-column: full; grid-row: footer; z-index: 10; background: var(--esp-page-background, var(--esp-color-background)); /* Spans every column; contain it so its content does not inflate the aside tracks (see the header note above). */ contain: inline-size; } &.sticky-header { slot[name=\"header\"]::slotted(esp-header), slot[name=\"sidebar\"]::slotted(esp-menu), slot[name=\"right\"]::slotted(esp-menu) { --esp-menu-top-offset: var(--_esp-page-fixed-header-offset); } > div.esp-page-top { position: sticky; top: var(--esp-page-sticky-header-top, 0); } } &.fixed-header { slot[name=\"header\"]::slotted(esp-header), slot[name=\"sidebar\"]::slotted(esp-menu), slot[name=\"right\"]::slotted(esp-menu) { --esp-menu-top-offset: var(--_esp-page-fixed-header-offset); } > div.esp-page-top { position: fixed; top: 0; left: 0; right: 0; } > div.esp-page-main, > aside.esp-page-left, > aside.esp-page-right, > div.esp-page-flyout, > .esp-page-surface, > .esp-page-canvas { margin-top: var(--_esp-page-fixed-header-offset); } } /* Inherit fonts for inputs and buttons */ input, button, textarea, select { font-family: inherit; font-size: inherit; } /* Make sure text areas without a rows attribute are not tiny */ textarea:not([rows]) { min-height: 10em; } /* Anything that has been anchored to should have extra scroll margin */ :target { scroll-margin-block: 5ex; } } .esp-page:has(esp-dialog[is-open=\"true\"]) { overflow: hidden; } /* Main and preview are additive surfaces. Once preview owns the trailing edge, move elevation there so no shadow appears at the internal join; the surface resumes its normal frame when preview is hidden. */ :host([preview-visible]) .esp-page > .esp-page-surface { box-shadow: none; } /* The preview's outer grid item and landmark span the complete page so the sticky wrapper's containing block has enough travel to remain pinned throughout a tall document. Its dynamic-viewport block size and vertical overflow keep its scroll position independent from the main document. It paints above full-bleed header chrome only in the preview's negotiated gutter track; overlay flyouts still take precedence by hiding preview presentation. */ .esp-page > div.esp-page-preview-space { grid-row: top-start / footer-end; z-index: calc(var(--esp-page-header-z-index, 20) + 1); pointer-events: none; > .esp-page-preview { position: absolute; inset-block: 0; inset-inline-start: 0; pointer-events: auto; > .sticky-wrapper { block-size: 100vh; block-size: 100dvh; overflow-y: auto; /* Scroll preview first, then hand wheel/trackpad motion back to the document at either boundary (or when no overflow exists). */ overscroll-behavior-y: auto; } } } @media (prefers-reduced-motion: reduce) { .esp-page { transition: none; } } `, ]"
53606
54025
  }
53607
54026
  ],
53608
54027
  "exports": [
@@ -53622,7 +54041,7 @@
53622
54041
  "declarations": [
53623
54042
  {
53624
54043
  "kind": "class",
53625
- "description": "Used to lay out standard page structure.",
54044
+ "description": "Arranges a page's header, navigation, main content, and footer.",
53626
54045
  "name": "EspalierPage",
53627
54046
  "cssProperties": [
53628
54047
  {
@@ -53742,7 +54161,7 @@
53742
54161
  "name": "--esp-page-header-z-index"
53743
54162
  },
53744
54163
  {
53745
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
54164
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
53746
54165
  "name": "--esp-field-background",
53747
54166
  "inheritedFrom": {
53748
54167
  "name": "EspalierElementBase",
@@ -56286,6 +56705,17 @@
56286
56705
  "description": "Whether preview is currently reserving width and allowing the main well\nto shrink toward `--esp-page-main-min-width`. Managed by the page.",
56287
56706
  "fieldName": "previewReclaiming"
56288
56707
  },
56708
+ {
56709
+ "type": {
56710
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
56711
+ },
56712
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
56713
+ "name": "surface-layer",
56714
+ "inheritedFrom": {
56715
+ "name": "EspalierElementBase",
56716
+ "module": "dist/shared/esp-element-base.js"
56717
+ }
56718
+ },
56289
56719
  {
56290
56720
  "name": "scheme",
56291
56721
  "type": {
@@ -57121,7 +57551,7 @@
57121
57551
  "declarations": [
57122
57552
  {
57123
57553
  "kind": "class",
57124
- "description": "Component for selecting a single option from a list of\noptions. For selecting multiple options, see\n[esp-pick-some](/components/pickers/pick-some/).\n\n```html\n<esp-box>\n <esp-form-item label=\"Pick a fruit\">\n <esp-pick-one>\n <esp-picker-item text=\"Apple\" value=\"apple\" icon=\"apple\"></esp-picker-item>\n <esp-picker-item text=\"Cherry\" value=\"cherry\" icon=\"cherry\"></esp-picker-item>\n <esp-picker-item text=\"Lemon\" value=\"lemon\" icon=\"lemon\"></esp-picker-item>\n </esp-pick-one>\n </esp-form-item>\n</esp-box>\n```\n\n### Initial state\n\nSet the initially selected item using the `selected` attribute\non the `esp-picker-item`:\n\n```html\n<esp-box>\n <esp-form-item label=\"Pick a fruit\">\n <esp-pick-one>\n <esp-picker-item text=\"Apple\" value=\"apple\" icon=\"apple\"></esp-picker-item>\n <esp-picker-item text=\"Cherry\" value=\"cherry\" icon=\"cherry\"></esp-picker-item>\n <esp-picker-item text=\"Lemon\" value=\"lemon\" icon=\"lemon\" selected></esp-picker-item>\n </esp-pick-one>\n </esp-form-item>\n</esp-box>\n```\n\nAn item's `text`, `icon`, and `styles` may change after it is selected (a\nplaceholder label replaced once its data loads, say), and the picker shows\nthe new label; the selection follows the item's `value`.\n\n### Variable-height items\n\nItems with longer text wrap naturally. The menu sizes itself\nto fit the actual content height of each item:\n\n```html\n<div style=\"width: 260px\">\n <esp-box>\n <esp-form-item label=\"Pick a plan\">\n <esp-pick-one>\n <esp-picker-item\n text=\"Basic - includes core features only\"\n value=\"basic\">\n </esp-picker-item>\n <esp-picker-item\n text=\"Pro - adds analytics, priority support, and advanced integrations for growing teams\"\n value=\"pro\">\n </esp-picker-item>\n <esp-picker-item\n text=\"Enterprise - everything in Pro plus SSO, audit logs, dedicated account management, and custom SLAs\"\n value=\"enterprise\">\n </esp-picker-item>\n <esp-picker-item\n text=\"Starter\"\n value=\"starter\">\n </esp-picker-item>\n </esp-pick-one>\n </esp-form-item>\n </esp-box>\n</div>\n```\n\n### Typeahead (local filtering)\n\nAdd the `typeahead` attribute to let the user type to filter\nthe option list. The input becomes editable and the picker\nswitches to a filter icon on focus:\n\n```html\n<esp-box>\n <esp-form-item label=\"Pick a fruit\">\n <esp-pick-one typeahead>\n <esp-picker-item text=\"Apple\" value=\"apple\"></esp-picker-item>\n <esp-picker-item text=\"Cherry\" value=\"cherry\"></esp-picker-item>\n <esp-picker-item text=\"Lemon\" value=\"lemon\"></esp-picker-item>\n </esp-pick-one>\n </esp-form-item>\n</esp-box>\n```\n\n### Typeahead (remote filtering)\n\nSet the `fetchItems` property to a callback that returns\nmatching items from a server. The picker debounces input\n(300 ms by default) and shows a loading indicator while\nthe request is in flight. Use the provided `AbortSignal`\nto cancel stale requests:\n\n```html\n<esp-box>\n <esp-form-item label=\"Search users\">\n <esp-pick-one typeahead placeholder=\"Type a name\\u2026\">\n </esp-pick-one>\n </esp-form-item>\n</esp-box>\n<script>\n const picker = findByTagName(\"esp-pick-one\")[0];\n picker.fetchItems = async (query, signal) => {\n const res = await fetch(\"/assets/data/grid-data.json\", { signal });\n const all = await res.json();\n const q = query.toLowerCase();\n return all\n .filter(u =>\n u.first_name.toLowerCase().includes(q) ||\n u.last_name.toLowerCase().includes(q))\n .slice(0, 20)\n .map(u => ({\n text: `${u.first_name} ${u.last_name}`,\n value: String(u.id),\n }));\n };\n</script>\n```",
57554
+ "description": "Selects one option from a list. For selecting multiple options, see\n[esp-pick-some](/components/pickers/pick-some/).\n\n```html\n<esp-box>\n <esp-form-item label=\"Pick a fruit\">\n <esp-pick-one>\n <esp-picker-item text=\"Apple\" value=\"apple\" icon=\"apple\"></esp-picker-item>\n <esp-picker-item text=\"Cherry\" value=\"cherry\" icon=\"cherry\"></esp-picker-item>\n <esp-picker-item text=\"Lemon\" value=\"lemon\" icon=\"lemon\"></esp-picker-item>\n </esp-pick-one>\n </esp-form-item>\n</esp-box>\n```\n\n### Initial state\n\nSet the initially selected item using the `selected` attribute\non the `esp-picker-item`:\n\n```html\n<esp-box>\n <esp-form-item label=\"Pick a fruit\">\n <esp-pick-one>\n <esp-picker-item text=\"Apple\" value=\"apple\" icon=\"apple\"></esp-picker-item>\n <esp-picker-item text=\"Cherry\" value=\"cherry\" icon=\"cherry\"></esp-picker-item>\n <esp-picker-item text=\"Lemon\" value=\"lemon\" icon=\"lemon\" selected></esp-picker-item>\n </esp-pick-one>\n </esp-form-item>\n</esp-box>\n```\n\nAn item's `text`, `icon`, and `styles` may change after it is selected (a\nplaceholder label replaced once its data loads, say), and the picker shows\nthe new label; the selection follows the item's `value`.\n\n### Variable-height items\n\nItems with longer text wrap naturally. The menu sizes itself\nto fit the actual content height of each item:\n\n```html\n<div style=\"width: 260px\">\n <esp-box>\n <esp-form-item label=\"Pick a plan\">\n <esp-pick-one>\n <esp-picker-item\n text=\"Basic - includes core features only\"\n value=\"basic\">\n </esp-picker-item>\n <esp-picker-item\n text=\"Pro - adds analytics, priority support, and advanced integrations for growing teams\"\n value=\"pro\">\n </esp-picker-item>\n <esp-picker-item\n text=\"Enterprise - everything in Pro plus SSO, audit logs, dedicated account management, and custom SLAs\"\n value=\"enterprise\">\n </esp-picker-item>\n <esp-picker-item\n text=\"Starter\"\n value=\"starter\">\n </esp-picker-item>\n </esp-pick-one>\n </esp-form-item>\n </esp-box>\n</div>\n```\n\n### Typeahead (local filtering)\n\nAdd the `typeahead` attribute to let the user type to filter\nthe option list. The input becomes editable and the picker\nswitches to a filter icon on focus:\n\n```html\n<esp-box>\n <esp-form-item label=\"Pick a fruit\">\n <esp-pick-one typeahead>\n <esp-picker-item text=\"Apple\" value=\"apple\"></esp-picker-item>\n <esp-picker-item text=\"Cherry\" value=\"cherry\"></esp-picker-item>\n <esp-picker-item text=\"Lemon\" value=\"lemon\"></esp-picker-item>\n </esp-pick-one>\n </esp-form-item>\n</esp-box>\n```\n\n### Typeahead (remote filtering)\n\nSet the `fetchItems` property to a callback that returns\nmatching items from a server. The picker debounces input\n(300 ms by default) and shows a loading indicator while\nthe request is in flight. Use the provided `AbortSignal`\nto cancel stale requests:\n\n```html\n<esp-box>\n <esp-form-item label=\"Search users\">\n <esp-pick-one typeahead placeholder=\"Type a name\\u2026\">\n </esp-pick-one>\n </esp-form-item>\n</esp-box>\n<script>\n const picker = findByTagName(\"esp-pick-one\")[0];\n picker.fetchItems = async (query, signal) => {\n const res = await fetch(\"/assets/data/grid-data.json\", { signal });\n const all = await res.json();\n const q = query.toLowerCase();\n return all\n .filter(u =>\n u.first_name.toLowerCase().includes(q) ||\n u.last_name.toLowerCase().includes(q))\n .slice(0, 20)\n .map(u => ({\n text: `${u.first_name} ${u.last_name}`,\n value: String(u.id),\n }));\n };\n</script>\n```",
57125
57555
  "name": "EspalierPickOne",
57126
57556
  "members": [
57127
57557
  {
@@ -59011,6 +59441,17 @@
59011
59441
  "module": "dist/pickers/esp-picker-base.js"
59012
59442
  }
59013
59443
  },
59444
+ {
59445
+ "type": {
59446
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
59447
+ },
59448
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
59449
+ "name": "surface-layer",
59450
+ "inheritedFrom": {
59451
+ "name": "EspalierElementBase",
59452
+ "module": "dist/shared/esp-element-base.js"
59453
+ }
59454
+ },
59014
59455
  {
59015
59456
  "name": "scheme",
59016
59457
  "type": {
@@ -59077,7 +59518,7 @@
59077
59518
  },
59078
59519
  "cssProperties": [
59079
59520
  {
59080
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
59521
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
59081
59522
  "name": "--esp-field-background",
59082
59523
  "inheritedFrom": {
59083
59524
  "name": "EspalierElementBase",
@@ -59160,7 +59601,7 @@
59160
59601
  "declarations": [
59161
59602
  {
59162
59603
  "kind": "class",
59163
- "description": "Component for selecting multiple options from a list of options.\n\n```html\n<esp-box>\n <esp-form-item label=\"Pick some fruit\">\n <esp-pick-some>\n <esp-picker-item text=\"Apple\" value=\"apple\" icon=\"apple\"></esp-picker-item>\n <esp-picker-item text=\"Cherry\" value=\"cherry\" icon=\"cherry\"></esp-picker-item>\n <esp-picker-item text=\"Lemon\" value=\"banana\" icon=\"lemon\"></esp-picker-item>\n </esp-pick-some>\n </esp-form-item>\n</esp-box>\n```\n\n### Initial state (HTML)\n\nSet the initially selected items using the `selected` attribute\non the `esp-picker-item` elements:\n\n```html\n<esp-box>\n <esp-form-item label=\"Pick a fruit\">\n <esp-pick-some>\n <esp-picker-item text=\"Apple\" value=\"apple\" icon=\"apple\"></esp-picker-item>\n <esp-picker-item text=\"Cherry\" value=\"cherry\" icon=\"cherry\" selected></esp-picker-item>\n <esp-picker-item text=\"Lemon\" value=\"lemon\" icon=\"lemon\" selected></esp-picker-item>\n </esp-pick-some>\n </esp-form-item>\n</esp-box>\n```\n\n### Initial state (programmatic / pickerItems)\n\nWhen populating options via the `pickerItems` property (common\nin Lit templates that load data from an API), set `selected: true`\non items that should be pre-selected. The picker automatically\ndetects items with `selected: true` and renders them as chips:\n\n```ts\nconst allTopics = await fetchTopics();\nconst mySelectedIds = new Set(user.topicIds);\n\npicker.pickerItems = allTopics.map(t => ({\n text: t.name,\n value: t.id,\n selected: mySelectedIds.has(t.id),\n}));\n```\n\nThis works with all modes — plain, typeahead (local), and\ntypeahead (remote via `fetchItems`). The component reads the\n`selected` flag from `pickerItems` during `willUpdate()` and\npopulates `selectedItems` automatically. **Do not** separately\nassign `selectedItems` — just set `selected: true` on the\nitems in `pickerItems`.\n\n### Typeahead (local filtering)\n\nAdd the `typeahead` attribute to let the user type to filter\nthe option list. The input becomes editable and the picker\nswitches to a filter icon on focus:\n\n```html\n<esp-box>\n <esp-form-item label=\"Pick some fruit\">\n <esp-pick-some typeahead>\n <esp-picker-item text=\"Apple\" value=\"apple\"></esp-picker-item>\n <esp-picker-item text=\"Cherry\" value=\"cherry\"></esp-picker-item>\n <esp-picker-item text=\"Lemon\" value=\"lemon\"></esp-picker-item>\n <esp-picker-item text=\"Banana\" value=\"banana\"></esp-picker-item>\n <esp-picker-item text=\"Mango\" value=\"mango\"></esp-picker-item>\n </esp-pick-some>\n </esp-form-item>\n</esp-box>\n```\n\n### Typeahead (remote filtering)\n\nSet the `fetchItems` property to a callback that returns\nmatching items from a server. The picker debounces input\n(300 ms by default) and shows a loading indicator while\nthe request is in flight. Use the provided `AbortSignal`\nto cancel stale requests:\n\n```html\n<esp-box>\n <esp-form-item label=\"Search users\">\n <esp-pick-some typeahead placeholder=\"Type a name…\">\n </esp-pick-some>\n </esp-form-item>\n</esp-box>\n<script>\n const picker = findByTagName(\"esp-pick-some\")[0];\n picker.fetchItems = async (query, signal) => {\n const res = await fetch(\"/assets/data/grid-data.json\", { signal });\n const all = await res.json();\n const q = query.toLowerCase();\n return all\n .filter(u =>\n u.first_name.toLowerCase().includes(q) ||\n u.last_name.toLowerCase().includes(q))\n .slice(0, 20)\n .map(u => ({\n text: `${u.first_name} ${u.last_name}`,\n value: String(u.id),\n }));\n };\n</script>\n```\n\n### Add new value\n\nSet the `addNewValue` property to a callback that creates\na new picker item from the typed text. When the user types\na value that does not match any existing item, the dropdown\ncaret becomes a plus icon. Clicking the plus (or pressing\nEnter) invokes the callback and adds the returned item as\na checked selection:\n\n```html\n<esp-box>\n <esp-form-item label=\"Pick some tags\">\n <esp-pick-some typeahead placeholder=\"Type to search or add…\">\n <esp-picker-item text=\"Bug\" value=\"bug\"></esp-picker-item>\n <esp-picker-item text=\"Feature\" value=\"feature\"></esp-picker-item>\n <esp-picker-item text=\"Docs\" value=\"docs\"></esp-picker-item>\n </esp-pick-some>\n </esp-form-item>\n</esp-box>\n<script>\n const picker = findByTagName(\"esp-pick-some\")[0];\n picker.addNewValue = (text) => ({\n text,\n value: text.toLowerCase().replace(/\\s+/g, \"-\"),\n selected: true,\n });\n</script>\n```",
59604
+ "description": "Selects multiple options from a list.\n\n```html\n<esp-box>\n <esp-form-item label=\"Pick some fruit\">\n <esp-pick-some>\n <esp-picker-item text=\"Apple\" value=\"apple\" icon=\"apple\"></esp-picker-item>\n <esp-picker-item text=\"Cherry\" value=\"cherry\" icon=\"cherry\"></esp-picker-item>\n <esp-picker-item text=\"Lemon\" value=\"banana\" icon=\"lemon\"></esp-picker-item>\n </esp-pick-some>\n </esp-form-item>\n</esp-box>\n```\n\n### Initial state (HTML)\n\nSet the initially selected items using the `selected` attribute\non the `esp-picker-item` elements:\n\n```html\n<esp-box>\n <esp-form-item label=\"Pick a fruit\">\n <esp-pick-some>\n <esp-picker-item text=\"Apple\" value=\"apple\" icon=\"apple\"></esp-picker-item>\n <esp-picker-item text=\"Cherry\" value=\"cherry\" icon=\"cherry\" selected></esp-picker-item>\n <esp-picker-item text=\"Lemon\" value=\"lemon\" icon=\"lemon\" selected></esp-picker-item>\n </esp-pick-some>\n </esp-form-item>\n</esp-box>\n```\n\n### Initial state (programmatic / pickerItems)\n\nWhen populating options via the `pickerItems` property (common\nin Lit templates that load data from an API), set `selected: true`\non items that should be pre-selected. The picker automatically\ndetects items with `selected: true` and renders them as chips:\n\n```ts\nconst allTopics = await fetchTopics();\nconst mySelectedIds = new Set(user.topicIds);\n\npicker.pickerItems = allTopics.map(t => ({\n text: t.name,\n value: t.id,\n selected: mySelectedIds.has(t.id),\n}));\n```\n\nThis works with all modes — plain, typeahead (local), and\ntypeahead (remote via `fetchItems`). The component reads the\n`selected` flag from `pickerItems` during `willUpdate()` and\npopulates `selectedItems` automatically. **Do not** separately\nassign `selectedItems` — just set `selected: true` on the\nitems in `pickerItems`.\n\n### Typeahead (local filtering)\n\nAdd the `typeahead` attribute to let the user type to filter\nthe option list. The input becomes editable and the picker\nswitches to a filter icon on focus:\n\n```html\n<esp-box>\n <esp-form-item label=\"Pick some fruit\">\n <esp-pick-some typeahead>\n <esp-picker-item text=\"Apple\" value=\"apple\"></esp-picker-item>\n <esp-picker-item text=\"Cherry\" value=\"cherry\"></esp-picker-item>\n <esp-picker-item text=\"Lemon\" value=\"lemon\"></esp-picker-item>\n <esp-picker-item text=\"Banana\" value=\"banana\"></esp-picker-item>\n <esp-picker-item text=\"Mango\" value=\"mango\"></esp-picker-item>\n </esp-pick-some>\n </esp-form-item>\n</esp-box>\n```\n\n### Typeahead (remote filtering)\n\nSet the `fetchItems` property to a callback that returns\nmatching items from a server. The picker debounces input\n(300 ms by default) and shows a loading indicator while\nthe request is in flight. Use the provided `AbortSignal`\nto cancel stale requests:\n\n```html\n<esp-box>\n <esp-form-item label=\"Search users\">\n <esp-pick-some typeahead placeholder=\"Type a name…\">\n </esp-pick-some>\n </esp-form-item>\n</esp-box>\n<script>\n const picker = findByTagName(\"esp-pick-some\")[0];\n picker.fetchItems = async (query, signal) => {\n const res = await fetch(\"/assets/data/grid-data.json\", { signal });\n const all = await res.json();\n const q = query.toLowerCase();\n return all\n .filter(u =>\n u.first_name.toLowerCase().includes(q) ||\n u.last_name.toLowerCase().includes(q))\n .slice(0, 20)\n .map(u => ({\n text: `${u.first_name} ${u.last_name}`,\n value: String(u.id),\n }));\n };\n</script>\n```\n\n### Add new value\n\nSet the `addNewValue` property to a callback that creates\na new picker item from the typed text. When the user types\na value that does not match any existing item, the dropdown\ncaret becomes a plus icon. Clicking the plus (or pressing\nEnter) invokes the callback and adds the returned item as\na checked selection:\n\n```html\n<esp-box>\n <esp-form-item label=\"Pick some tags\">\n <esp-pick-some typeahead placeholder=\"Type to search or add…\">\n <esp-picker-item text=\"Bug\" value=\"bug\"></esp-picker-item>\n <esp-picker-item text=\"Feature\" value=\"feature\"></esp-picker-item>\n <esp-picker-item text=\"Docs\" value=\"docs\"></esp-picker-item>\n </esp-pick-some>\n </esp-form-item>\n</esp-box>\n<script>\n const picker = findByTagName(\"esp-pick-some\")[0];\n picker.addNewValue = (text) => ({\n text,\n value: text.toLowerCase().replace(/\\s+/g, \"-\"),\n selected: true,\n });\n</script>\n```",
59164
59605
  "name": "EspalierPickSome",
59165
59606
  "members": [
59166
59607
  {
@@ -61120,6 +61561,17 @@
61120
61561
  "module": "dist/pickers/esp-picker-base.js"
61121
61562
  }
61122
61563
  },
61564
+ {
61565
+ "type": {
61566
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
61567
+ },
61568
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
61569
+ "name": "surface-layer",
61570
+ "inheritedFrom": {
61571
+ "name": "EspalierElementBase",
61572
+ "module": "dist/shared/esp-element-base.js"
61573
+ }
61574
+ },
61123
61575
  {
61124
61576
  "name": "scheme",
61125
61577
  "type": {
@@ -61178,7 +61630,7 @@
61178
61630
  },
61179
61631
  "cssProperties": [
61180
61632
  {
61181
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
61633
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
61182
61634
  "name": "--esp-field-background",
61183
61635
  "inheritedFrom": {
61184
61636
  "name": "EspalierElementBase",
@@ -62781,6 +63233,17 @@
62781
63233
  "description": "An explicit width for the picker trigger. Accepts any valid\nCSS width value (e.g. `\"5em\"`, `\"80px\"`). When set, the host\nelement is sized to this width while the dropdown menu is\nallowed to be wider to accommodate longer items.\n\n```html\n<esp-pick-one width=\"5em\">\n <esp-picker-item text=\"Apple\" value=\"apple\"></esp-picker-item>\n</esp-pick-one>\n```",
62782
63234
  "fieldName": "width"
62783
63235
  },
63236
+ {
63237
+ "type": {
63238
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
63239
+ },
63240
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
63241
+ "name": "surface-layer",
63242
+ "inheritedFrom": {
63243
+ "name": "EspalierElementBase",
63244
+ "module": "dist/shared/esp-element-base.js"
63245
+ }
63246
+ },
62784
63247
  {
62785
63248
  "name": "scheme",
62786
63249
  "type": {
@@ -62825,7 +63288,7 @@
62825
63288
  },
62826
63289
  "cssProperties": [
62827
63290
  {
62828
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
63291
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
62829
63292
  "name": "--esp-field-background",
62830
63293
  "inheritedFrom": {
62831
63294
  "name": "EspalierElementBase",
@@ -63946,7 +64409,7 @@
63946
64409
  "declarations": [
63947
64410
  {
63948
64411
  "kind": "class",
63949
- "description": "The single-select picker's selection state.\n\nTwo facts have to stay in step: which item is selected, and — when a `.value`\nwas set that matched nothing — what that unmatched value was so it can be\nre-applied once items arrive. Consumers can set `.value` before the slotted\n`esp-picker-item` elements have been extracted, so \"no match yet\" is a normal\nstate rather than an error.\n\nThe rule is *not* \"pending if and only if nothing matched\". A provisional\nselection derived from a DOM `selected` attribute can coexist with a pending\nprogrammatic `.value`, and the pending value must still win once items\nresolve — see withProvisionalItem. Encoding that as named\nconstructors replaces six hand-maintained assignment sites, three of which\nlived inside inline `render()` handlers where the distinction was invisible.\n\n`pending` is deliberately distinct from `undefined`: `.value = undefined` is\nan explicit clear, which is not the same as \"nothing is pending\".",
64412
+ "description": "Track the selected item and any programmatic value waiting for matching options.\n\nConsumers may set `.value` before options arrive. A provisional selection\nfrom a DOM `selected` attribute can coexist with that pending value; the\nprogrammatic value wins once a matching item becomes available. See\nwithProvisionalItem.\n\n`pending` is separate from `undefined` because `.value = undefined` explicitly\nclears the selection.",
63950
64413
  "name": "PickOneSelection",
63951
64414
  "members": [
63952
64415
  {
@@ -64427,7 +64890,7 @@
64427
64890
  "declarations": [
64428
64891
  {
64429
64892
  "kind": "class",
64430
- "description": "Display a contextual UI overlay that appears when a\nuser interacts with an element, providing a temporary space\nfor additional information, options, or actions without blocking\nthe underlying content. Uses the native Popover API to render\nin the browser's top layer, escaping all ancestor `overflow`\nand `z-index` contexts.\n\n```html\n<esp-popover>\n <esp-button label=\"Show popover\" slot=\"target\"></esp-button>\n <esp-box slot=\"popover\">\n <h2>I'm a popover!</h2>\n <p>It can have anything in it.</p>\n <esp-popover>\n <esp-button label=\"Show nested popover\" slot=\"target\"></esp-button>\n <esp-box slot=\"popover\">\n <h2>I'm a nested popover!</h2>\n <p>I will not cause my parent popover to close.</p>\n <esp-popover>\n <esp-button label=\"Show another popover\" slot=\"target\"></esp-button>\n <esp-box slot=\"popover\">\n <h2>I'm a very nested popover!</h2>\n <p>I will not cause my parent popovers to close.</p>\n </esp-box>\n </esp-popover>\n </esp-box>\n </esp-popover>\n </esp-box>\n</esp-popover>\n```",
64893
+ "description": "Shows contextual information, options, or actions while the underlying\npage remains interactive. Uses the native Popover API to render\nin the browser's top layer, escaping all ancestor `overflow`\nand `z-index` contexts.\n\n```html\n<esp-popover>\n <esp-button label=\"Show popover\" slot=\"target\"></esp-button>\n <esp-box slot=\"popover\">\n <h2>I'm a popover!</h2>\n <p>It can have anything in it.</p>\n <esp-popover>\n <esp-button label=\"Show nested popover\" slot=\"target\"></esp-button>\n <esp-box slot=\"popover\">\n <h2>I'm a nested popover!</h2>\n <p>I will not cause my parent popover to close.</p>\n <esp-popover>\n <esp-button label=\"Show another popover\" slot=\"target\"></esp-button>\n <esp-box slot=\"popover\">\n <h2>I'm a very nested popover!</h2>\n <p>I will not cause my parent popovers to close.</p>\n </esp-box>\n </esp-popover>\n </esp-box>\n </esp-popover>\n </esp-box>\n</esp-popover>\n```",
64431
64894
  "name": "EspalierPopover",
64432
64895
  "slots": [
64433
64896
  {
@@ -64922,7 +65385,7 @@
64922
65385
  "name": "--esp-progress-text-color"
64923
65386
  },
64924
65387
  {
64925
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
65388
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
64926
65389
  "name": "--esp-field-background",
64927
65390
  "inheritedFrom": {
64928
65391
  "name": "EspalierElementBase",
@@ -65864,6 +66327,17 @@
65864
66327
  "description": "Display mode for the progress indicator.\n\n- `bar` — linear progress bar (default)\n- `circle` — circular SVG ring",
65865
66328
  "fieldName": "mode"
65866
66329
  },
66330
+ {
66331
+ "type": {
66332
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
66333
+ },
66334
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
66335
+ "name": "surface-layer",
66336
+ "inheritedFrom": {
66337
+ "name": "EspalierElementBase",
66338
+ "module": "dist/shared/esp-element-base.js"
66339
+ }
66340
+ },
65867
66341
  {
65868
66342
  "name": "scheme",
65869
66343
  "type": {
@@ -66990,6 +67464,17 @@
66990
67464
  "description": "Disables all child radio buttons in the group.",
66991
67465
  "fieldName": "disabled"
66992
67466
  },
67467
+ {
67468
+ "type": {
67469
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
67470
+ },
67471
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
67472
+ "name": "surface-layer",
67473
+ "inheritedFrom": {
67474
+ "name": "EspalierElementBase",
67475
+ "module": "dist/shared/esp-element-base.js"
67476
+ }
67477
+ },
66993
67478
  {
66994
67479
  "name": "scheme",
66995
67480
  "type": {
@@ -67048,7 +67533,7 @@
67048
67533
  "customElement": true,
67049
67534
  "cssProperties": [
67050
67535
  {
67051
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
67536
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
67052
67537
  "name": "--esp-field-background",
67053
67538
  "inheritedFrom": {
67054
67539
  "name": "EspalierElementBase",
@@ -67147,7 +67632,7 @@
67147
67632
  "name": "--esp-radio-button-focus-shadow"
67148
67633
  },
67149
67634
  {
67150
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
67635
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
67151
67636
  "name": "--esp-field-background",
67152
67637
  "inheritedFrom": {
67153
67638
  "name": "EspalierElementBase",
@@ -68061,6 +68546,17 @@
68061
68546
  "description": "Controls whether the radio button is disabled.",
68062
68547
  "fieldName": "disabled"
68063
68548
  },
68549
+ {
68550
+ "type": {
68551
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
68552
+ },
68553
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
68554
+ "name": "surface-layer",
68555
+ "inheritedFrom": {
68556
+ "name": "EspalierElementBase",
68557
+ "module": "dist/shared/esp-element-base.js"
68558
+ }
68559
+ },
68064
68560
  {
68065
68561
  "name": "scheme",
68066
68562
  "type": {
@@ -68192,7 +68688,7 @@
68192
68688
  "name": "--esp-repeater-grid-row-gap"
68193
68689
  },
68194
68690
  {
68195
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
68691
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
68196
68692
  "name": "--esp-field-background",
68197
68693
  "inheritedFrom": {
68198
68694
  "name": "EspalierElementBase",
@@ -70236,6 +70732,17 @@
70236
70732
  "description": "Optional consumer-provided error message. When set it takes priority\nover internal fetch errors for display purposes.",
70237
70733
  "fieldName": "errorMessage"
70238
70734
  },
70735
+ {
70736
+ "type": {
70737
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
70738
+ },
70739
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
70740
+ "name": "surface-layer",
70741
+ "inheritedFrom": {
70742
+ "name": "EspalierElementBase",
70743
+ "module": "dist/shared/esp-element-base.js"
70744
+ }
70745
+ },
70239
70746
  {
70240
70747
  "name": "scheme",
70241
70748
  "type": {
@@ -70354,7 +70861,7 @@
70354
70861
  "declarations": [
70355
70862
  {
70356
70863
  "kind": "class",
70357
- "description": "Root container for the Espalier design system.\n\nProvides the full design-token pipeline — colors, scales, and\nfonts — to every descendant component. All tokens are\nJS-computed from a pair of Base64-encoded theme objects and\nemitted as CSS custom properties on the host element.\n\n### Theme API\n\nSupply a `light-theme` and/or `dark-theme` attribute containing\na **Base64-encoded JSON** partial theme. Fields you omit fall\nback to built-in defaults. Switch between them with `scheme`.\n\n```html\n<esp-root\n light-theme=\"eyJzZWVkQ29sb3IiOiJva2xjaCgwLjcgMC4yIDE1NSkifQ==\"\n scheme=\"light\">\n <esp-box>\n <h2>Themed content</h2>\n </esp-box>\n</esp-root>\n```\n\n### Nested theme previews and scoped events\n\nA nested root creates a scoped theme for its descendants. It does not create\na separate event bus: every root publishes theme changes through the same\nEspalier singleton. Subscribe through the root whose changes you own so a\nnested preview cannot be mistaken for an application-level theme change.\n\n```ts\nconst applicationRoot = document.querySelector<EspalierRoot>(\"#application-theme\")!;\n\nconst unsubscribe = applicationRoot.subscribeScoped(\"scheme-changed\", ({ scheme }) => {\n localStorage.setItem(\"scheme\", scheme);\n});\n\n// Initial state is read directly; scoped events report later changes only.\nrenderScheme(applicationRoot.scheme);\n\n// Call when the owning integration is disposed.\nunsubscribe();\n```\n\nThis contract covers `scheme-changed`, `seed-color-changed`,\n`theme-changed`, and `icon-sprite-url-changed`. Components derived from\n`EspalierElementBase` use the same closest-root filter internally.\n\nThe root also owns the application's banner-texture vocabulary:\n`EspalierRoot.registerTexture(name, definition)` registers an\nimage-backed texture preset that any\n[esp-image](/components/image) banner in the application selects with\n`texture=\"<name>\"`, and `EspalierRoot.registeredTextures()` enumerates\nthe presets for building texture pickers.",
70864
+ "description": "Root container for the Espalier design system.\n\nSupplies descendant components with color, size, and font tokens.\nConfigure light and dark themes through JavaScript properties or\nBase64-encoded attributes. The root emits CSS custom properties\nfor the active theme.\n\n### Theme API\n\nSupply a `light-theme` and/or `dark-theme` attribute containing\na **Base64-encoded JSON** partial theme. Fields you omit fall\nback to built-in defaults. Switch between them with `scheme`.\n\n```html\n<esp-root\n light-theme=\"eyJzZWVkQ29sb3IiOiJva2xjaCgwLjcgMC4yIDE1NSkifQ==\"\n scheme=\"light\">\n <esp-box>\n <h2>Themed content</h2>\n </esp-box>\n</esp-root>\n```\n\n### Nested theme previews and scoped events\n\nA nested root creates a scoped theme for its descendants. It does not create\na separate event bus: every root publishes theme changes through the same\nEspalier singleton. Subscribe through the root whose changes you own so a\nnested preview cannot be mistaken for an application-level theme change.\n\n```ts\nconst applicationRoot = document.querySelector<EspalierRoot>(\"#application-theme\")!;\n\nconst unsubscribe = applicationRoot.subscribeScoped(\"scheme-changed\", ({ scheme }) => {\n localStorage.setItem(\"scheme\", scheme);\n});\n\n// Initial state is read directly; scoped events report later changes only.\nrenderScheme(applicationRoot.scheme);\n\n// Call when the owning integration is disposed.\nunsubscribe();\n```\n\nThis contract covers `scheme-changed`, `seed-color-changed`,\n`theme-changed`, and `icon-sprite-url-changed`. Components derived from\n`EspalierElementBase` use the same closest-root filter internally.\n\nThe root also owns the application's banner-texture vocabulary:\n`EspalierRoot.registerTexture(name, definition)` registers an\nimage-backed texture preset that any\n[esp-image](/components/image) banner in the application selects with\n`texture=\"<name>\"`, and `EspalierRoot.registeredTextures()` enumerates\nthe presets for building texture pickers.",
70358
70865
  "name": "EspalierRoot",
70359
70866
  "cssProperties": [
70360
70867
  {
@@ -71176,7 +71683,7 @@
71176
71683
  "kind": "field",
71177
71684
  "name": "lightDomStyles",
71178
71685
  "static": true,
71179
- "default": "` /* ── Base typography ──────────────────────────────────── */ esp-root { font-family: var(--_esp-font-body-effective, var(--esp-font-body, system-ui, sans-serif)); font-weight: var(--esp-font-weight-body); line-height: 1.5; font-size: var(--esp-type-normal); color: var(--esp-color-text); } /* ── Lists ────────────────────────────────────────────── */ esp-root ul[role=\"list\"], esp-root ol[role=\"list\"] { list-style: none; } esp-root ul li::marker, esp-root ol li::marker { font-weight: bold; } /* ── Form controls ────────────────────────────────────── */ esp-root input, esp-root select, esp-root button { font-family: var(--_esp-font-body-effective, var(--esp-font-body, system-ui, sans-serif)); } /* ── Links ────────────────────────────────────────────── */ esp-root a:not([class]) { text-decoration-skip-ink: auto; font-weight: bold; color: var(--esp-color-link); } esp-root a { text-decoration: underline; text-decoration-skip-ink: auto; color: var(--esp-color-link); } esp-root a:hover { background-color: var(--esp-color-link-hover-bg); color: var(--esp-color-link-hover); text-decoration: none; } /* ── Inherited text styles ────────────────────────────── */ esp-root input, esp-root button, esp-root textarea, esp-root select, esp-root p, esp-root ul, esp-root li { font-family: var(--_esp-font-body-effective, var(--esp-font-body, system-ui, sans-serif)); color: var(--esp-color-text); font-size: inherit; } esp-root textarea:not([rows]) { min-height: 10em; } /* ── Headings ─────────────────────────────────────────── */ esp-root h1, esp-root h2, esp-root h3, esp-root h4, esp-root h5 { font-family: var( --_esp-font-headings-effective, var( --esp-font-headings, var(--_esp-font-body-effective, var(--esp-font-body, system-ui, sans-serif)) ) ); font-weight: var(--esp-font-weight-headings); color: var(--esp-color-headings); margin: var(--esp-size-small-to-normal) 0 var(--esp-size-tiny-to-small); } esp-root h5 { font-size: var(--esp-type-small); line-height: 1.4; } esp-root h4 { font-size: var(--esp-type-medium); line-height: 1.3; } esp-root h3 { font-size: var(--esp-type-big); line-height: 1.2; } esp-root h2 { font-size: var(--esp-type-large); line-height: 1.15; } esp-root h1 { font-size: var(--esp-type-huge); line-height: 1.1; } /* ── Code / Pre ───────────────────────────────────────── */ esp-root pre, esp-root code { font-family: var(--_esp-font-monospace-effective, var(--esp-font-monospace, monospace)); font-weight: var(--esp-font-weight-monospace); } esp-root code { font-size: var(--esp-type-small); background: var(--esp-color-layer-1); border-radius: var(--esp-size-border-radius); padding: 0 0.35em; display: inline-block; border: 1px dotted var(--esp-color-border); } esp-root pre { background-color: var(--esp-color-layer-1); border-radius: var(--esp-size-border-radius); border: 1px dotted var(--esp-color-border); } esp-root pre > code { font-size: var(--esp-type-tiny); display: block; background: none; border: none; hyphens: none; tab-size: 2; white-space: pre; padding: var(--esp-size-padding); overflow: auto; line-height: 1.8; } /* ── Images ────────────────────────────────────────────── */ esp-root img, esp-root picture { max-width: 100%; display: block; } `",
71686
+ "default": "` ${surfaceLayerAttributes(\"esp-root [surface-layer]\").cssText} /* ── Base typography ──────────────────────────────────── */ esp-root { font-family: var(--_esp-font-body-effective, var(--esp-font-body, system-ui, sans-serif)); font-weight: var(--esp-font-weight-body); line-height: 1.5; font-size: var(--esp-type-normal); color: var(--esp-color-text); } /* ── Lists ────────────────────────────────────────────── */ esp-root ul[role=\"list\"], esp-root ol[role=\"list\"] { list-style: none; } esp-root ul li::marker, esp-root ol li::marker { font-weight: bold; } /* ── Form controls ────────────────────────────────────── */ esp-root input, esp-root select, esp-root button { font-family: var(--_esp-font-body-effective, var(--esp-font-body, system-ui, sans-serif)); } /* ── Links ────────────────────────────────────────────── */ esp-root a:not([class]) { text-decoration-skip-ink: auto; font-weight: bold; color: var(--esp-color-link); } esp-root a { text-decoration: underline; text-decoration-skip-ink: auto; color: var(--esp-color-link); } esp-root a:hover { background-color: var(--esp-color-link-hover-bg); color: var(--esp-color-link-hover); text-decoration: none; } /* ── Inherited text styles ────────────────────────────── */ esp-root input, esp-root button, esp-root textarea, esp-root select, esp-root p, esp-root ul, esp-root li { font-family: var(--_esp-font-body-effective, var(--esp-font-body, system-ui, sans-serif)); color: var(--esp-color-text); font-size: inherit; } esp-root textarea:not([rows]) { min-height: 10em; } /* ── Headings ─────────────────────────────────────────── */ esp-root h1, esp-root h2, esp-root h3, esp-root h4, esp-root h5 { font-family: var( --_esp-font-headings-effective, var( --esp-font-headings, var(--_esp-font-body-effective, var(--esp-font-body, system-ui, sans-serif)) ) ); font-weight: var(--esp-font-weight-headings); color: var(--esp-color-headings); margin: var(--esp-size-small-to-normal) 0 var(--esp-size-tiny-to-small); } esp-root h5 { font-size: var(--esp-type-small); line-height: 1.4; } esp-root h4 { font-size: var(--esp-type-medium); line-height: 1.3; } esp-root h3 { font-size: var(--esp-type-big); line-height: 1.2; } esp-root h2 { font-size: var(--esp-type-large); line-height: 1.15; } esp-root h1 { font-size: var(--esp-type-huge); line-height: 1.1; } /* ── Code / Pre ───────────────────────────────────────── */ esp-root pre, esp-root code { font-family: var(--_esp-font-monospace-effective, var(--esp-font-monospace, monospace)); font-weight: var(--esp-font-weight-monospace); } esp-root code { font-size: var(--esp-type-small); background: var(--esp-color-layer-1); border-radius: var(--esp-size-border-radius); padding: 0 0.35em; display: inline-block; border: 1px dotted var(--esp-color-border); } esp-root pre { background-color: var(--esp-color-layer-1); border-radius: var(--esp-size-border-radius); border: 1px dotted var(--esp-color-border); } esp-root pre > code { font-size: var(--esp-type-tiny); display: block; background: none; border: none; hyphens: none; tab-size: 2; white-space: pre; padding: var(--esp-size-padding); overflow: auto; line-height: 1.8; } /* ── Images ────────────────────────────────────────────── */ esp-root img, esp-root picture { max-width: 100%; display: block; } `",
71180
71687
  "description": "Light-DOM styles injected into `document.head` so that\nslotted content (headings, links, code blocks, lists, etc.)\ninherits the design-system tokens defined on `:host`.\nSelectors are scoped to `esp-root` descendants to avoid\nleaking into content outside the root."
71181
71688
  }
71182
71689
  ],
@@ -71368,7 +71875,7 @@
71368
71875
  },
71369
71876
  {
71370
71877
  "kind": "class",
71371
- "description": "A horizontal flow: children sit in a wrapping row with a themed gap —\n\"row, wrap, gap Y\" without writing flexbox (ADR-010's micro half).\n\n```html\n<esp-row gap=\"small\" justify=\"between\">\n <span>Yoga · 60 min</span>\n <esp-button label=\"Book\"></esp-button>\n</esp-row>\n```\n\nRows wrap by default, so a button row or badge cluster degrades to\nmultiple lines at narrow widths instead of overflowing; set `nowrap`\nfor toolbars that must stay on one line. Cross-axis alignment\ndefaults to `center` — the icon-beside-label, text-beside-button\ncase a row exists for.",
71878
+ "description": "Arranges children in a wrapping row with a themed gap.\n\n```html\n<esp-row gap=\"small\" justify=\"between\">\n <span>Yoga · 60 min</span>\n <esp-button label=\"Book\"></esp-button>\n</esp-row>\n```\n\nRows wrap by default, so a button row or badge cluster degrades to\nmultiple lines at narrow widths instead of overflowing; set `nowrap`\nfor toolbars that must stay on one line. Cross-axis alignment\ndefaults to `center` — the icon-beside-label, text-beside-button\ncase a row exists for.",
71372
71879
  "name": "EspalierRow",
71373
71880
  "cssProperties": [
71374
71881
  {
@@ -71376,7 +71883,7 @@
71376
71883
  "name": "--esp-row-gap"
71377
71884
  },
71378
71885
  {
71379
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
71886
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
71380
71887
  "name": "--esp-field-background",
71381
71888
  "inheritedFrom": {
71382
71889
  "name": "EspalierElementBase",
@@ -72255,6 +72762,17 @@
72255
72762
  "description": "Keeps every child on one line instead of wrapping.",
72256
72763
  "fieldName": "nowrap"
72257
72764
  },
72765
+ {
72766
+ "type": {
72767
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
72768
+ },
72769
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
72770
+ "name": "surface-layer",
72771
+ "inheritedFrom": {
72772
+ "name": "EspalierElementBase",
72773
+ "module": "dist/shared/esp-element-base.js"
72774
+ }
72775
+ },
72258
72776
  {
72259
72777
  "name": "scheme",
72260
72778
  "type": {
@@ -72407,7 +72925,7 @@
72407
72925
  "name": "--esp-search-loading-indicator-track-color"
72408
72926
  },
72409
72927
  {
72410
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
72928
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
72411
72929
  "name": "--esp-field-background",
72412
72930
  "inheritedFrom": {
72413
72931
  "name": "EspalierElementBase",
@@ -73490,6 +74008,17 @@
73490
74008
  "description": "Placeholder text for the search input.",
73491
74009
  "fieldName": "placeholder"
73492
74010
  },
74011
+ {
74012
+ "type": {
74013
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
74014
+ },
74015
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
74016
+ "name": "surface-layer",
74017
+ "inheritedFrom": {
74018
+ "name": "EspalierElementBase",
74019
+ "module": "dist/shared/esp-element-base.js"
74020
+ }
74021
+ },
73493
74022
  {
73494
74023
  "name": "scheme",
73495
74024
  "type": {
@@ -73591,7 +74120,7 @@
73591
74120
  "declarations": [
73592
74121
  {
73593
74122
  "kind": "class",
73594
- "description": "A full-bleed page band with a centered content well — the natural host\nfor a theme zone.\n\n`esp-section` paints the local `--esp-color-background` edge to edge\nand centers its content in a capped well. It carries no card identity:\nno raised surface, no border radius, no shadow. Give it a `context`\n(see the theming guide, `/guides/color/theming` on the docs site) and\nthe band renders the zone's complete token table with nothing to\nneutralize:\n\n```html\n<esp-section context=\"inverted\">\n <h2>The reversed band</h2>\n <p>Everything in here renders on the zone's tokens, and the band\n spans its container edge to edge.</p>\n</esp-section>\n```\n\nInside `esp-page kind=\"site\"` sections stack full-width and their\nwells share the page's `--esp-page-well-max-width`, so section\ncontent, the header, and the footer all align on one column. Outside\na page the well defaults to the same width and the band fills\nwhatever container it is given.\n\nThe vertical rhythm defaults to the theme's fluid `--esp-size-section`\nstep. For edge-to-edge content (a full-bleed hero image) clear both\nthe well cap and the inline breathing room —\n`--esp-section-max-width: none; --esp-section-padding-inline: 0;` —\nand drop `--esp-section-padding-block` too when the media should\nmeet the band's edges; the band's zone behavior is unaffected.\n\nA section may also tint one raster alpha mask behind its content.\nThe component owns clipping, no-repeat mask behavior, stacking, and\ninteraction isolation; consumers opt in with the `decoration`\nattribute and supply only the image, semantic tint, position, size, and\nopacity:\n\n```html\n<esp-section\n decoration\n style=\"\n --esp-section-decoration-image: url('/assets/brand-mark.png');\n --esp-section-decoration-position: calc(100% + 2rem) -2rem;\n --esp-section-decoration-size: min(34rem, 62%);\n --esp-section-decoration-opacity: 0.09;\n \"\n>\n <h1>Rooted in warmth</h1>\n</esp-section>\n```\n\nWithout `decoration` the section renders no decorative layer at all, and\nthe `--esp-section-decoration-*` hooks have no effect. The attribute is\nwhat keeps an undecorated band free of painted planes: accessibility\nauditors such as axe-core and Lighthouse do not model `mask-image`, so a\ntinted plane hidden only by a mask is read as the background behind every\nword in the section. Set it together with the image, never on its own. A\ndecorated band is audited as though the tint covered it everywhere, so\ntext that passes an audit also reads where it crosses the mark.\n\nCSS image values are trusted stylesheet input. Products that accept image\nreferences from documents or users must enforce their own asset ownership\nand URL policy before mapping a value to this hook.",
74123
+ "description": "A full-bleed page band with a centered content well — the natural host\nfor a theme zone.\n\n`esp-section` paints the local `--esp-color-background` edge to edge\nand centers its content in a capped well. It carries no card identity:\nno raised surface, no border radius, no shadow. Give it a `context`\n(see [theme overview](/espalier-guides/#foundations)) and\nthe band renders the zone's complete token table with nothing to\nneutralize:\n\n```html\n<esp-section context=\"inverted\">\n <h2>The reversed band</h2>\n <p>Everything in here renders on the zone's tokens, and the band\n spans its container edge to edge.</p>\n</esp-section>\n```\n\nInside `esp-page kind=\"site\"` sections stack full-width and their\nwells share the page's `--esp-page-well-max-width`, so section\ncontent, the header, and the footer all align on one column. Outside\na page the well defaults to the same width and the band fills\nwhatever container it is given.\n\nThe vertical rhythm defaults to the theme's fluid `--esp-size-section`\nstep. For edge-to-edge content (a full-bleed hero image) clear both\nthe well cap and the inline breathing room —\n`--esp-section-max-width: none; --esp-section-padding-inline: 0;` —\nand drop `--esp-section-padding-block` too when the media should\nmeet the band's edges; the band's zone behavior is unaffected.\n\nA section may also tint one raster alpha mask behind its content.\nThe component owns clipping, no-repeat mask behavior, stacking, and\ninteraction isolation; consumers opt in with the `decoration`\nattribute and supply only the image, semantic tint, position, size, and\nopacity:\n\n```html\n<esp-section\n decoration\n style=\"\n --esp-section-decoration-image: url('/assets/brand-mark.png');\n --esp-section-decoration-position: calc(100% + 2rem) -2rem;\n --esp-section-decoration-size: min(34rem, 62%);\n --esp-section-decoration-opacity: 0.09;\n \"\n>\n <h1>Rooted in warmth</h1>\n</esp-section>\n```\n\nWithout `decoration` the section renders no decorative layer at all, and\nthe `--esp-section-decoration-*` hooks have no effect. The attribute is\nwhat keeps an undecorated band free of painted planes: accessibility\nauditors such as axe-core and Lighthouse do not model `mask-image`, so a\ntinted plane hidden only by a mask is read as the background behind every\nword in the section. Set it together with the image, never on its own. A\ndecorated band is audited as though the tint covered it everywhere, so\ntext that passes an audit also reads where it crosses the mark.\n\nCSS image values are trusted stylesheet input. Products that accept image\nreferences from documents or users must enforce their own asset ownership\nand URL policy before mapping a value to this hook.",
73595
74124
  "name": "EspalierSection",
73596
74125
  "cssProperties": [
73597
74126
  {
@@ -73631,7 +74160,7 @@
73631
74160
  "name": "--esp-section-decoration-opacity"
73632
74161
  },
73633
74162
  {
73634
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
74163
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
73635
74164
  "name": "--esp-field-background",
73636
74165
  "inheritedFrom": {
73637
74166
  "name": "EspalierElementBase",
@@ -74451,6 +74980,17 @@
74451
74980
  "description": "Whether the section paints its decorative mask plane. Set it whenever\n`--esp-section-decoration-image` names an image; without it the\nsection renders no decorative layer and the decoration hooks are\ninert.",
74452
74981
  "fieldName": "decoration"
74453
74982
  },
74983
+ {
74984
+ "type": {
74985
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
74986
+ },
74987
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
74988
+ "name": "surface-layer",
74989
+ "inheritedFrom": {
74990
+ "name": "EspalierElementBase",
74991
+ "module": "dist/shared/esp-element-base.js"
74992
+ }
74993
+ },
74454
74994
  {
74455
74995
  "name": "scheme",
74456
74996
  "type": {
@@ -74930,7 +75470,7 @@
74930
75470
  "description": "The minimum absolute $L_c$ score (default 60)"
74931
75471
  }
74932
75472
  ],
74933
- "description": "Ensure that a text color achieves at least the target\n$|L_c|$ against a background color.\n\nIf the current contrast is insufficient, lightness is\nadjusted (made darker on light bg, lighter on dark bg)\nuntil the target is met or the lightness boundary is hit."
75473
+ "description": "Adjust a text color toward the target absolute APCA Lc score.\n\nIf the current contrast is insufficient, lightness is\nadjusted (made darker on light bg, lighter on dark bg)\nuntil the target is met or the lightness boundary is hit."
74934
75474
  },
74935
75475
  {
74936
75476
  "kind": "function",
@@ -75372,13 +75912,13 @@
75372
75912
  "kind": "variable",
75373
75913
  "name": "CONFIGURED_BRAND_LOGO_ASPECT_RATIO",
75374
75914
  "default": "css`3 / 1`",
75375
- "description": "The ratio a configured-brand logo's box is reserved at before its bytes\narrive, shared by both consumers so they cannot drift apart.\n\nBoth hosts give the logo a definite block size and leave the inline size to\nthe image, so until the image resolves there is no natural ratio to derive it\nfrom: a pending request lays the element out at zero inline size and a failed\none at the width of its alt text. Either way the brand region resizes for a\nreason that has nothing to do with the logo — on first paint, and again on\nevery scheme swap that fails, because both schemes exchange `src` at runtime.\n\nEach host spends it as the fallback of its own\n`--esp-{header,footer}-brand-logo-aspect-ratio`, in an\n`aspect-ratio: auto <ratio>` declaration. `auto` is what makes this one\ndeclaration the whole fix: it keeps a replaced element's *natural* ratio\nauthoritative whenever it has one, so a logo that loads is laid out exactly\nas it is today — including a very wide one, where the inline cap clamps the\nused width without feeding back into the definite block size. The ratio\ngoverns only the states with nothing to measure.\n\n`3 / 1` because the existing caps already imply a wordmark of about that\nshape: the footer's `3rem` block size against a `12rem` inline cap clamps at\n4:1, and the header's `0.72 x --esp-header-height` against the same cap\nclamps near 3.7:1. A 3:1 reservation is therefore never born clamped, so the\nreserved box is always the full ratio box."
75915
+ "description": "Reserve a logo's width while its image is loading or has failed.\n\nThe header and footer give logos a fixed block size, so an image without\nintrinsic dimensions would otherwise have zero width or use its alt text's\nwidth. `aspect-ratio: auto <ratio>` uses this fallback only until a natural\nratio is available, keeping loaded logos unchanged.\n\nThe 3:1 fallback fits below both hosts' existing inline caps, so the initial\nreservation is not clipped."
75376
75916
  },
75377
75917
  {
75378
75918
  "kind": "variable",
75379
75919
  "name": "configuredBrandLogoGeometry",
75380
75920
  "default": "css` .brand-logo { overflow-wrap: anywhere; } `",
75381
- "description": "Keep a failed logo's alt text from deciding how wide the logo's box is.\n\nThe `alt` text is not suppressed — it still names the image for assistive\ntechnology and still renders as the visible fallback. But a failed image is\nno longer replaced content, so it takes an automatic minimum size from its\nown text, and a minimum wider than the reserved box wins: measured in real\nChrome, a one-word alt of 37 characters pushed the header's logo back out to\nits inline cap and swallowed the reservation whole. Allowing a break anywhere\nmakes that minimum one character wide, so the reserved ratio decides the box\nand the alt text wraps inside it.\n\nThe footer already had this by inheritance — it sets `overflow-wrap` on\n`.configured-brand`, where the header sets it only on `.brand-text` — so this\nmakes the two consumers agree rather than changing the footer. It has no\neffect at all on an image that loads, because replaced content has no text to\nbreak."
75921
+ "description": "Allow failed-image alt text to wrap within the reserved logo width.\n\nA failed image can take its automatic minimum width from its alt text.\nBreaking long words prevents that minimum from overriding the reserved box.\nThe text remains visible and accessible. Loaded images are unaffected."
75382
75922
  },
75383
75923
  {
75384
75924
  "kind": "function",
@@ -76216,11 +76756,11 @@
76216
76756
  "declarations": [
76217
76757
  {
76218
76758
  "kind": "class",
76219
- "description": "Shared base class for Espalier elements.\n\nThe reusable `.esp-field` shell used by text inputs, textareas,\npickers, date pickers, buttons, and similar controls exposes the\nfollowing shared styling hooks:",
76759
+ "description": "Shared base class for Espalier elements.\n\nThe reusable `.esp-field` shell used by text inputs, textareas,\npickers, date pickers, buttons, and similar controls exposes the\nfollowing shared styling hooks:\n\nA field fills one layer above the surface it sits on.",
76220
76760
  "name": "EspalierElementBase",
76221
76761
  "cssProperties": [
76222
76762
  {
76223
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
76763
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
76224
76764
  "name": "--esp-field-background"
76225
76765
  },
76226
76766
  {
@@ -76804,6 +77344,13 @@
76804
77344
  ],
76805
77345
  "events": [],
76806
77346
  "attributes": [
77347
+ {
77348
+ "type": {
77349
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
77350
+ },
77351
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
77352
+ "name": "surface-layer"
77353
+ },
76807
77354
  {
76808
77355
  "name": "scheme",
76809
77356
  "type": {
@@ -78912,7 +79459,7 @@
78912
79459
  }
78913
79460
  }
78914
79461
  ],
78915
- "description": "Compute justified album rows with explicit cell widths.\n\nAlgorithm:\n1. Add images to a row one at a time, scaled to the target row height.\n2. When the combined width overflows the container, the row breaks either\n before or after the overflowing image — whichever leaves the row height\n closer to the target. Keeping the image shrinks the row slightly below\n target; pushing it to the next row stretches the row above target.\n3. Scale the finished row so it fills the container width exactly.\n\nCompleted rows always span the available width. When their justified height exceeds\n`maxRowHeight`, their cells retain their relative widths while expanding to\nthe row width at the ceiling; a renderer can use `object-fit: cover` to crop\nthe resulting cells. A final row below a finite `maxImagesPerRow` instead\nremains partial and preserves its image proportions. Its height is capped by\nthe tallest preceding row (or the target height when it is the only row) and\nby `maxRowHeight`.\n\nWith an omitted or invalid `maxImagesPerRow`, rows are unlimited in length\nand every row is completed."
79462
+ "description": "Compute justified album rows and the geometry a renderer needs for cropping.\n\nCompleted rows fill the available width. A row capped by `maxRowHeight` keeps\nrelative cell widths, allowing `object-fit: cover` to crop its images. A final\nrow below a finite `maxImagesPerRow` remains partial and preserves proportions;\nits height is capped by the preceding rows or target height and `maxRowHeight`.\n\nAn omitted or invalid `maxImagesPerRow` means unlimited, completed rows."
78916
79463
  }
78917
79464
  ],
78918
79465
  "exports": [
@@ -80418,6 +80965,47 @@
80418
80965
  }
80419
80966
  ]
80420
80967
  },
80968
+ {
80969
+ "kind": "javascript-module",
80970
+ "path": "dist/shared/surface-layer.js",
80971
+ "declarations": [
80972
+ {
80973
+ "kind": "function",
80974
+ "name": "surfaceLayerAttributes",
80975
+ "return": {
80976
+ "type": {
80977
+ "text": "CSSResult"
80978
+ }
80979
+ },
80980
+ "parameters": [
80981
+ {
80982
+ "name": "selector",
80983
+ "type": {
80984
+ "text": "string"
80985
+ }
80986
+ },
80987
+ {
80988
+ "name": "defaultLayer",
80989
+ "optional": true,
80990
+ "type": {
80991
+ "text": "SurfaceLayer"
80992
+ }
80993
+ }
80994
+ ],
80995
+ "description": "Support `surface-layer=\"1|2|3|4\"` on consumer surfaces in a shadow root.\nInclude `surfaceLayerAttributes(\":host\")` for the consumer component's host,\nor `surfaceLayerAttributes(\"[surface-layer]\")` for elements inside its shadow\ntree. Espalier installs these rules for its own hosts and root light DOM.\nThe attribute declares depth; consumer components still paint their surface.\nA component that always paints a known layer can pass that depth as its\ndefault, so callers do not need to declare it on each instance."
80996
+ }
80997
+ ],
80998
+ "exports": [
80999
+ {
81000
+ "kind": "js",
81001
+ "name": "surfaceLayerAttributes",
81002
+ "declaration": {
81003
+ "name": "surfaceLayerAttributes",
81004
+ "module": "dist/shared/surface-layer.js"
81005
+ }
81006
+ }
81007
+ ]
81008
+ },
80421
81009
  {
80422
81010
  "kind": "javascript-module",
80423
81011
  "path": "dist/shared/theme-fit-report.js",
@@ -80974,7 +81562,7 @@
80974
81562
  "text": "Readonly<\n Record<RoleName, Readonly<Record<string, ReadonlyArray<[SemanticColorName, LightnessKey]>>>>\n>"
80975
81563
  },
80976
81564
  "default": "{ canvas: { color: [ [\"background\", \"surface\"], [\"layer1\", \"raised1\"], [\"layer2\", \"raised2\"], [\"layer3\", \"raised3\"], [\"layer4\", \"raised4\"], ], }, ink: { color: [ [\"text\", \"text\"], [\"typeOverline\", \"text\"], [\"typeLabel\", \"text\"], [\"typeLead\", \"text\"], [\"typeCaption\", \"text\"], [\"inputSelection\", \"text\"], ], heading: [ [\"headings\", \"muted\"], [\"headingsHover\", \"ink\"], ], }, accent: { color: [ [\"linkHoverBg\", \"raised2\"], [\"inputSelectionBg\", \"raised2\"], [\"inputCaret\", \"ink\"], ], text: [[\"link\", \"accent\"]], hover: [[\"linkHover\", \"text\"]], }, action: { // Neither stop here is fixed — both are derived per scheme, whether // or not the theme names the slot. See ACTION_SURFACE_TARGET_L, // ROLE_DERIVED_SURFACE, and ROLE_DERIVED_INK. The values below are // the token → stop structure this table exists to declare; the // derivation replaces them at compile time. color: [[\"actionBackground\", \"muted\"]], ink: [[\"actionText\", \"surface\"]], }, structure: { color: [ [\"border\", \"border\"], [\"shadow\", \"shadow\"], ], }, }",
80977
- "description": "Which semantic tokens each role slot paints, and at which ramp stop.\n\nThis is the compilation table: it turns five designer-facing roles\ninto the twenty-three engine-room tokens. `dangerText` is absent by\ndesign — the status family is reserved and keeps its own source.\n\nThe `accent` role has no body-text slot on purpose. \"Rose is\ndecorative only\" is a structural guarantee here, not a comment: text\nthat must come from the accent family comes through `accent.text`,\nthe deepened variant a designer picked for legibility. `accent.hover`\nlets a hover change *family*, not just lightness — \"plum link, rose\nhover\" is an ordinary brand rule (ESP0172); absent, `linkHover` keeps\nfollowing `accent.text` exactly as before."
81565
+ "description": "Map role slots to semantic tokens and ramp stops.\n\n`dangerText` stays outside the roles layer because status meanings are reserved.\nAccent body copy must use the legible `accent.text` slot. `accent.hover` can\nselect a different family; without it, hovered links follow `accent.text`."
80978
81566
  },
80979
81567
  {
80980
81568
  "kind": "variable",
@@ -81029,7 +81617,7 @@
81029
81617
  }
81030
81618
  }
81031
81619
  ],
81032
- "description": "Compile roles into semantic mappings.\n\nOnly the tokens a declared role owns are emitted, so a theme that\ndeclares no roles compiles to nothing and every existing theme keeps\nits exact output. Token-level `semanticMappings` layer over the\nresult — the engine room stays available for the cases roles do not\nreach."
81620
+ "description": "Compile declared roles into semantic mappings.\n\nA theme with no roles produces no role overrides. Token-level\n`semanticMappings` take precedence over the compiled result."
81033
81621
  },
81034
81622
  {
81035
81623
  "kind": "function",
@@ -82439,7 +83027,7 @@
82439
83027
  "declarations": [
82440
83028
  {
82441
83029
  "kind": "class",
82442
- "description": "A range slider for selecting numeric values. Built entirely from\ncustom elements with ARIA `role=\"slider\"` — no native\n`<input type=\"range\">` — for consistent cross-browser styling and\nintentional touch interaction.\n\nOn touch devices, only dragging the thumb changes the value.\nTapping the track is ignored so the user can scroll the page\nwithout accidentally adjusting the slider. On desktop, clicking\nanywhere on the track jumps to that value and begins a drag.\n\nThe slider accepts an accessible name from two sources, checked\nin priority order:\n\n1. Slot content — `<esp-slider>Volume</esp-slider>` renders a\n visible label and names the slider via `aria-labelledby`.\n2. The `label` attribute — `<esp-slider label=\"Volume\">` applies\n an invisible `aria-label` when no slot content is present.\n\n```html\n<esp-box>\n <esp-slider min=\"0\" max=\"10\" step=\"0.5\" value=\"5\">\n Rating\n </esp-slider>\n <esp-slider disabled min=\"0\" max=\"100\" value=\"30\"\n label=\"Disabled slider\">\n </esp-slider>\n</esp-box>\n```",
83030
+ "description": "A numeric range slider with ARIA `role=\"slider\"`, keyboard support,\nand separate touch and pointer behavior.\n\nOn touch devices, only dragging the thumb changes the value.\nTapping the track is ignored so the user can scroll the page\nwithout accidentally adjusting the slider. On desktop, clicking\nanywhere on the track jumps to that value and begins a drag.\n\nThe slider accepts an accessible name from two sources, checked\nin priority order:\n\n1. Slot content — `<esp-slider>Volume</esp-slider>` renders a\n visible label and names the slider via `aria-labelledby`.\n2. The `label` attribute — `<esp-slider label=\"Volume\">` applies\n an invisible `aria-label` when no slot content is present.\n\n```html\n<esp-box>\n <esp-slider min=\"0\" max=\"10\" step=\"0.5\" value=\"5\">\n Rating\n </esp-slider>\n <esp-slider disabled min=\"0\" max=\"100\" value=\"30\"\n label=\"Disabled slider\">\n </esp-slider>\n</esp-box>\n```",
82443
83031
  "name": "EspalierSlider",
82444
83032
  "cssProperties": [
82445
83033
  {
@@ -82479,7 +83067,7 @@
82479
83067
  "name": "--esp-slider-focus-shadow"
82480
83068
  },
82481
83069
  {
82482
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
83070
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
82483
83071
  "name": "--esp-field-background",
82484
83072
  "inheritedFrom": {
82485
83073
  "name": "EspalierElementBase",
@@ -83841,6 +84429,17 @@
83841
84429
  "description": "Accessible label for the slider. Used as `aria-label` on the\nthumb when no slot content is present. When the default slot\nhas content, `aria-labelledby` takes precedence and this\nproperty is ignored.",
83842
84430
  "fieldName": "label"
83843
84431
  },
84432
+ {
84433
+ "type": {
84434
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
84435
+ },
84436
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
84437
+ "name": "surface-layer",
84438
+ "inheritedFrom": {
84439
+ "name": "EspalierElementBase",
84440
+ "module": "dist/shared/esp-element-base.js"
84441
+ }
84442
+ },
83844
84443
  {
83845
84444
  "name": "scheme",
83846
84445
  "type": {
@@ -83948,7 +84547,7 @@
83948
84547
  },
83949
84548
  {
83950
84549
  "kind": "class",
83951
- "description": "A vertical flow: children stack in a column with a themed gap —\n\"column, gap X\" without writing flexbox (ADR-010's micro half).\n\n```html\n<esp-stack gap=\"medium\">\n <h2>Title</h2>\n <p>Copy under it.</p>\n <esp-button label=\"Act\"></esp-button>\n</esp-stack>\n```\n\nThe default cross-axis alignment is `stretch`, matching normal block\nflow: grids, form fields, and text fill the column — with one\ndeliberate exception: slotted `esp-button` and `esp-button-group`\nkeep their natural width, because a field should fill its column\nwhile the submit button under it should not. That is the pairing a\nplain flex column cannot express with any single `align-items`\nvalue. An explicit `align` — `align=\"stretch\"` included, authored as\nan attribute or assigned to the property — governs every child\nuniformly; removing the attribute restores the default state, and\n`align-self` on any child overrides either way.",
84550
+ "description": "Stacks children vertically with a themed gap.\n\n```html\n<esp-stack gap=\"medium\">\n <h2>Title</h2>\n <p>Copy under it.</p>\n <esp-button label=\"Act\"></esp-button>\n</esp-stack>\n```\n\nThe default cross-axis alignment is `stretch`, matching normal block\nflow: grids, form fields, and text fill the column — with one\ndeliberate exception: slotted `esp-button` and `esp-button-group`\nkeep their natural width, because a field should fill its column\nwhile the submit button under it should not. That is the pairing a\nplain flex column cannot express with any single `align-items`\nvalue. An explicit `align` — `align=\"stretch\"` included, authored as\nan attribute or assigned to the property — governs every child\nuniformly; removing the attribute restores the default state, and\n`align-self` on any child overrides either way.",
83952
84551
  "name": "EspalierStack",
83953
84552
  "cssProperties": [
83954
84553
  {
@@ -83956,7 +84555,7 @@
83956
84555
  "name": "--esp-stack-gap"
83957
84556
  },
83958
84557
  {
83959
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
84558
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
83960
84559
  "name": "--esp-field-background",
83961
84560
  "inheritedFrom": {
83962
84561
  "name": "EspalierElementBase",
@@ -84793,6 +85392,17 @@
84793
85392
  "description": "Cross-axis alignment of children: `start`, `center`, `end`, or\n`stretch`.",
84794
85393
  "fieldName": "align"
84795
85394
  },
85395
+ {
85396
+ "type": {
85397
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
85398
+ },
85399
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
85400
+ "name": "surface-layer",
85401
+ "inheritedFrom": {
85402
+ "name": "EspalierElementBase",
85403
+ "module": "dist/shared/esp-element-base.js"
85404
+ }
85405
+ },
84796
85406
  {
84797
85407
  "name": "scheme",
84798
85408
  "type": {
@@ -84921,7 +85531,7 @@
84921
85531
  "default": "32ch"
84922
85532
  },
84923
85533
  {
84924
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
85534
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
84925
85535
  "name": "--esp-field-background",
84926
85536
  "inheritedFrom": {
84927
85537
  "name": "EspalierElementBase",
@@ -85924,6 +86534,17 @@
85924
86534
  "description": "Visual chrome around the status icon. `pill` renders the filled\ncircular treatment, `outline` keeps only the border, and `none`\nrenders the intent-colored icon alone.",
85925
86535
  "fieldName": "chrome"
85926
86536
  },
86537
+ {
86538
+ "type": {
86539
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
86540
+ },
86541
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
86542
+ "name": "surface-layer",
86543
+ "inheritedFrom": {
86544
+ "name": "EspalierElementBase",
86545
+ "module": "dist/shared/esp-element-base.js"
86546
+ }
86547
+ },
85927
86548
  {
85928
86549
  "name": "scheme",
85929
86550
  "type": {
@@ -86012,63 +86633,43 @@
86012
86633
  },
86013
86634
  {
86014
86635
  "kind": "javascript-module",
86015
- "path": "dist/switch/esp-switch.js",
86636
+ "path": "dist/tabs/esp-tab-group.js",
86016
86637
  "declarations": [
86017
86638
  {
86018
86639
  "kind": "class",
86019
- "description": "A toggle switch for boolean on/off values. Uses a CSS-only\ntrack-and-thumb design with a smooth sliding transition.\nFunctionally equivalent to a checkbox but visually communicates\nan immediate state change rather than a deferred selection.\n\n```html\n<esp-box>\n <esp-form-item label=\"Notifications\">\n <esp-switch value=\"notifications\">\n Enable notifications\n </esp-switch>\n </esp-form-item>\n</esp-box>\n```\n\n```html\n<esp-box>\n <esp-switch checked value=\"dark-mode\">\n Dark mode\n </esp-switch>\n <esp-switch disabled value=\"disabled-opt\">\n Disabled option\n </esp-switch>\n</esp-box>\n```\n\n### Text mode\n\nSet `mode=\"text\"` with `off-label` and `on-label` to render a\ntwo-option text toggle instead of a track and thumb. This is\nideal for binary choices like a billing-period selector:\n\n```html\n<esp-box>\n <esp-switch\n mode=\"text\"\n off-label=\"Monthly\"\n on-label=\"Annual\"\n value=\"annual\"\n >Billing period</esp-switch>\n</esp-box>\n```\n\nThe slotted text is visually hidden in text mode and folded\ninto the control's accessible label together with the current\nselected option. Screen readers will announce both the label\nand the active choice while the `off-label` / `on-label`\ntext is presented visually.",
86020
- "name": "EspalierSwitch",
86640
+ "description": "A tabbed interface that groups multiple\n[esp-tab](/components/tabs) elements. The tab strip is\ngenerated automatically from each child's `label` attribute.\nOnly one tab panel is visible at a time.\n\nA field fills one layer above the surface it sits on.\nUse `surface-layer=\"1|2|3|4\"` to select the default painted layer.\n\n```html\n<esp-tab-group>\n <esp-tab label=\"Overview\">\n <p>Overview content goes here.</p>\n </esp-tab>\n <esp-tab label=\"Details\">\n <p>Detailed information lives here.</p>\n </esp-tab>\n <esp-tab label=\"Settings\">\n <p>Configuration options.</p>\n </esp-tab>\n</esp-tab-group>\n```\n\n### Pre-selected tab\n\nSet `active` on a child `esp-tab` to make it the initially\nvisible panel:\n\n```html\n<esp-tab-group>\n <esp-tab label=\"Alpha\">\n <p>Alpha panel.</p>\n </esp-tab>\n <esp-tab label=\"Beta\" active>\n <p>Beta starts active.</p>\n </esp-tab>\n</esp-tab-group>\n```\n\n### Disabled tab\n\nIndividual tabs can be disabled:\n\n```html\n<esp-tab-group>\n <esp-tab label=\"Available\">\n <p>You can select this tab.</p>\n </esp-tab>\n <esp-tab label=\"Locked\" disabled>\n <p>This tab cannot be reached.</p>\n </esp-tab>\n <esp-tab label=\"Also available\">\n <p>This tab works too.</p>\n </esp-tab>\n</esp-tab-group>\n```\n\n### Disabled group\n\nSet `disabled` on the group to disable all tabs at once:\n\n```html\n<esp-tab-group disabled>\n <esp-tab label=\"One\">\n <p>All tabs are disabled.</p>\n </esp-tab>\n <esp-tab label=\"Two\">\n <p>Cannot switch to this.</p>\n </esp-tab>\n</esp-tab-group>\n```\n\n### Zone theming\n\nTab groups pick up zone theming from a `context` attribute:\n\n```html\n<esp-tab-group context=\"quiet\">\n <esp-tab label=\"First\">\n <p>Zone-correct content.</p>\n </esp-tab>\n <esp-tab label=\"Second\">\n <p>More content.</p>\n </esp-tab>\n</esp-tab-group>\n```\n\n### Listening for tab changes\n\n```html\n<esp-tab-group>\n <esp-tab label=\"Tab 1\">\n <p>First panel.</p>\n </esp-tab>\n <esp-tab label=\"Tab 2\">\n <p>Second panel.</p>\n </esp-tab>\n</esp-tab-group>\n<script>\n const tabs = findByTagName(\"esp-tab-group\")[0];\n tabs.addEventListener(\"esp-tab-group-changed\", (ev) => {\n console.log(\"Switched to:\", ev.detail.label, \"at index\", ev.detail.index);\n });\n</script>\n```",
86641
+ "name": "EspalierTabGroup",
86021
86642
  "cssProperties": [
86022
86643
  {
86023
- "description": "The width of the switch track. Defaults to `calc(var(--esp-size-normal-to-medium) * 1.75)`.",
86024
- "name": "--esp-switch-width"
86025
- },
86026
- {
86027
- "description": "The height of the switch track. Defaults to `var(--esp-size-normal-to-medium)`.",
86028
- "name": "--esp-switch-height"
86029
- },
86030
- {
86031
- "description": "The diameter of the thumb. Defaults to `calc(var(--esp-switch-height) - 4px)`.",
86032
- "name": "--esp-switch-thumb-size"
86033
- },
86034
- {
86035
- "description": "The track background color when the switch is off. Defaults to `var(--esp-color-border)`.",
86036
- "name": "--esp-switch-track-color"
86037
- },
86038
- {
86039
- "description": "The track background color when the switch is on. Defaults to `var(--esp-color-action-background)`.",
86040
- "name": "--esp-switch-track-color-on"
86041
- },
86042
- {
86043
- "description": "The thumb color. Defaults to `var(--esp-color-layer-1)`.",
86044
- "name": "--esp-switch-thumb-color"
86644
+ "description": "Border color of the tab group container.",
86645
+ "name": "--esp-tab-color-border"
86045
86646
  },
86046
86647
  {
86047
- "description": "The color of the focus ring shadow. Defaults to `var(--esp-color-shadow)`.",
86048
- "name": "--esp-switch-focus-shadow"
86648
+ "description": "Background color of the overall component.",
86649
+ "name": "--esp-tab-color-background"
86049
86650
  },
86050
86651
  {
86051
- "description": "Padding inside each text-mode label cell. Defaults to `var(--esp-size-tiny) var(--esp-size-small)`.",
86052
- "name": "--esp-switch-text-padding"
86652
+ "description": "Background color of the tab strip.",
86653
+ "name": "--esp-tab-color-strip-background"
86053
86654
  },
86054
86655
  {
86055
- "description": "Font size for text-mode labels. Defaults to `var(--esp-size-font)`.",
86056
- "name": "--esp-switch-text-font-size"
86656
+ "description": "Tab button hover background.",
86657
+ "name": "--esp-tab-color-button-hover"
86057
86658
  },
86058
86659
  {
86059
- "description": "Text color of the active label in text mode. Defaults to `var(--esp-color-action-text)`.",
86060
- "name": "--esp-switch-text-active-color"
86660
+ "description": "Active tab button background.",
86661
+ "name": "--esp-tab-color-button-active"
86061
86662
  },
86062
86663
  {
86063
- "description": "Text color of the inactive label in text mode. Defaults to `var(--esp-color-layer-1)`.",
86064
- "name": "--esp-switch-text-inactive-color"
86664
+ "description": "Text color for tab buttons.",
86665
+ "name": "--esp-tab-color-text"
86065
86666
  },
86066
86667
  {
86067
- "description": "Background color of the sliding highlight in text mode. Defaults to `var(--esp-color-action-background)`.",
86068
- "name": "--esp-switch-text-highlight-color"
86668
+ "description": "Padding inside the panel area and tab buttons.",
86669
+ "name": "--esp-tab-size-padding"
86069
86670
  },
86070
86671
  {
86071
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
86672
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
86072
86673
  "name": "--esp-field-background",
86073
86674
  "inheritedFrom": {
86074
86675
  "name": "EspalierElementBase",
@@ -86124,194 +86725,108 @@
86124
86725
  }
86125
86726
  }
86126
86727
  ],
86728
+ "cssParts": [
86729
+ {
86730
+ "description": "The tab strip container.",
86731
+ "name": "tab-list"
86732
+ },
86733
+ {
86734
+ "description": "Each individual tab button in the strip.",
86735
+ "name": "tab-button"
86736
+ },
86737
+ {
86738
+ "description": "The panel area below the tab strip.",
86739
+ "name": "panels"
86740
+ }
86741
+ ],
86127
86742
  "slots": [
86128
86743
  {
86129
- "description": "The label text for the switch. In default mode this renders beside the track; in text mode it is visually hidden and included in the control's accessible label.",
86744
+ "description": "Place `esp-tab` elements in the default slot.",
86130
86745
  "name": ""
86131
86746
  }
86132
86747
  ],
86133
86748
  "members": [
86134
86749
  {
86135
86750
  "kind": "field",
86136
- "name": "formAssociated",
86137
- "type": {
86138
- "text": "boolean"
86139
- },
86140
- "static": true,
86141
- "default": "true"
86142
- },
86143
- {
86144
- "kind": "field",
86145
- "name": "internals",
86751
+ "name": "panelsSlot",
86146
86752
  "privacy": "private"
86147
86753
  },
86148
86754
  {
86149
86755
  "kind": "field",
86150
- "name": "formCtrl",
86151
- "privacy": "private",
86152
- "default": "new FormFieldController({ host: this, internals: this.internals, getFormValue: () => (this.checked ? this.value || \"on\" : null), getValidity: () => { if (this.required && !this.checked) { return { flags: { valueMissing: true }, message: this.requiredMessage || \"Please toggle this switch to continue.\", }; } return null; }, onReset: () => { this.checked = false; }, onRestore: (state: string) => { this.checked = state === \"on\" || state === this.value; }, onDisabled: (isDisabled: boolean) => { this.disabled = isDisabled; }, })"
86153
- },
86154
- {
86155
- "kind": "field",
86156
- "name": "formItemDescription",
86157
- "privacy": "private",
86158
- "default": "new FormFieldDescriptionController({ host: this, getTarget: () => this.shadowRoot?.querySelector<HTMLElement>(\".switch-control\"), })"
86159
- },
86160
- {
86161
- "kind": "field",
86162
- "name": "mode",
86163
- "type": {
86164
- "text": "\"switch\" | \"text\""
86165
- },
86166
- "privacy": "public",
86167
- "default": "\"switch\"",
86168
- "description": "The visual presentation of the switch.\n- `\"switch\"` (default) renders a track-and-thumb toggle.\n- `\"text\"` renders two side-by-side text labels with a\n sliding highlight behind the active one.",
86169
- "attribute": "mode",
86170
- "reflects": true
86171
- },
86172
- {
86173
- "kind": "field",
86174
- "name": "offLabel",
86175
- "type": {
86176
- "text": "string"
86177
- },
86178
- "privacy": "public",
86179
- "default": "\"\"",
86180
- "description": "Label shown for the unchecked (off) state when\n`mode=\"text\"`.",
86181
- "attribute": "off-label"
86182
- },
86183
- {
86184
- "kind": "field",
86185
- "name": "onLabel",
86186
- "type": {
86187
- "text": "string"
86188
- },
86189
- "privacy": "public",
86190
- "default": "\"\"",
86191
- "description": "Label shown for the checked (on) state when\n`mode=\"text\"`.",
86192
- "attribute": "on-label"
86193
- },
86194
- {
86195
- "kind": "field",
86196
- "name": "checked",
86197
- "type": {
86198
- "text": "boolean"
86199
- },
86200
- "privacy": "public",
86201
- "default": "false",
86202
- "description": "Whether the switch is in the on state.",
86203
- "attribute": "checked",
86204
- "reflects": true
86205
- },
86206
- {
86207
- "kind": "field",
86208
- "name": "value",
86209
- "type": {
86210
- "text": "string"
86211
- },
86212
- "privacy": "public",
86213
- "default": "\"\"",
86214
- "description": "The value associated with this switch. Submitted with the\nform when the switch is on.",
86215
- "attribute": "value"
86216
- },
86217
- {
86218
- "kind": "field",
86219
- "name": "name",
86220
- "type": {
86221
- "text": "string"
86222
- },
86223
- "privacy": "public",
86224
- "default": "\"\"",
86225
- "description": "The name used when the switch participates in a `<form>`.",
86226
- "attribute": "name",
86227
- "reflects": true
86228
- },
86229
- {
86230
- "kind": "field",
86231
- "name": "required",
86756
+ "name": "disabled",
86232
86757
  "type": {
86233
86758
  "text": "boolean"
86234
86759
  },
86235
86760
  "privacy": "public",
86236
86761
  "default": "false",
86237
- "description": "When true, the switch must be on before the form can\nbe submitted.",
86238
- "attribute": "required",
86762
+ "description": "Disables all tabs in the group, preventing any tab from\nbeing selected.",
86763
+ "attribute": "disabled",
86239
86764
  "reflects": true
86240
86765
  },
86241
86766
  {
86242
86767
  "kind": "field",
86243
- "name": "requiredMessage",
86244
- "type": {
86245
- "text": "string"
86246
- },
86247
- "privacy": "public",
86248
- "default": "\"\"",
86249
- "description": "A custom message to display when the switch is required but\nnot on. Defaults to `\"Please toggle this switch to continue.\"`\nwhen not set.\n\n```html\n<esp-switch required required-message=\"You must accept the terms.\">\n I accept\n</esp-switch>\n```",
86250
- "attribute": "required-message"
86251
- },
86252
- {
86253
- "kind": "field",
86254
- "name": "disabled",
86768
+ "name": "tabData",
86255
86769
  "type": {
86256
- "text": "boolean"
86770
+ "text": "TabData[]"
86257
86771
  },
86258
- "privacy": "public",
86259
- "default": "false",
86260
- "description": "Controls whether the switch is disabled.",
86261
- "attribute": "disabled",
86262
- "reflects": true
86772
+ "privacy": "private",
86773
+ "default": "[]"
86263
86774
  },
86264
86775
  {
86265
86776
  "kind": "method",
86266
- "name": "focus",
86267
- "privacy": "public",
86777
+ "name": "getTabs",
86778
+ "privacy": "private",
86268
86779
  "return": {
86269
86780
  "type": {
86270
- "text": "void"
86271
- }
86272
- },
86273
- "parameters": [
86274
- {
86275
- "name": "options",
86276
- "optional": true,
86277
- "type": {
86278
- "text": "FocusOptions"
86279
- }
86781
+ "text": "EspalierTab[]"
86280
86782
  }
86281
- ],
86282
- "description": "Focus the switch control."
86783
+ }
86283
86784
  },
86284
86785
  {
86285
86786
  "kind": "method",
86286
- "name": "validate",
86287
- "privacy": "public",
86787
+ "name": "syncTabData",
86788
+ "privacy": "private",
86288
86789
  "return": {
86289
86790
  "type": {
86290
86791
  "text": "void"
86291
86792
  }
86292
- },
86293
- "description": "Re-run constraint validation and dispatch `esp-validity-changed`."
86793
+ }
86294
86794
  },
86295
86795
  {
86296
86796
  "kind": "method",
86297
- "name": "checkValidity",
86298
- "privacy": "public",
86797
+ "name": "activateTab",
86798
+ "privacy": "private",
86299
86799
  "return": {
86300
86800
  "type": {
86301
- "text": "boolean"
86801
+ "text": "void"
86302
86802
  }
86303
86803
  },
86304
- "description": "Check whether the current state is valid (delegates to ElementInternals)."
86804
+ "parameters": [
86805
+ {
86806
+ "name": "index",
86807
+ "type": {
86808
+ "text": "number"
86809
+ }
86810
+ }
86811
+ ]
86305
86812
  },
86306
86813
  {
86307
86814
  "kind": "method",
86308
- "name": "toggle",
86815
+ "name": "handleTabClick",
86309
86816
  "privacy": "private",
86310
86817
  "return": {
86311
86818
  "type": {
86312
86819
  "text": "void"
86313
86820
  }
86314
- }
86821
+ },
86822
+ "parameters": [
86823
+ {
86824
+ "name": "index",
86825
+ "type": {
86826
+ "text": "number"
86827
+ }
86828
+ }
86829
+ ]
86315
86830
  },
86316
86831
  {
86317
86832
  "kind": "method",
@@ -86333,7 +86848,7 @@
86333
86848
  },
86334
86849
  {
86335
86850
  "kind": "method",
86336
- "name": "handleLabelSlotChange",
86851
+ "name": "handleChildUpdated",
86337
86852
  "privacy": "private",
86338
86853
  "return": {
86339
86854
  "type": {
@@ -86343,69 +86858,13 @@
86343
86858
  },
86344
86859
  {
86345
86860
  "kind": "method",
86346
- "name": "getCurrentTextModeLabel",
86347
- "privacy": "private",
86348
- "return": {
86349
- "type": {
86350
- "text": "string"
86351
- }
86352
- }
86353
- },
86354
- {
86355
- "kind": "method",
86356
- "name": "getTextModeAriaLabel",
86861
+ "name": "handleSlotChange",
86357
86862
  "privacy": "private",
86358
- "return": {
86359
- "type": {
86360
- "text": "string"
86361
- }
86362
- }
86363
- },
86364
- {
86365
- "kind": "method",
86366
- "name": "formResetCallback",
86367
86863
  "return": {
86368
86864
  "type": {
86369
86865
  "text": "void"
86370
86866
  }
86371
- },
86372
- "description": "Called by the browser when the owning `<form>` is reset."
86373
- },
86374
- {
86375
- "kind": "method",
86376
- "name": "formStateRestoreCallback",
86377
- "return": {
86378
- "type": {
86379
- "text": "void"
86380
- }
86381
- },
86382
- "parameters": [
86383
- {
86384
- "name": "state",
86385
- "type": {
86386
- "text": "string"
86387
- }
86388
- }
86389
- ],
86390
- "description": "Called by the browser to restore form state (bfcache, etc.)."
86391
- },
86392
- {
86393
- "kind": "method",
86394
- "name": "formDisabledCallback",
86395
- "return": {
86396
- "type": {
86397
- "text": "void"
86398
- }
86399
- },
86400
- "parameters": [
86401
- {
86402
- "name": "isDisabled",
86403
- "type": {
86404
- "text": "boolean"
86405
- }
86406
- }
86407
- ],
86408
- "description": "Called by the browser when a parent `<fieldset>` is enabled or disabled."
86867
+ }
86409
86868
  },
86410
86869
  {
86411
86870
  "kind": "field",
@@ -87135,100 +87594,32 @@
87135
87594
  "events": [
87136
87595
  {
87137
87596
  "type": {
87138
- "text": "CustomEvent<{ checked: boolean; value: string }>"
87139
- },
87140
- "description": "Fired when the switch is toggled. The detail contains the new `checked` state and the switch `value`.",
87141
- "name": "esp-value-changed"
87142
- },
87143
- {
87144
- "type": {
87145
- "text": "CustomEvent<{ valid: boolean; message: string }>"
87597
+ "text": "CustomEvent<{ index: number; label: string }>"
87146
87598
  },
87147
- "description": "Fired whenever constraint validation runs.",
87148
- "name": "esp-validity-changed"
87599
+ "description": "Fired when the active tab changes. The detail contains the `index` and `label` of the newly active tab.",
87600
+ "name": "esp-tab-group-changed"
87149
87601
  }
87150
87602
  ],
87151
87603
  "attributes": [
87152
87604
  {
87153
- "name": "mode",
87154
- "type": {
87155
- "text": "\"switch\" | \"text\""
87156
- },
87157
- "default": "\"switch\"",
87158
- "description": "The visual presentation of the switch.\n- `\"switch\"` (default) renders a track-and-thumb toggle.\n- `\"text\"` renders two side-by-side text labels with a\n sliding highlight behind the active one.",
87159
- "fieldName": "mode"
87160
- },
87161
- {
87162
- "name": "off-label",
87163
- "type": {
87164
- "text": "string"
87165
- },
87166
- "default": "\"\"",
87167
- "description": "Label shown for the unchecked (off) state when\n`mode=\"text\"`.",
87168
- "fieldName": "offLabel"
87169
- },
87170
- {
87171
- "name": "on-label",
87172
- "type": {
87173
- "text": "string"
87174
- },
87175
- "default": "\"\"",
87176
- "description": "Label shown for the checked (on) state when\n`mode=\"text\"`.",
87177
- "fieldName": "onLabel"
87178
- },
87179
- {
87180
- "name": "checked",
87181
- "type": {
87182
- "text": "boolean"
87183
- },
87184
- "default": "false",
87185
- "description": "Whether the switch is in the on state.",
87186
- "fieldName": "checked"
87187
- },
87188
- {
87189
- "name": "value",
87190
- "type": {
87191
- "text": "string"
87192
- },
87193
- "default": "\"\"",
87194
- "description": "The value associated with this switch. Submitted with the\nform when the switch is on.",
87195
- "fieldName": "value"
87196
- },
87197
- {
87198
- "name": "name",
87199
- "type": {
87200
- "text": "string"
87201
- },
87202
- "default": "\"\"",
87203
- "description": "The name used when the switch participates in a `<form>`.",
87204
- "fieldName": "name"
87205
- },
87206
- {
87207
- "name": "required",
87605
+ "name": "disabled",
87208
87606
  "type": {
87209
87607
  "text": "boolean"
87210
87608
  },
87211
87609
  "default": "false",
87212
- "description": "When true, the switch must be on before the form can\nbe submitted.",
87213
- "fieldName": "required"
87214
- },
87215
- {
87216
- "name": "required-message",
87217
- "type": {
87218
- "text": "string"
87219
- },
87220
- "default": "\"\"",
87221
- "description": "A custom message to display when the switch is required but\nnot on. Defaults to `\"Please toggle this switch to continue.\"`\nwhen not set.\n\n```html\n<esp-switch required required-message=\"You must accept the terms.\">\n I accept\n</esp-switch>\n```",
87222
- "fieldName": "requiredMessage"
87610
+ "description": "Disables all tabs in the group, preventing any tab from\nbeing selected.",
87611
+ "fieldName": "disabled"
87223
87612
  },
87224
87613
  {
87225
- "name": "disabled",
87226
87614
  "type": {
87227
- "text": "boolean"
87615
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
87228
87616
  },
87229
- "default": "false",
87230
- "description": "Controls whether the switch is disabled.",
87231
- "fieldName": "disabled"
87617
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
87618
+ "name": "surface-layer",
87619
+ "inheritedFrom": {
87620
+ "name": "EspalierElementBase",
87621
+ "module": "dist/shared/esp-element-base.js"
87622
+ }
87232
87623
  },
87233
87624
  {
87234
87625
  "name": "scheme",
@@ -87272,88 +87663,64 @@
87272
87663
  "name": "EspalierElementBase",
87273
87664
  "module": "dist/shared/esp-element-base.js"
87274
87665
  },
87666
+ "tagName": "esp-tab-group",
87667
+ "customElement": true,
87275
87668
  "docPageTitle": {
87276
- "name": "Switch",
87277
- "description": ""
87669
+ "name": "Tab",
87670
+ "description": "Group"
87278
87671
  },
87279
87672
  "docUrl": {
87280
- "name": "/components/switch",
87673
+ "name": "/components/tabs",
87281
87674
  "description": ""
87282
87675
  },
87283
87676
  "menuGroup": {
87284
- "name": "Form",
87285
- "description": "Controls"
87677
+ "name": "Navigation",
87678
+ "description": ""
87286
87679
  },
87287
87680
  "menuLabel": {
87288
- "name": "Switch",
87289
- "description": ""
87681
+ "name": "Tab",
87682
+ "description": "Group"
87290
87683
  },
87291
87684
  "menuIcon": {
87292
- "name": "button",
87685
+ "name": "layout-navbar",
87293
87686
  "description": ""
87294
- },
87295
- "tagName": "esp-switch",
87296
- "customElement": true
87687
+ }
87297
87688
  }
87298
87689
  ],
87299
87690
  "exports": [
87300
87691
  {
87301
87692
  "kind": "js",
87302
- "name": "EspalierSwitch",
87693
+ "name": "EspalierTabGroup",
87303
87694
  "declaration": {
87304
- "name": "EspalierSwitch",
87305
- "module": "dist/switch/esp-switch.js"
87695
+ "name": "EspalierTabGroup",
87696
+ "module": "dist/tabs/esp-tab-group.js"
87306
87697
  }
87307
87698
  },
87308
87699
  {
87309
87700
  "kind": "custom-element-definition",
87310
- "name": "esp-switch",
87701
+ "name": "esp-tab-group",
87311
87702
  "declaration": {
87312
- "name": "EspalierSwitch",
87313
- "module": "dist/switch/esp-switch.js"
87703
+ "name": "EspalierTabGroup",
87704
+ "module": "dist/tabs/esp-tab-group.js"
87314
87705
  }
87315
87706
  }
87316
87707
  ]
87317
87708
  },
87318
87709
  {
87319
87710
  "kind": "javascript-module",
87320
- "path": "dist/tabs/esp-tab-group.js",
87711
+ "path": "dist/tabs/esp-tab.js",
87321
87712
  "declarations": [
87322
87713
  {
87323
87714
  "kind": "class",
87324
- "description": "A tabbed interface that groups multiple\n[esp-tab](/components/tabs) elements. The tab strip is\ngenerated automatically from each child's `label` attribute.\nOnly one tab panel is visible at a time.\n\n```html\n<esp-tab-group>\n <esp-tab label=\"Overview\">\n <p>Overview content goes here.</p>\n </esp-tab>\n <esp-tab label=\"Details\">\n <p>Detailed information lives here.</p>\n </esp-tab>\n <esp-tab label=\"Settings\">\n <p>Configuration options.</p>\n </esp-tab>\n</esp-tab-group>\n```\n\n### Pre-selected tab\n\nSet `active` on a child `esp-tab` to make it the initially\nvisible panel:\n\n```html\n<esp-tab-group>\n <esp-tab label=\"Alpha\">\n <p>Alpha panel.</p>\n </esp-tab>\n <esp-tab label=\"Beta\" active>\n <p>Beta starts active.</p>\n </esp-tab>\n</esp-tab-group>\n```\n\n### Disabled tab\n\nIndividual tabs can be disabled:\n\n```html\n<esp-tab-group>\n <esp-tab label=\"Available\">\n <p>You can select this tab.</p>\n </esp-tab>\n <esp-tab label=\"Locked\" disabled>\n <p>This tab cannot be reached.</p>\n </esp-tab>\n <esp-tab label=\"Also available\">\n <p>This tab works too.</p>\n </esp-tab>\n</esp-tab-group>\n```\n\n### Disabled group\n\nSet `disabled` on the group to disable all tabs at once:\n\n```html\n<esp-tab-group disabled>\n <esp-tab label=\"One\">\n <p>All tabs are disabled.</p>\n </esp-tab>\n <esp-tab label=\"Two\">\n <p>Cannot switch to this.</p>\n </esp-tab>\n</esp-tab-group>\n```\n\n### Zone theming\n\nTab groups pick up zone theming from a `context` attribute:\n\n```html\n<esp-tab-group context=\"quiet\">\n <esp-tab label=\"First\">\n <p>Zone-correct content.</p>\n </esp-tab>\n <esp-tab label=\"Second\">\n <p>More content.</p>\n </esp-tab>\n</esp-tab-group>\n```\n\n### Listening for tab changes\n\n```html\n<esp-tab-group>\n <esp-tab label=\"Tab 1\">\n <p>First panel.</p>\n </esp-tab>\n <esp-tab label=\"Tab 2\">\n <p>Second panel.</p>\n </esp-tab>\n</esp-tab-group>\n<script>\n const tabs = findByTagName(\"esp-tab-group\")[0];\n tabs.addEventListener(\"esp-tab-group-changed\", (ev) => {\n console.log(\"Switched to:\", ev.detail.label, \"at index\", ev.detail.index);\n });\n</script>\n```",
87325
- "name": "EspalierTabGroup",
87715
+ "description": "A single tab panel used inside\n[esp-tab-group](/components/tabs). Each tab provides a\n`label` that appears in the generated tab strip and a default\nslot for the panel content.\n\n```html\n<esp-tab-group>\n <esp-tab label=\"First\">\n <p>Content for the first tab.</p>\n </esp-tab>\n <esp-tab label=\"Second\">\n <p>Content for the second tab.</p>\n </esp-tab>\n</esp-tab-group>\n```\n\n### Pre-selected tab\n\nSet the `active` attribute to make a tab active on load:\n\n```html\n<esp-tab-group>\n <esp-tab label=\"One\">\n <p>Not active by default.</p>\n </esp-tab>\n <esp-tab label=\"Two\" active>\n <p>This tab starts active.</p>\n </esp-tab>\n</esp-tab-group>\n```\n\n### Disabled tab\n\n```html\n<esp-tab-group>\n <esp-tab label=\"Enabled\">\n <p>This tab works normally.</p>\n </esp-tab>\n <esp-tab label=\"Disabled\" disabled>\n <p>Cannot reach this panel.</p>\n </esp-tab>\n</esp-tab-group>\n```",
87716
+ "name": "EspalierTab",
87326
87717
  "cssProperties": [
87327
87718
  {
87328
- "description": "Border color of the tab group container.",
87329
- "name": "--esp-tab-color-border"
87330
- },
87331
- {
87332
- "description": "Background color of the overall component.",
87333
- "name": "--esp-tab-color-background"
87334
- },
87335
- {
87336
- "description": "Background color of the tab strip.",
87337
- "name": "--esp-tab-color-strip-background"
87338
- },
87339
- {
87340
- "description": "Tab button hover background.",
87341
- "name": "--esp-tab-color-button-hover"
87342
- },
87343
- {
87344
- "description": "Active tab button background.",
87345
- "name": "--esp-tab-color-button-active"
87346
- },
87347
- {
87348
- "description": "Text color for tab buttons.",
87349
- "name": "--esp-tab-color-text"
87350
- },
87351
- {
87352
- "description": "Padding inside the panel area and tab buttons.",
87719
+ "description": "Padding inside the panel area.",
87353
87720
  "name": "--esp-tab-size-padding"
87354
87721
  },
87355
87722
  {
87356
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
87723
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
87357
87724
  "name": "--esp-field-background",
87358
87725
  "inheritedFrom": {
87359
87726
  "name": "EspalierElementBase",
@@ -87411,144 +87778,51 @@
87411
87778
  ],
87412
87779
  "cssParts": [
87413
87780
  {
87414
- "description": "The tab strip container.",
87415
- "name": "tab-list"
87416
- },
87417
- {
87418
- "description": "Each individual tab button in the strip.",
87419
- "name": "tab-button"
87420
- },
87421
- {
87422
- "description": "The panel area below the tab strip.",
87423
- "name": "panels"
87781
+ "description": "The panel wrapper element.",
87782
+ "name": "panel"
87424
87783
  }
87425
87784
  ],
87426
87785
  "slots": [
87427
87786
  {
87428
- "description": "Place `esp-tab` elements in the default slot.",
87787
+ "description": "The default slot holds the panel content displayed when this tab is active. Label, active-state, and disabled-state updates are reported through a private coordination event consumed by the parent tab group.",
87429
87788
  "name": ""
87430
87789
  }
87431
87790
  ],
87432
87791
  "members": [
87433
87792
  {
87434
87793
  "kind": "field",
87435
- "name": "panelsSlot",
87436
- "privacy": "private"
87794
+ "name": "label",
87795
+ "type": {
87796
+ "text": "string"
87797
+ },
87798
+ "privacy": "public",
87799
+ "default": "\"\"",
87800
+ "description": "The text displayed in the tab button within the tab strip.",
87801
+ "attribute": "label"
87437
87802
  },
87438
87803
  {
87439
87804
  "kind": "field",
87440
- "name": "disabled",
87805
+ "name": "active",
87441
87806
  "type": {
87442
87807
  "text": "boolean"
87443
87808
  },
87444
87809
  "privacy": "public",
87445
87810
  "default": "false",
87446
- "description": "Disables all tabs in the group, preventing any tab from\nbeing selected.",
87447
- "attribute": "disabled",
87811
+ "description": "Whether this tab is currently active (its panel is visible).",
87812
+ "attribute": "active",
87448
87813
  "reflects": true
87449
87814
  },
87450
87815
  {
87451
87816
  "kind": "field",
87452
- "name": "tabData",
87817
+ "name": "disabled",
87453
87818
  "type": {
87454
- "text": "TabData[]"
87455
- },
87456
- "privacy": "private",
87457
- "default": "[]"
87458
- },
87459
- {
87460
- "kind": "method",
87461
- "name": "getTabs",
87462
- "privacy": "private",
87463
- "return": {
87464
- "type": {
87465
- "text": "EspalierTab[]"
87466
- }
87467
- }
87468
- },
87469
- {
87470
- "kind": "method",
87471
- "name": "syncTabData",
87472
- "privacy": "private",
87473
- "return": {
87474
- "type": {
87475
- "text": "void"
87476
- }
87477
- }
87478
- },
87479
- {
87480
- "kind": "method",
87481
- "name": "activateTab",
87482
- "privacy": "private",
87483
- "return": {
87484
- "type": {
87485
- "text": "void"
87486
- }
87487
- },
87488
- "parameters": [
87489
- {
87490
- "name": "index",
87491
- "type": {
87492
- "text": "number"
87493
- }
87494
- }
87495
- ]
87496
- },
87497
- {
87498
- "kind": "method",
87499
- "name": "handleTabClick",
87500
- "privacy": "private",
87501
- "return": {
87502
- "type": {
87503
- "text": "void"
87504
- }
87505
- },
87506
- "parameters": [
87507
- {
87508
- "name": "index",
87509
- "type": {
87510
- "text": "number"
87511
- }
87512
- }
87513
- ]
87514
- },
87515
- {
87516
- "kind": "method",
87517
- "name": "handleKeyDown",
87518
- "privacy": "private",
87519
- "return": {
87520
- "type": {
87521
- "text": "void"
87522
- }
87819
+ "text": "boolean"
87523
87820
  },
87524
- "parameters": [
87525
- {
87526
- "name": "ev",
87527
- "type": {
87528
- "text": "KeyboardEvent"
87529
- }
87530
- }
87531
- ]
87532
- },
87533
- {
87534
- "kind": "method",
87535
- "name": "handleChildUpdated",
87536
- "privacy": "private",
87537
- "return": {
87538
- "type": {
87539
- "text": "void"
87540
- }
87541
- }
87542
- },
87543
- {
87544
- "kind": "method",
87545
- "name": "handleSlotChange",
87546
- "privacy": "private",
87547
- "return": {
87548
- "type": {
87549
- "text": "void"
87550
- }
87551
- }
87821
+ "privacy": "public",
87822
+ "default": "false",
87823
+ "description": "Whether this tab is disabled (cannot be selected).",
87824
+ "attribute": "disabled",
87825
+ "reflects": true
87552
87826
  },
87553
87827
  {
87554
87828
  "kind": "field",
@@ -88275,25 +88549,46 @@
88275
88549
  }
88276
88550
  }
88277
88551
  ],
88278
- "events": [
88552
+ "events": [],
88553
+ "attributes": [
88279
88554
  {
88555
+ "name": "label",
88280
88556
  "type": {
88281
- "text": "CustomEvent<{ index: number; label: string }>"
88557
+ "text": "string"
88282
88558
  },
88283
- "description": "Fired when the active tab changes. The detail contains the `index` and `label` of the newly active tab.",
88284
- "name": "esp-tab-group-changed"
88285
- }
88286
- ],
88287
- "attributes": [
88559
+ "default": "\"\"",
88560
+ "description": "The text displayed in the tab button within the tab strip.",
88561
+ "fieldName": "label"
88562
+ },
88563
+ {
88564
+ "name": "active",
88565
+ "type": {
88566
+ "text": "boolean"
88567
+ },
88568
+ "default": "false",
88569
+ "description": "Whether this tab is currently active (its panel is visible).",
88570
+ "fieldName": "active"
88571
+ },
88288
88572
  {
88289
88573
  "name": "disabled",
88290
88574
  "type": {
88291
88575
  "text": "boolean"
88292
88576
  },
88293
88577
  "default": "false",
88294
- "description": "Disables all tabs in the group, preventing any tab from\nbeing selected.",
88578
+ "description": "Whether this tab is disabled (cannot be selected).",
88295
88579
  "fieldName": "disabled"
88296
88580
  },
88581
+ {
88582
+ "type": {
88583
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
88584
+ },
88585
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
88586
+ "name": "surface-layer",
88587
+ "inheritedFrom": {
88588
+ "name": "EspalierElementBase",
88589
+ "module": "dist/shared/esp-element-base.js"
88590
+ }
88591
+ },
88297
88592
  {
88298
88593
  "name": "scheme",
88299
88594
  "type": {
@@ -88336,64 +88631,100 @@
88336
88631
  "name": "EspalierElementBase",
88337
88632
  "module": "dist/shared/esp-element-base.js"
88338
88633
  },
88339
- "tagName": "esp-tab-group",
88634
+ "tagName": "esp-tab",
88340
88635
  "customElement": true,
88341
88636
  "docPageTitle": {
88342
88637
  "name": "Tab",
88343
- "description": "Group"
88638
+ "description": ""
88344
88639
  },
88345
88640
  "docUrl": {
88346
- "name": "/components/tabs",
88641
+ "name": "/components/tabs/tab",
88347
88642
  "description": ""
88348
88643
  },
88349
88644
  "menuGroup": {
88350
88645
  "name": "Navigation",
88351
88646
  "description": ""
88352
- },
88353
- "menuLabel": {
88354
- "name": "Tab",
88355
- "description": "Group"
88356
- },
88357
- "menuIcon": {
88358
- "name": "layout-navbar",
88359
- "description": ""
88360
88647
  }
88361
88648
  }
88362
88649
  ],
88363
88650
  "exports": [
88364
88651
  {
88365
88652
  "kind": "js",
88366
- "name": "EspalierTabGroup",
88653
+ "name": "EspalierTab",
88367
88654
  "declaration": {
88368
- "name": "EspalierTabGroup",
88369
- "module": "dist/tabs/esp-tab-group.js"
88655
+ "name": "EspalierTab",
88656
+ "module": "dist/tabs/esp-tab.js"
88370
88657
  }
88371
88658
  },
88372
88659
  {
88373
88660
  "kind": "custom-element-definition",
88374
- "name": "esp-tab-group",
88661
+ "name": "esp-tab",
88375
88662
  "declaration": {
88376
- "name": "EspalierTabGroup",
88377
- "module": "dist/tabs/esp-tab-group.js"
88663
+ "name": "EspalierTab",
88664
+ "module": "dist/tabs/esp-tab.js"
88378
88665
  }
88379
88666
  }
88380
88667
  ]
88381
88668
  },
88382
88669
  {
88383
88670
  "kind": "javascript-module",
88384
- "path": "dist/tabs/esp-tab.js",
88671
+ "path": "dist/switch/esp-switch.js",
88385
88672
  "declarations": [
88386
88673
  {
88387
88674
  "kind": "class",
88388
- "description": "A single tab panel used inside\n[esp-tab-group](/components/tabs). Each tab provides a\n`label` that appears in the generated tab strip and a default\nslot for the panel content.\n\n```html\n<esp-tab-group>\n <esp-tab label=\"First\">\n <p>Content for the first tab.</p>\n </esp-tab>\n <esp-tab label=\"Second\">\n <p>Content for the second tab.</p>\n </esp-tab>\n</esp-tab-group>\n```\n\n### Pre-selected tab\n\nSet the `active` attribute to make a tab active on load:\n\n```html\n<esp-tab-group>\n <esp-tab label=\"One\">\n <p>Not active by default.</p>\n </esp-tab>\n <esp-tab label=\"Two\" active>\n <p>This tab starts active.</p>\n </esp-tab>\n</esp-tab-group>\n```\n\n### Disabled tab\n\n```html\n<esp-tab-group>\n <esp-tab label=\"Enabled\">\n <p>This tab works normally.</p>\n </esp-tab>\n <esp-tab label=\"Disabled\" disabled>\n <p>Cannot reach this panel.</p>\n </esp-tab>\n</esp-tab-group>\n```",
88389
- "name": "EspalierTab",
88675
+ "description": "An on/off switch for settings that take effect immediately. Use a\ncheckbox when the choice should be applied with a later form submission.\n\n```html\n<esp-box>\n <esp-form-item label=\"Notifications\">\n <esp-switch value=\"notifications\">\n Enable notifications\n </esp-switch>\n </esp-form-item>\n</esp-box>\n```\n\n```html\n<esp-box>\n <esp-switch checked value=\"dark-mode\">\n Dark mode\n </esp-switch>\n <esp-switch disabled value=\"disabled-opt\">\n Disabled option\n </esp-switch>\n</esp-box>\n```\n\n### Text mode\n\nSet `mode=\"text\"` with `off-label` and `on-label` to render a\ntwo-option text toggle instead of a track and thumb. This is\nideal for binary choices like a billing-period selector:\n\n```html\n<esp-box>\n <esp-switch\n mode=\"text\"\n off-label=\"Monthly\"\n on-label=\"Annual\"\n value=\"annual\"\n >Billing period</esp-switch>\n</esp-box>\n```\n\nThe slotted text is visually hidden in text mode and folded\ninto the control's accessible label together with the current\nselected option. Screen readers will announce both the label\nand the active choice while the `off-label` / `on-label`\ntext is presented visually.",
88676
+ "name": "EspalierSwitch",
88390
88677
  "cssProperties": [
88391
88678
  {
88392
- "description": "Padding inside the panel area.",
88393
- "name": "--esp-tab-size-padding"
88679
+ "description": "The width of the switch track. Defaults to `calc(var(--esp-size-normal-to-medium) * 1.75)`.",
88680
+ "name": "--esp-switch-width"
88394
88681
  },
88395
88682
  {
88396
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
88683
+ "description": "The height of the switch track. Defaults to `var(--esp-size-normal-to-medium)`.",
88684
+ "name": "--esp-switch-height"
88685
+ },
88686
+ {
88687
+ "description": "The diameter of the thumb. Defaults to `calc(var(--esp-switch-height) - 4px)`.",
88688
+ "name": "--esp-switch-thumb-size"
88689
+ },
88690
+ {
88691
+ "description": "The track background color when the switch is off. Defaults to `var(--esp-color-border)`.",
88692
+ "name": "--esp-switch-track-color"
88693
+ },
88694
+ {
88695
+ "description": "The track background color when the switch is on. Defaults to `var(--esp-color-action-background)`.",
88696
+ "name": "--esp-switch-track-color-on"
88697
+ },
88698
+ {
88699
+ "description": "The thumb color. Defaults to `var(--esp-color-layer-1)`.",
88700
+ "name": "--esp-switch-thumb-color"
88701
+ },
88702
+ {
88703
+ "description": "The color of the focus ring shadow. Defaults to `var(--esp-color-shadow)`.",
88704
+ "name": "--esp-switch-focus-shadow"
88705
+ },
88706
+ {
88707
+ "description": "Padding inside each text-mode label cell. Defaults to `var(--esp-size-tiny) var(--esp-size-small)`.",
88708
+ "name": "--esp-switch-text-padding"
88709
+ },
88710
+ {
88711
+ "description": "Font size for text-mode labels. Defaults to `var(--esp-size-font)`.",
88712
+ "name": "--esp-switch-text-font-size"
88713
+ },
88714
+ {
88715
+ "description": "Text color of the active label in text mode. Defaults to `var(--esp-color-action-text)`.",
88716
+ "name": "--esp-switch-text-active-color"
88717
+ },
88718
+ {
88719
+ "description": "Text color of the inactive label in text mode. Defaults to `var(--esp-color-layer-1)`.",
88720
+ "name": "--esp-switch-text-inactive-color"
88721
+ },
88722
+ {
88723
+ "description": "Background color of the sliding highlight in text mode. Defaults to `var(--esp-color-action-background)`.",
88724
+ "name": "--esp-switch-text-highlight-color"
88725
+ },
88726
+ {
88727
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
88397
88728
  "name": "--esp-field-background",
88398
88729
  "inheritedFrom": {
88399
88730
  "name": "EspalierElementBase",
@@ -88449,42 +88780,131 @@
88449
88780
  }
88450
88781
  }
88451
88782
  ],
88452
- "cssParts": [
88453
- {
88454
- "description": "The panel wrapper element.",
88455
- "name": "panel"
88456
- }
88457
- ],
88458
88783
  "slots": [
88459
88784
  {
88460
- "description": "The default slot holds the panel content displayed when this tab is active. Label, active-state, and disabled-state updates are reported through a private coordination event consumed by the parent tab group.",
88785
+ "description": "The label text for the switch. In default mode this renders beside the track; in text mode it is visually hidden and included in the control's accessible label.",
88461
88786
  "name": ""
88462
88787
  }
88463
88788
  ],
88464
88789
  "members": [
88465
88790
  {
88466
88791
  "kind": "field",
88467
- "name": "label",
88792
+ "name": "formAssociated",
88793
+ "type": {
88794
+ "text": "boolean"
88795
+ },
88796
+ "static": true,
88797
+ "default": "true"
88798
+ },
88799
+ {
88800
+ "kind": "field",
88801
+ "name": "internals",
88802
+ "privacy": "private"
88803
+ },
88804
+ {
88805
+ "kind": "field",
88806
+ "name": "formCtrl",
88807
+ "privacy": "private",
88808
+ "default": "new FormFieldController({ host: this, internals: this.internals, getFormValue: () => (this.checked ? this.value || \"on\" : null), getValidity: () => { if (this.required && !this.checked) { return { flags: { valueMissing: true }, message: this.requiredMessage || \"Please toggle this switch to continue.\", }; } return null; }, onReset: () => { this.checked = false; }, onRestore: (state: string) => { this.checked = state === \"on\" || state === this.value; }, onDisabled: (isDisabled: boolean) => { this.disabled = isDisabled; }, })"
88809
+ },
88810
+ {
88811
+ "kind": "field",
88812
+ "name": "formItemDescription",
88813
+ "privacy": "private",
88814
+ "default": "new FormFieldDescriptionController({ host: this, getTarget: () => this.shadowRoot?.querySelector<HTMLElement>(\".switch-control\"), })"
88815
+ },
88816
+ {
88817
+ "kind": "field",
88818
+ "name": "mode",
88819
+ "type": {
88820
+ "text": "\"switch\" | \"text\""
88821
+ },
88822
+ "privacy": "public",
88823
+ "default": "\"switch\"",
88824
+ "description": "The visual presentation of the switch.\n- `\"switch\"` (default) renders a track-and-thumb toggle.\n- `\"text\"` renders two side-by-side text labels with a\n sliding highlight behind the active one.",
88825
+ "attribute": "mode",
88826
+ "reflects": true
88827
+ },
88828
+ {
88829
+ "kind": "field",
88830
+ "name": "offLabel",
88468
88831
  "type": {
88469
88832
  "text": "string"
88470
88833
  },
88471
88834
  "privacy": "public",
88472
88835
  "default": "\"\"",
88473
- "description": "The text displayed in the tab button within the tab strip.",
88474
- "attribute": "label"
88836
+ "description": "Label shown for the unchecked (off) state when\n`mode=\"text\"`.",
88837
+ "attribute": "off-label"
88475
88838
  },
88476
88839
  {
88477
88840
  "kind": "field",
88478
- "name": "active",
88841
+ "name": "onLabel",
88842
+ "type": {
88843
+ "text": "string"
88844
+ },
88845
+ "privacy": "public",
88846
+ "default": "\"\"",
88847
+ "description": "Label shown for the checked (on) state when\n`mode=\"text\"`.",
88848
+ "attribute": "on-label"
88849
+ },
88850
+ {
88851
+ "kind": "field",
88852
+ "name": "checked",
88479
88853
  "type": {
88480
88854
  "text": "boolean"
88481
88855
  },
88482
88856
  "privacy": "public",
88483
88857
  "default": "false",
88484
- "description": "Whether this tab is currently active (its panel is visible).",
88485
- "attribute": "active",
88858
+ "description": "Whether the switch is in the on state.",
88859
+ "attribute": "checked",
88860
+ "reflects": true
88861
+ },
88862
+ {
88863
+ "kind": "field",
88864
+ "name": "value",
88865
+ "type": {
88866
+ "text": "string"
88867
+ },
88868
+ "privacy": "public",
88869
+ "default": "\"\"",
88870
+ "description": "The value associated with this switch. Submitted with the\nform when the switch is on.",
88871
+ "attribute": "value"
88872
+ },
88873
+ {
88874
+ "kind": "field",
88875
+ "name": "name",
88876
+ "type": {
88877
+ "text": "string"
88878
+ },
88879
+ "privacy": "public",
88880
+ "default": "\"\"",
88881
+ "description": "The name used when the switch participates in a `<form>`.",
88882
+ "attribute": "name",
88486
88883
  "reflects": true
88487
88884
  },
88885
+ {
88886
+ "kind": "field",
88887
+ "name": "required",
88888
+ "type": {
88889
+ "text": "boolean"
88890
+ },
88891
+ "privacy": "public",
88892
+ "default": "false",
88893
+ "description": "When true, the switch must be on before the form can\nbe submitted.",
88894
+ "attribute": "required",
88895
+ "reflects": true
88896
+ },
88897
+ {
88898
+ "kind": "field",
88899
+ "name": "requiredMessage",
88900
+ "type": {
88901
+ "text": "string"
88902
+ },
88903
+ "privacy": "public",
88904
+ "default": "\"\"",
88905
+ "description": "A custom message to display when the switch is required but\nnot on. Defaults to `\"Please toggle this switch to continue.\"`\nwhen not set.\n\n```html\n<esp-switch required required-message=\"You must accept the terms.\">\n I accept\n</esp-switch>\n```",
88906
+ "attribute": "required-message"
88907
+ },
88488
88908
  {
88489
88909
  "kind": "field",
88490
88910
  "name": "disabled",
@@ -88493,10 +88913,156 @@
88493
88913
  },
88494
88914
  "privacy": "public",
88495
88915
  "default": "false",
88496
- "description": "Whether this tab is disabled (cannot be selected).",
88916
+ "description": "Controls whether the switch is disabled.",
88497
88917
  "attribute": "disabled",
88498
88918
  "reflects": true
88499
88919
  },
88920
+ {
88921
+ "kind": "method",
88922
+ "name": "focus",
88923
+ "privacy": "public",
88924
+ "return": {
88925
+ "type": {
88926
+ "text": "void"
88927
+ }
88928
+ },
88929
+ "parameters": [
88930
+ {
88931
+ "name": "options",
88932
+ "optional": true,
88933
+ "type": {
88934
+ "text": "FocusOptions"
88935
+ }
88936
+ }
88937
+ ],
88938
+ "description": "Focus the switch control."
88939
+ },
88940
+ {
88941
+ "kind": "method",
88942
+ "name": "validate",
88943
+ "privacy": "public",
88944
+ "return": {
88945
+ "type": {
88946
+ "text": "void"
88947
+ }
88948
+ },
88949
+ "description": "Re-run constraint validation and dispatch `esp-validity-changed`."
88950
+ },
88951
+ {
88952
+ "kind": "method",
88953
+ "name": "checkValidity",
88954
+ "privacy": "public",
88955
+ "return": {
88956
+ "type": {
88957
+ "text": "boolean"
88958
+ }
88959
+ },
88960
+ "description": "Check whether the current state is valid (delegates to ElementInternals)."
88961
+ },
88962
+ {
88963
+ "kind": "method",
88964
+ "name": "toggle",
88965
+ "privacy": "private",
88966
+ "return": {
88967
+ "type": {
88968
+ "text": "void"
88969
+ }
88970
+ }
88971
+ },
88972
+ {
88973
+ "kind": "method",
88974
+ "name": "handleKeyDown",
88975
+ "privacy": "private",
88976
+ "return": {
88977
+ "type": {
88978
+ "text": "void"
88979
+ }
88980
+ },
88981
+ "parameters": [
88982
+ {
88983
+ "name": "ev",
88984
+ "type": {
88985
+ "text": "KeyboardEvent"
88986
+ }
88987
+ }
88988
+ ]
88989
+ },
88990
+ {
88991
+ "kind": "method",
88992
+ "name": "handleLabelSlotChange",
88993
+ "privacy": "private",
88994
+ "return": {
88995
+ "type": {
88996
+ "text": "void"
88997
+ }
88998
+ }
88999
+ },
89000
+ {
89001
+ "kind": "method",
89002
+ "name": "getCurrentTextModeLabel",
89003
+ "privacy": "private",
89004
+ "return": {
89005
+ "type": {
89006
+ "text": "string"
89007
+ }
89008
+ }
89009
+ },
89010
+ {
89011
+ "kind": "method",
89012
+ "name": "getTextModeAriaLabel",
89013
+ "privacy": "private",
89014
+ "return": {
89015
+ "type": {
89016
+ "text": "string"
89017
+ }
89018
+ }
89019
+ },
89020
+ {
89021
+ "kind": "method",
89022
+ "name": "formResetCallback",
89023
+ "return": {
89024
+ "type": {
89025
+ "text": "void"
89026
+ }
89027
+ },
89028
+ "description": "Called by the browser when the owning `<form>` is reset."
89029
+ },
89030
+ {
89031
+ "kind": "method",
89032
+ "name": "formStateRestoreCallback",
89033
+ "return": {
89034
+ "type": {
89035
+ "text": "void"
89036
+ }
89037
+ },
89038
+ "parameters": [
89039
+ {
89040
+ "name": "state",
89041
+ "type": {
89042
+ "text": "string"
89043
+ }
89044
+ }
89045
+ ],
89046
+ "description": "Called by the browser to restore form state (bfcache, etc.)."
89047
+ },
89048
+ {
89049
+ "kind": "method",
89050
+ "name": "formDisabledCallback",
89051
+ "return": {
89052
+ "type": {
89053
+ "text": "void"
89054
+ }
89055
+ },
89056
+ "parameters": [
89057
+ {
89058
+ "name": "isDisabled",
89059
+ "type": {
89060
+ "text": "boolean"
89061
+ }
89062
+ }
89063
+ ],
89064
+ "description": "Called by the browser when a parent `<fieldset>` is enabled or disabled."
89065
+ },
88500
89066
  {
88501
89067
  "kind": "field",
88502
89068
  "name": "seedColorBacker",
@@ -89222,25 +89788,94 @@
89222
89788
  }
89223
89789
  }
89224
89790
  ],
89225
- "events": [],
89791
+ "events": [
89792
+ {
89793
+ "type": {
89794
+ "text": "CustomEvent<{ checked: boolean; value: string }>"
89795
+ },
89796
+ "description": "Fired when the switch is toggled. The detail contains the new `checked` state and the switch `value`.",
89797
+ "name": "esp-value-changed"
89798
+ },
89799
+ {
89800
+ "type": {
89801
+ "text": "CustomEvent<{ valid: boolean; message: string }>"
89802
+ },
89803
+ "description": "Fired whenever constraint validation runs.",
89804
+ "name": "esp-validity-changed"
89805
+ }
89806
+ ],
89226
89807
  "attributes": [
89227
89808
  {
89228
- "name": "label",
89809
+ "name": "mode",
89810
+ "type": {
89811
+ "text": "\"switch\" | \"text\""
89812
+ },
89813
+ "default": "\"switch\"",
89814
+ "description": "The visual presentation of the switch.\n- `\"switch\"` (default) renders a track-and-thumb toggle.\n- `\"text\"` renders two side-by-side text labels with a\n sliding highlight behind the active one.",
89815
+ "fieldName": "mode"
89816
+ },
89817
+ {
89818
+ "name": "off-label",
89229
89819
  "type": {
89230
89820
  "text": "string"
89231
89821
  },
89232
89822
  "default": "\"\"",
89233
- "description": "The text displayed in the tab button within the tab strip.",
89234
- "fieldName": "label"
89823
+ "description": "Label shown for the unchecked (off) state when\n`mode=\"text\"`.",
89824
+ "fieldName": "offLabel"
89235
89825
  },
89236
89826
  {
89237
- "name": "active",
89827
+ "name": "on-label",
89828
+ "type": {
89829
+ "text": "string"
89830
+ },
89831
+ "default": "\"\"",
89832
+ "description": "Label shown for the checked (on) state when\n`mode=\"text\"`.",
89833
+ "fieldName": "onLabel"
89834
+ },
89835
+ {
89836
+ "name": "checked",
89238
89837
  "type": {
89239
89838
  "text": "boolean"
89240
89839
  },
89241
89840
  "default": "false",
89242
- "description": "Whether this tab is currently active (its panel is visible).",
89243
- "fieldName": "active"
89841
+ "description": "Whether the switch is in the on state.",
89842
+ "fieldName": "checked"
89843
+ },
89844
+ {
89845
+ "name": "value",
89846
+ "type": {
89847
+ "text": "string"
89848
+ },
89849
+ "default": "\"\"",
89850
+ "description": "The value associated with this switch. Submitted with the\nform when the switch is on.",
89851
+ "fieldName": "value"
89852
+ },
89853
+ {
89854
+ "name": "name",
89855
+ "type": {
89856
+ "text": "string"
89857
+ },
89858
+ "default": "\"\"",
89859
+ "description": "The name used when the switch participates in a `<form>`.",
89860
+ "fieldName": "name"
89861
+ },
89862
+ {
89863
+ "name": "required",
89864
+ "type": {
89865
+ "text": "boolean"
89866
+ },
89867
+ "default": "false",
89868
+ "description": "When true, the switch must be on before the form can\nbe submitted.",
89869
+ "fieldName": "required"
89870
+ },
89871
+ {
89872
+ "name": "required-message",
89873
+ "type": {
89874
+ "text": "string"
89875
+ },
89876
+ "default": "\"\"",
89877
+ "description": "A custom message to display when the switch is required but\nnot on. Defaults to `\"Please toggle this switch to continue.\"`\nwhen not set.\n\n```html\n<esp-switch required required-message=\"You must accept the terms.\">\n I accept\n</esp-switch>\n```",
89878
+ "fieldName": "requiredMessage"
89244
89879
  },
89245
89880
  {
89246
89881
  "name": "disabled",
@@ -89248,9 +89883,20 @@
89248
89883
  "text": "boolean"
89249
89884
  },
89250
89885
  "default": "false",
89251
- "description": "Whether this tab is disabled (cannot be selected).",
89886
+ "description": "Controls whether the switch is disabled.",
89252
89887
  "fieldName": "disabled"
89253
89888
  },
89889
+ {
89890
+ "type": {
89891
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
89892
+ },
89893
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
89894
+ "name": "surface-layer",
89895
+ "inheritedFrom": {
89896
+ "name": "EspalierElementBase",
89897
+ "module": "dist/shared/esp-element-base.js"
89898
+ }
89899
+ },
89254
89900
  {
89255
89901
  "name": "scheme",
89256
89902
  "type": {
@@ -89293,37 +89939,45 @@
89293
89939
  "name": "EspalierElementBase",
89294
89940
  "module": "dist/shared/esp-element-base.js"
89295
89941
  },
89296
- "tagName": "esp-tab",
89297
- "customElement": true,
89298
89942
  "docPageTitle": {
89299
- "name": "Tab",
89943
+ "name": "Switch",
89300
89944
  "description": ""
89301
89945
  },
89302
89946
  "docUrl": {
89303
- "name": "/components/tabs/tab",
89947
+ "name": "/components/switch",
89304
89948
  "description": ""
89305
89949
  },
89306
89950
  "menuGroup": {
89307
- "name": "Navigation",
89951
+ "name": "Form",
89952
+ "description": "Controls"
89953
+ },
89954
+ "menuLabel": {
89955
+ "name": "Switch",
89308
89956
  "description": ""
89309
- }
89957
+ },
89958
+ "menuIcon": {
89959
+ "name": "button",
89960
+ "description": ""
89961
+ },
89962
+ "tagName": "esp-switch",
89963
+ "customElement": true
89310
89964
  }
89311
89965
  ],
89312
89966
  "exports": [
89313
89967
  {
89314
89968
  "kind": "js",
89315
- "name": "EspalierTab",
89969
+ "name": "EspalierSwitch",
89316
89970
  "declaration": {
89317
- "name": "EspalierTab",
89318
- "module": "dist/tabs/esp-tab.js"
89971
+ "name": "EspalierSwitch",
89972
+ "module": "dist/switch/esp-switch.js"
89319
89973
  }
89320
89974
  },
89321
89975
  {
89322
89976
  "kind": "custom-element-definition",
89323
- "name": "esp-tab",
89977
+ "name": "esp-switch",
89324
89978
  "declaration": {
89325
- "name": "EspalierTab",
89326
- "module": "dist/tabs/esp-tab.js"
89979
+ "name": "EspalierSwitch",
89980
+ "module": "dist/switch/esp-switch.js"
89327
89981
  }
89328
89982
  }
89329
89983
  ]
@@ -90502,6 +91156,17 @@
90502
91156
  "description": "Maximum number of characters.",
90503
91157
  "fieldName": "maxlength"
90504
91158
  },
91159
+ {
91160
+ "type": {
91161
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
91162
+ },
91163
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
91164
+ "name": "surface-layer",
91165
+ "inheritedFrom": {
91166
+ "name": "EspalierElementBase",
91167
+ "module": "dist/shared/esp-element-base.js"
91168
+ }
91169
+ },
90505
91170
  {
90506
91171
  "name": "scheme",
90507
91172
  "type": {
@@ -90568,7 +91233,7 @@
90568
91233
  },
90569
91234
  "cssProperties": [
90570
91235
  {
90571
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
91236
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
90572
91237
  "name": "--esp-field-background",
90573
91238
  "inheritedFrom": {
90574
91239
  "name": "EspalierElementBase",
@@ -90667,7 +91332,7 @@
90667
91332
  "name": "--esp-toaster-padding"
90668
91333
  },
90669
91334
  {
90670
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
91335
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
90671
91336
  "name": "--esp-field-background",
90672
91337
  "inheritedFrom": {
90673
91338
  "name": "EspalierElementBase",
@@ -91631,6 +92296,17 @@
91631
92296
  "tagName": "esp-toaster",
91632
92297
  "customElement": true,
91633
92298
  "attributes": [
92299
+ {
92300
+ "type": {
92301
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
92302
+ },
92303
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
92304
+ "name": "surface-layer",
92305
+ "inheritedFrom": {
92306
+ "name": "EspalierElementBase",
92307
+ "module": "dist/shared/esp-element-base.js"
92308
+ }
92309
+ },
91634
92310
  {
91635
92311
  "name": "scheme",
91636
92312
  "type": {
@@ -91737,7 +92413,7 @@
91737
92413
  "name": "--esp-tooltip-focus-outline"
91738
92414
  },
91739
92415
  {
91740
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
92416
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
91741
92417
  "name": "--esp-field-background",
91742
92418
  "inheritedFrom": {
91743
92419
  "name": "EspalierElementBase",
@@ -92743,6 +93419,17 @@
92743
93419
  "description": "Tabindex applied to the tooltip trigger span.\n\nLeave it unset to get the right default automatically: a tooltip\nwrapping a focusable button (`<button>` or `<esp-button>`) takes its\nwrapper span out of the tab order (`-1`) so it does not add a second,\nredundant tab stop, while a tooltip wrapping non-interactive content\n(an icon, an abbreviation, plain text) keeps the wrapper focusable\n(`0`) so keyboard users can still reveal it on focus.\n\nSet it explicitly to override that auto-detection — for example\n`trigger-tabindex=\"0\"` to force a focusable wrapper, or\n`trigger-tabindex=\"-1\"` to opt a non-button trigger out. An explicit\nvalue always wins; a non-numeric or `NaN` value falls back to `0`.\n\nWhen the wrapper leaves the tab order, keyboard focus lands on the\nslotted control instead, so the tooltip's `aria-describedby` is\nmirrored onto that control and assistive tech still announces the\ndescription on focus.",
92744
93420
  "fieldName": "triggerTabindex"
92745
93421
  },
93422
+ {
93423
+ "type": {
93424
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
93425
+ },
93426
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
93427
+ "name": "surface-layer",
93428
+ "inheritedFrom": {
93429
+ "name": "EspalierElementBase",
93430
+ "module": "dist/shared/esp-element-base.js"
93431
+ }
93432
+ },
92746
93433
  {
92747
93434
  "name": "scheme",
92748
93435
  "type": {
@@ -93794,6 +94481,17 @@
93794
94481
  "description": "Marks a branch whose children are lazy-loaded by the tree's\n`loadChildren` callback on first expand.",
93795
94482
  "fieldName": "hasChildren"
93796
94483
  },
94484
+ {
94485
+ "type": {
94486
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
94487
+ },
94488
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
94489
+ "name": "surface-layer",
94490
+ "inheritedFrom": {
94491
+ "name": "EspalierElementBase",
94492
+ "module": "dist/shared/esp-element-base.js"
94493
+ }
94494
+ },
93797
94495
  {
93798
94496
  "name": "scheme",
93799
94497
  "type": {
@@ -93856,7 +94554,7 @@
93856
94554
  "customElement": true,
93857
94555
  "cssProperties": [
93858
94556
  {
93859
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
94557
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
93860
94558
  "name": "--esp-field-background",
93861
94559
  "inheritedFrom": {
93862
94560
  "name": "EspalierElementBase",
@@ -93975,7 +94673,7 @@
93975
94673
  },
93976
94674
  {
93977
94675
  "kind": "class",
93978
- "description": "A form-associated hierarchy primitive for displaying and selecting\nfrom a slug-structured tree — a folder/path picker, a category\nchooser, or any place a flat field should become an expandable tree.\nFor static declarative node definitions, see\n[`<esp-tree-item>`](/components/tree/item/).\n\nThe name is apt: an espalier is a tree trained flat against a frame,\nso the visual language — branches trained to vertical guide wires,\ngrafting a new shoot onto an existing limb — is the component's own\nmetaphor.\n\n## Data-driven first, composition-capable\n\nBind a hierarchy to the `nodes` property for the primary, data-driven\nAPI. For static trees, nest\n[`<esp-tree-item>`](/components/tree/item) children instead — they are\nparsed into the same model, so the keyboard and accessibility model is\nidentical either way. When both are supplied, `nodes` wins. Data-driven\nnodes must provide stable, unique `id` values; the tree keys expansion,\nlazy loading, focus, and host re-feeds by `id` so relabeling or moving a\nnode can change its path without changing its identity.\n\n```html\n<esp-form-item label=\"Folder\">\n <esp-tree name=\"folder\" required></esp-tree>\n</esp-form-item>\n<script>\n const tree = findByTagName(\"esp-tree\")[0];\n tree.nodes = [\n { id: \"weddings\", segment: \"weddings\", label: \"Weddings\", children: [\n { id: \"w2026\", segment: \"2026\", label: \"2026\" },\n ] },\n { id: \"portraits\", segment: \"portraits\", label: \"Portraits\" },\n ];\n</script>\n```\n\nNodes may also carry opaque host data. The tree never reads into `data`;\nit carries the value through select, edit, toggle, and graft events so a\nhost can round-trip its own model without a side lookup.\n\n```html\n<esp-tree id=\"navigation\"></esp-tree>\n<script>\n const tree = findById(\"navigation\");\n tree.nodes = [\n {\n id: \"nav-about\",\n segment: \"about\",\n label: \"About Us\",\n data: { target: { kind: \"page\", pageId: \"about-us\" } },\n editable: true,\n },\n ];\n tree.addEventListener(\"esp-tree-select\", (event) => {\n console.log(event.detail.node.id, event.detail.node.data);\n });\n</script>\n```\n\n## Selection and form value\n\nSingle-select for v1. Clicking a selectable row selects it; selecting\na branch opens it, and clicking an already-selected branch toggles it\nopen or closed. Selecting a row outside the open selected branch\ncollapses that branch unless `keep-expanded` is set. Branches can also\nexpand and collapse through the leading icon button or with keyboard:\n`ArrowRight` expands closed branches first, then focuses a row's\ntrailing action button when one is present; `ArrowLeft` collapses or\nmoves to the parent. `Enter` and `Space` select the focused row. The\nsubmitted form value is the selected node's full path\n(defaulting to root-inclusive web paths like `/weddings/2026`), or\n`null` when nothing is selected. The root row is always present,\ndefaults to label and path `/`, and is selectable by default; set\n`root-selectable=\"false\"` when the root should be navigation-only.\nValidity reflects the committed selection through the standard\nEspalier form flow.\n\n## Lazy loading\n\nMark a branch with `hasChildren` and set the `loadChildren` callback to\nfetch its children on first expand. Expansion state is stable across\nre-renders and reloads.\n\n```html\n<esp-tree id=\"lazy\"></esp-tree>\n<script>\n const tree = findById(\"lazy\");\n tree.nodes = [{ id: \"root\", segment: \"library\", label: \"Library\", hasChildren: true }];\n tree.loadChildren = async () => {\n const res = await fetch(\"/assets/data/tree-library-children.json\");\n return await res.json();\n };\n</script>\n```\n\n## Grafting a new branch\n\nSet `allow-graft` to expose inline `+` affordances at the level where\na new child would appear. Clicking one opens the built-in graft\ndialog, which shows the parent and resulting path while the value is\nnormalized to a valid web-path segment. Enter commits, Escape\ncancels. Keyboard users can press `Insert` on the focused row to add\nbeneath it. Newly grafted branches are ephemeral client state the\nhost reads back via events — the component persists nothing.\n\n```html\n<esp-tree id=\"graftable\" allow-graft></esp-tree>\n<script>\n const tree = findById(\"graftable\");\n tree.nodes = [\n { id: \"weddings\", segment: \"weddings\", label: \"Weddings\", children: [\n { id: \"w2026\", segment: \"2026\", label: \"2026\" },\n ] },\n { id: \"portraits\", segment: \"portraits\", label: \"Portraits\" },\n ];\n tree.addEventListener(\"esp-tree-graft\", (e) => {\n console.log(\"grafted\", e.detail.path);\n });\n</script>\n```\n\nAs the input changes, the tree emits a live `esp-tree-graft-input`\nevent. A consumer can run an async check the offline component cannot\n(most importantly a server-side duplicate not present in the loaded\nchildren) and push the result back with `setGraftError(message)`; the\ntree then shows it inline and blocks the commit until it is cleared.\nFor advanced add flows, listen for the cancelable\n`esp-tree-graft-request` event and call `preventDefault()` to skip\nthe default dialog.\n\nReplace the `buildPath` property to use non-web path semantics, and\nreplace `normalizeGraftSegment` to loosen, tighten, or completely\nskip the default web-slug normalization used by the graft dialog.\n\n## Node actions\n\nMark a node with `editable: true` to render an edit action on the\ntrailing side of the row. Clicking it, or pressing `F2` on the\nfocused row, emits `esp-tree-edit` with the node path and public node\ndata so the host can open its own editor. Optional flags\n(`deletable`, `movableUp`, `movableDown`, `cuttable`, and\n`pasteChildTarget`) expose the matching built-in editing actions for\nhost-owned tree workflows.\n\nIf a row has exactly one action, the tree renders one trailing\nicon-only `esp-button` with an accessible label such as `Edit About`.\nIf a row has more than one action, the actions collapse into one\ntrailing `esp-action-menu` trigger so rows never accumulate multiple\nvisible action buttons. The tree only emits events — it does not\ndelete, move, cut, paste, mutate source nodes, or own clipboard state.\n\n```html\n<esp-tree id=\"navigation\"></esp-tree>\n<script>\n const tree = findById(\"navigation\");\n tree.nodes = [\n {\n id: \"about\",\n segment: \"about\",\n label: \"About\",\n editable: true,\n deletable: true,\n movableDown: true,\n cuttable: true,\n children: [\n {\n id: \"team\",\n segment: \"team\",\n label: \"Team\",\n editable: true,\n },\n ],\n },\n {\n id: \"services\",\n segment: \"services\",\n label: \"Services\",\n pasteChildTarget: true,\n },\n ];\n tree.addEventListener(\"esp-tree-cut\", (event) => {\n console.log(\"cut requested\", event.detail.path);\n });\n tree.addEventListener(\"esp-tree-paste-child\", (event) => {\n console.log(\"paste as child requested\", event.detail.path);\n });\n</script>\n```\n\n## Expand/collapse icon\n\nThe expand/collapse caret ships inline with the component, so unlike\nnode `icon`s it needs no icon sprite to be hosted. Override it by\nsetting the `expandIcon` property to your own (trusted) SVG markup; it\nrenders on every branch toggle and is rotated 90° when the branch is\nopen, so provide a right-pointing icon. For icon pairs that should not\nrotate, set `collapseIcon` to the trusted SVG markup to use while the\nbranch is open.\n\n```html\n<esp-tree id=\"custom-caret\"></esp-tree>\n<script>\n const tree = findById(\"custom-caret\");\n tree.nodes = [\n { id: \"weddings\", segment: \"weddings\", label: \"Weddings\", children: [\n { id: \"w2026\", segment: \"2026\", label: \"2026\" },\n ] },\n ];\n // A filled triangle caret instead of the default outline chevron.\n tree.expandIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"\n fill=\"currentColor\" stroke=\"none\"><path d=\"M9 6l6 6l-6 6z\" /></svg>`;\n</script>\n```",
94676
+ "description": "A form-associated hierarchy primitive for displaying and selecting\nfrom a slug-structured tree — a folder/path picker, a category\nchooser, or any place a flat field should become an expandable tree.\nFor static declarative node definitions, see\n[`<esp-tree-item>`](/components/tree/item/).\n\n## Data-driven first, composition-capable\n\nBind a hierarchy to the `nodes` property for the primary, data-driven\nAPI. For static trees, nest\n[`<esp-tree-item>`](/components/tree/item) children instead — they are\nparsed into the same model, so the keyboard and accessibility model is\nidentical either way. When both are supplied, `nodes` wins. Data-driven\nnodes must provide stable, unique `id` values; the tree keys expansion,\nlazy loading, focus, and host re-feeds by `id` so relabeling or moving a\nnode can change its path without changing its identity.\n\n```html\n<esp-form-item label=\"Folder\">\n <esp-tree name=\"folder\" required></esp-tree>\n</esp-form-item>\n<script>\n const tree = findByTagName(\"esp-tree\")[0];\n tree.nodes = [\n { id: \"weddings\", segment: \"weddings\", label: \"Weddings\", children: [\n { id: \"w2026\", segment: \"2026\", label: \"2026\" },\n ] },\n { id: \"portraits\", segment: \"portraits\", label: \"Portraits\" },\n ];\n</script>\n```\n\nNodes may also carry opaque host data. The tree never reads into `data`;\nit carries the value through select, edit, toggle, and graft events so a\nhost can round-trip its own model without a side lookup.\n\n```html\n<esp-tree id=\"navigation\"></esp-tree>\n<script>\n const tree = findById(\"navigation\");\n tree.nodes = [\n {\n id: \"nav-about\",\n segment: \"about\",\n label: \"About Us\",\n data: { target: { kind: \"page\", pageId: \"about-us\" } },\n editable: true,\n },\n ];\n tree.addEventListener(\"esp-tree-select\", (event) => {\n console.log(event.detail.node.id, event.detail.node.data);\n });\n</script>\n```\n\n## Selection and form value\n\nSingle-select for v1. Clicking a selectable row selects it; selecting\na branch opens it, and clicking an already-selected branch toggles it\nopen or closed. Selecting a row outside the open selected branch\ncollapses that branch unless `keep-expanded` is set. Branches can also\nexpand and collapse through the leading icon button or with keyboard:\n`ArrowRight` expands closed branches first, then focuses a row's\ntrailing action button when one is present; `ArrowLeft` collapses or\nmoves to the parent. `Enter` and `Space` select the focused row. The\nsubmitted form value is the selected node's full path\n(defaulting to root-inclusive web paths like `/weddings/2026`), or\n`null` when nothing is selected. The root row is always present,\ndefaults to label and path `/`, and is selectable by default; set\n`root-selectable=\"false\"` when the root should be navigation-only.\nValidity reflects the committed selection through the standard\nEspalier form flow.\n\n## Lazy loading\n\nMark a branch with `hasChildren` and set the `loadChildren` callback to\nfetch its children on first expand. Expansion state is stable across\nre-renders and reloads.\n\n```html\n<esp-tree id=\"lazy\"></esp-tree>\n<script>\n const tree = findById(\"lazy\");\n tree.nodes = [{ id: \"root\", segment: \"library\", label: \"Library\", hasChildren: true }];\n tree.loadChildren = async () => {\n const res = await fetch(\"/assets/data/tree-library-children.json\");\n return await res.json();\n };\n</script>\n```\n\n## Grafting a new branch\n\nSet `allow-graft` to expose inline `+` affordances at the level where\na new child would appear. Clicking one opens the built-in graft\ndialog, which shows the parent and resulting path while the value is\nnormalized to a valid web-path segment. Enter commits, Escape\ncancels. Keyboard users can press `Insert` on the focused row to add\nbeneath it. Newly grafted branches are ephemeral client state the\nhost reads back via events — the component persists nothing.\n\n```html\n<esp-tree id=\"graftable\" allow-graft></esp-tree>\n<script>\n const tree = findById(\"graftable\");\n tree.nodes = [\n { id: \"weddings\", segment: \"weddings\", label: \"Weddings\", children: [\n { id: \"w2026\", segment: \"2026\", label: \"2026\" },\n ] },\n { id: \"portraits\", segment: \"portraits\", label: \"Portraits\" },\n ];\n tree.addEventListener(\"esp-tree-graft\", (e) => {\n console.log(\"grafted\", e.detail.path);\n });\n</script>\n```\n\nAs the input changes, the tree emits a live `esp-tree-graft-input`\nevent. A consumer can run an async check the offline component cannot\n(most importantly a server-side duplicate not present in the loaded\nchildren) and push the result back with `setGraftError(message)`; the\ntree then shows it inline and blocks the commit until it is cleared.\nFor advanced add flows, listen for the cancelable\n`esp-tree-graft-request` event and call `preventDefault()` to skip\nthe default dialog.\n\nReplace the `buildPath` property to use non-web path semantics, and\nreplace `normalizeGraftSegment` to loosen, tighten, or completely\nskip the default web-slug normalization used by the graft dialog.\n\n## Node actions\n\nMark a node with `editable: true` to render an edit action on the\ntrailing side of the row. Clicking it, or pressing `F2` on the\nfocused row, emits `esp-tree-edit` with the node path and public node\ndata so the host can open its own editor. Optional flags\n(`deletable`, `movableUp`, `movableDown`, `cuttable`, and\n`pasteChildTarget`) expose the matching built-in editing actions for\nhost-owned tree workflows.\n\nIf a row has exactly one action, the tree renders one trailing\nicon-only `esp-button` with an accessible label such as `Edit About`.\nIf a row has more than one action, the actions collapse into one\ntrailing `esp-action-menu` trigger so rows never accumulate multiple\nvisible action buttons. The tree only emits events — it does not\ndelete, move, cut, paste, mutate source nodes, or own clipboard state.\n\n```html\n<esp-tree id=\"navigation\"></esp-tree>\n<script>\n const tree = findById(\"navigation\");\n tree.nodes = [\n {\n id: \"about\",\n segment: \"about\",\n label: \"About\",\n editable: true,\n deletable: true,\n movableDown: true,\n cuttable: true,\n children: [\n {\n id: \"team\",\n segment: \"team\",\n label: \"Team\",\n editable: true,\n },\n ],\n },\n {\n id: \"services\",\n segment: \"services\",\n label: \"Services\",\n pasteChildTarget: true,\n },\n ];\n tree.addEventListener(\"esp-tree-cut\", (event) => {\n console.log(\"cut requested\", event.detail.path);\n });\n tree.addEventListener(\"esp-tree-paste-child\", (event) => {\n console.log(\"paste as child requested\", event.detail.path);\n });\n</script>\n```\n\n## Expand/collapse icon\n\nThe expand/collapse caret ships inline with the component, so unlike\nnode `icon`s it needs no icon sprite to be hosted. Override it by\nsetting the `expandIcon` property to your own (trusted) SVG markup; it\nrenders on every branch toggle and is rotated 90° when the branch is\nopen, so provide a right-pointing icon. For icon pairs that should not\nrotate, set `collapseIcon` to the trusted SVG markup to use while the\nbranch is open.\n\n```html\n<esp-tree id=\"custom-caret\"></esp-tree>\n<script>\n const tree = findById(\"custom-caret\");\n tree.nodes = [\n { id: \"weddings\", segment: \"weddings\", label: \"Weddings\", children: [\n { id: \"w2026\", segment: \"2026\", label: \"2026\" },\n ] },\n ];\n // A filled triangle caret instead of the default outline chevron.\n tree.expandIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"\n fill=\"currentColor\" stroke=\"none\"><path d=\"M9 6l6 6l-6 6z\" /></svg>`;\n</script>\n```",
93979
94677
  "name": "EspalierTree",
93980
94678
  "cssProperties": [
93981
94679
  {
@@ -94023,7 +94721,7 @@
94023
94721
  "name": "--esp-tree-icon-color"
94024
94722
  },
94025
94723
  {
94026
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
94724
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
94027
94725
  "name": "--esp-field-background",
94028
94726
  "inheritedFrom": {
94029
94727
  "name": "EspalierElementBase",
@@ -97112,6 +97810,17 @@
97112
97810
  "description": "Keep already-expanded branches open when selecting a row outside of\nthe selected branch. By default, selecting outside an open selected\nbranch collapses that branch.",
97113
97811
  "fieldName": "keepExpanded"
97114
97812
  },
97813
+ {
97814
+ "type": {
97815
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
97816
+ },
97817
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
97818
+ "name": "surface-layer",
97819
+ "inheritedFrom": {
97820
+ "name": "EspalierElementBase",
97821
+ "module": "dist/shared/esp-element-base.js"
97822
+ }
97823
+ },
97115
97824
  {
97116
97825
  "name": "scheme",
97117
97826
  "type": {
@@ -97301,7 +98010,7 @@
97301
98010
  "name": "--esp-video-message-color"
97302
98011
  },
97303
98012
  {
97304
- "description": "Background color of the shared field shell. Defaults to `var(--esp-color-layer-2)`.",
98013
+ "description": "Background color of the shared field shell. Defaults to one layer above the nearest declared surface, capped at `var(--esp-color-layer-4)`, or `var(--esp-color-layer-2)` without a declared surface. An explicit override wins.",
97305
98014
  "name": "--esp-field-background",
97306
98015
  "inheritedFrom": {
97307
98016
  "name": "EspalierElementBase",
@@ -98957,6 +99666,17 @@
98957
99666
  "description": "CORS mode for the video's requests, as on `<video crossorigin>`. Needed\nfor caption tracks served from another origin.",
98958
99667
  "fieldName": "crossOrigin"
98959
99668
  },
99669
+ {
99670
+ "type": {
99671
+ "text": "\"1\"|\"2\"|\"3\"|\"4\""
99672
+ },
99673
+ "description": "Declares the painted surface depth. Fields use the next local layer, capped at layer 4.",
99674
+ "name": "surface-layer",
99675
+ "inheritedFrom": {
99676
+ "name": "EspalierElementBase",
99677
+ "module": "dist/shared/esp-element-base.js"
99678
+ }
99679
+ },
98960
99680
  {
98961
99681
  "name": "scheme",
98962
99682
  "type": {
@@ -99682,7 +100402,7 @@
99682
100402
  {
99683
100403
  "kind": "variable",
99684
100404
  "name": "datePickerStyles",
99685
- "default": "css` /* ── Error state ─────────────────────────────────── */ .temporal-error { font-family: var( --_esp-font-body-effective, var(--esp-font-body, var(--_esp-font-body-fallback)) ); font-size: var(--esp-type-small); color: var(--esp-color-danger-text); padding: var(--esp-size-padding); } /* ── Trigger field ───────────────────────────────── */ .esp-field { display: grid; grid-template-columns: 1fr min-content; cursor: pointer; > input.esp-input { cursor: pointer; width: auto; } > .field-icon { display: grid; place-content: center; cursor: pointer; > svg { height: calc(1.2 * var(--esp-size-font)); width: calc(1.2 * var(--esp-size-font)); margin: 0 var(--esp-size-tiny); } } } /* ── Popover shell (native top-layer) ────────────── */ .picker-popover { position: fixed; inset: unset; margin: 0; padding: var(--esp-size-small); border-radius: var(--esp-size-border-radius); border: 1px solid var(--esp-color-border); background: var(--esp-color-layer-1); box-shadow: 0 4px 16px var(--esp-color-shadow); color: var(--esp-color-text); font-family: var( --_esp-font-body-effective, var(--esp-font-body, var(--_esp-font-body-fallback)) ); font-size: var(--esp-size-font); opacity: 0; transition: opacity 150ms ease-in; overflow: visible; } .picker-popover.visible { opacity: 1; } /* ── Calendar ────────────────────────────────────── */ .calendar { --_cell-size: 2.4em; display: flex; flex-direction: column; gap: var(--esp-size-tiny); user-select: none; /* Clip the swiping body, but with a margin so the title controls' outset focus ring (2px offset + 2px width) paints above the header: the header has no padding above the title, so a hard clip at the box edge cut the ring's top. Browsers without overflow-clip-margin clip at the edge, as hidden did. */ overflow: clip; overflow-clip-margin: 4px; } .cal-body { will-change: transform; } .cal-header { display: flex; align-items: center; justify-content: space-between; padding-bottom: var(--esp-size-tiny); } .cal-title { font-weight: bold; font-size: var(--esp-type-normal); color: var(--esp-color-headings); } /* The month and year are buttons that open the jump surfaces, but the title must look exactly as it did as a span: every native button default is reset and the typography is inherited from .cal-title. Users discover the controls by clicking; the pointer cursor is the only pointer affordance, and the focus ring shows for keyboard focus only. No hover or expanded treatment, by design. */ .cal-title-part { appearance: none; display: inline; margin: 0; padding: 0; border: 0; border-radius: var(--esp-size-border-radius); background: none; font: inherit; letter-spacing: inherit; color: inherit; cursor: pointer; /* Outset, unlike .nav-btn's inset ring: an inset ring would cross the glyphs of a padding-less control. The ring's top lies outside the calendar box; .calendar's overflow-clip-margin lets it paint. Focus rings read against the calendar's own ground, so they take --esp-color-text. They used --esp-color-action-text until ESP0223: that token is only guaranteed legible on --esp-color-action-background, and it happened to be near-black in light and near-white in dark, which is the polarity the calendar ground wants too. The moment the default action surface moved, the borrowed token inverted and the ring vanished into the calendar. A ring drawn inside a selected cell is the exception and overrides this, because that cell paints the action surface. */ &:focus-visible { outline: 2px solid var(--esp-color-text); outline-offset: 2px; } } .nav-btn { display: grid; place-content: center; background: none; border: none; cursor: pointer; padding: var(--esp-size-tiny); border-radius: var(--esp-size-border-radius); color: var(--esp-color-text); transition: background-color 120ms ease; > svg { height: var(--esp-size-normal); width: var(--esp-size-normal); } &:hover { background-color: var(--esp-color-layer-3); } &:focus-visible { outline: 2px solid var(--esp-color-text); outline-offset: -2px; } } /* ── Weekday labels ──────────────────────────────── */ .weekday-row { display: grid; grid-template-columns: repeat(7, 1fr); text-align: center; } .weekday-label { font-size: var(--esp-type-tiny); font-weight: 600; color: var(--esp-color-headings); text-decoration: none; padding: var(--esp-size-tiny) 0; } /* ── Day grid ────────────────────────────────────── */ .day-grid { display: flex; flex-direction: column; } /* Show the focused-day ring only when the grid itself contains focus, so it disappears when the user tabs to the nav arrows or time picker. */ .day-grid:focus-within .day-cell.focused { outline: 2px solid var(--esp-color-text); outline-offset: -2px; z-index: 1; } .day-row { display: grid; grid-template-columns: repeat(7, 1fr); } /* ── Day, month, and year cells ──────────────────── */ /* The month and year surfaces reuse the day cell's rest, hover, selected, disabled, and focus treatments so they read as the same calendar; only the geometry and the day-specific states differ. */ .day-cell, .jump-cell { position: relative; display: grid; place-content: center; border: none; border-radius: var(--esp-size-border-radius); background: none; cursor: pointer; font-family: inherit; font-size: inherit; color: var(--esp-color-text); transition: background-color 100ms ease, color 100ms ease; &:hover:not(.disabled):not(.selected) { background-color: var(--esp-color-layer-3); } &:focus-visible { outline: 2px solid var(--esp-color-text); outline-offset: -2px; z-index: 1; } /* Selected / range endpoints. A selected cell is a filled state, so it paints the one surface/ink pair the engine guarantees: APCA enforces --esp-color-action-text against --esp-color-action-background and nothing against --esp-color-layer-3, which this rule used to paint. Under a layer-3 ground the label had no enforced partner at all — it read at Lc 24.6 in light once ESP0223 moved the action surface, and 60.4 after taking --esp-color-text, still under the Lc 75 that ADR-002 sets for content text. On the action pair it is 76.0 and 81.6. In the dark scheme layer-3 and the action surface resolve to the same value, so that scheme is unchanged. */ &.selected { background-color: var(--esp-color-action-background); color: var(--esp-color-action-text); font-weight: bold; } } /* Everything drawn ON a selected cell reads against the action surface that cell paints, not against the calendar ground, so it takes that surface's own ink. These are declared at top level rather than nested under .selected because the focused-day ring is .day-grid:focus-within .day-cell.focused — specificity (0,4,0). A nested &:focus-visible compiles to (0,3,0) and loses to it, which is how a first attempt at this left a keyboard-focused selected day ringed in --esp-color-text at Lc 9.1 while looking correct on the month and year surfaces, whose cells carry no .focused rule. Naming the same selector plus .selected is what actually wins. A .day-cell.selected:focus-visible form would be unreachable: _renderDayCell drives .focused and tabindex=\"0\" off one flag and a day cell only renders inside .day-grid, so the grid selector always matches first. The jump surfaces have no .focused rule, so they take the :focus-visible form. */ .day-grid:focus-within .day-cell.focused.selected, .jump-cell.selected:focus-visible { outline-color: var(--esp-color-action-text); } .day-cell.today.selected { box-shadow: inset 0 0 0 1px var(--esp-color-action-text); } .day-cell { width: var(--_cell-size); height: var(--_cell-size); /* Outside month — dim */ &.outside-month { opacity: 0.38; } /* Today ring */ &.today { font-weight: bold; box-shadow: inset 0 0 0 1px var(--esp-color-border); border-radius: var(--esp-size-border-radius); } /* Range span highlight — washed-out brand colour */ &.in-range { background-color: oklch(from var(--esp-color-layer-3) calc(l + 0.15) calc(c * 0.4) h); border-radius: 0; } &.range-start { border-radius: var(--esp-size-border-radius) 0 0 var(--esp-size-border-radius); } &.range-end { border-radius: 0 var(--esp-size-border-radius) var(--esp-size-border-radius) 0; } } /* Disabled — declared after the day-specific states at the same specificity, so a disabled day that is also outside the month keeps the disabled opacity, as it did before the surfaces shared the cell rules. */ .day-cell.disabled, .jump-cell.disabled { opacity: 0.3; cursor: not-allowed; } /* ── Month and year jump surfaces ────────────────── */ /* A surface fills the body, which is pinned to the day grid's measured height while the surface is showing (see _applySurfaceChange), so the popover keeps its footprint and position. */ .jump-surface { box-sizing: border-box; block-size: 100%; } .month-surface { display: grid; grid-template-columns: repeat(3, 1fr); grid-auto-rows: 1fr; gap: var(--esp-size-tiny); } .year-surface { /* Relative so each year's offsetTop is measured against the list, which is what _scrollYearIntoView positions by. */ position: relative; display: flex; flex-direction: column; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; } .year-cell { flex: 0 0 auto; min-block-size: var(--_cell-size); } /* ── Time picker ─────────────────────────────────── */ .time-picker { display: flex; align-items: center; flex-wrap: nowrap; gap: var(--esp-size-tiny); padding-top: var(--esp-size-small); border-top: 1px solid var(--esp-color-border); margin-top: var(--esp-size-small); white-space: nowrap; /* Push the action buttons to the right edge */ > esp-button:first-of-type { margin-left: auto; } } /* Only-time mode: no top border */ :host([mode=\"time\"]) .time-picker { border-top: none; margin-top: 0; padding-top: 0; } .popover-actions { display: flex; gap: var(--esp-size-tiny); align-items: center; justify-content: flex-end; padding-top: var(--esp-size-small); border-top: 1px solid var(--esp-color-border); margin-top: var(--esp-size-small); } .time-sep { font-size: var(--esp-type-medium); font-weight: bold; color: var(--esp-color-headings); padding: 0 2px; } `",
100405
+ "default": "css` /* ── Error state ─────────────────────────────────── */ .temporal-error { font-family: var( --_esp-font-body-effective, var(--esp-font-body, var(--_esp-font-body-fallback)) ); font-size: var(--esp-type-small); color: var(--esp-color-danger-text); padding: var(--esp-size-padding); } /* ── Trigger field ───────────────────────────────── */ .esp-field { display: grid; grid-template-columns: 1fr min-content; cursor: pointer; > input.esp-input { cursor: pointer; width: auto; } > .field-icon { display: grid; place-content: center; cursor: pointer; > svg { height: calc(1.2 * var(--esp-size-font)); width: calc(1.2 * var(--esp-size-font)); margin: 0 var(--esp-size-tiny); } } } /* ── Popover shell (native top-layer) ────────────── */ .picker-popover { --_esp-surface-layer: 1; position: fixed; inset: unset; margin: 0; padding: var(--esp-size-small); border-radius: var(--esp-size-border-radius); border: 1px solid var(--esp-color-border); background: var(--esp-color-layer-1); box-shadow: 0 4px 16px var(--esp-color-shadow); color: var(--esp-color-text); font-family: var( --_esp-font-body-effective, var(--esp-font-body, var(--_esp-font-body-fallback)) ); font-size: var(--esp-size-font); opacity: 0; transition: opacity 150ms ease-in; overflow: visible; } .picker-popover.visible { opacity: 1; } /* ── Calendar ────────────────────────────────────── */ .calendar { --_cell-size: 2.4em; display: flex; flex-direction: column; gap: var(--esp-size-tiny); user-select: none; /* Clip the swiping body, but with a margin so the title controls' outset focus ring (2px offset + 2px width) paints above the header: the header has no padding above the title, so a hard clip at the box edge cut the ring's top. Browsers without overflow-clip-margin clip at the edge, as hidden did. */ overflow: clip; overflow-clip-margin: 4px; } .cal-body { will-change: transform; } .cal-header { display: flex; align-items: center; justify-content: space-between; padding-bottom: var(--esp-size-tiny); } .cal-title { font-weight: bold; font-size: var(--esp-type-normal); color: var(--esp-color-headings); } /* The month and year are buttons that open the jump surfaces, but the title must look exactly as it did as a span: every native button default is reset and the typography is inherited from .cal-title. Users discover the controls by clicking; the pointer cursor is the only pointer affordance, and the focus ring shows for keyboard focus only. No hover or expanded treatment, by design. */ .cal-title-part { appearance: none; display: inline; margin: 0; padding: 0; border: 0; border-radius: var(--esp-size-border-radius); background: none; font: inherit; letter-spacing: inherit; color: inherit; cursor: pointer; /* Outset, unlike .nav-btn's inset ring: an inset ring would cross the glyphs of a padding-less control. The ring's top lies outside the calendar box; .calendar's overflow-clip-margin lets it paint. Focus rings read against the calendar's own ground, so they take --esp-color-text. They used --esp-color-action-text until ESP0223: that token is only guaranteed legible on --esp-color-action-background, and it happened to be near-black in light and near-white in dark, which is the polarity the calendar ground wants too. The moment the default action surface moved, the borrowed token inverted and the ring vanished into the calendar. A ring drawn inside a selected cell is the exception and overrides this, because that cell paints the action surface. */ &:focus-visible { outline: 2px solid var(--esp-color-text); outline-offset: 2px; } } .nav-btn { display: grid; place-content: center; background: none; border: none; cursor: pointer; padding: var(--esp-size-tiny); border-radius: var(--esp-size-border-radius); color: var(--esp-color-text); transition: background-color 120ms ease; > svg { height: var(--esp-size-normal); width: var(--esp-size-normal); } &:hover { background-color: var(--esp-color-layer-3); } &:focus-visible { outline: 2px solid var(--esp-color-text); outline-offset: -2px; } } /* ── Weekday labels ──────────────────────────────── */ .weekday-row { display: grid; grid-template-columns: repeat(7, 1fr); text-align: center; } .weekday-label { font-size: var(--esp-type-tiny); font-weight: 600; color: var(--esp-color-headings); text-decoration: none; padding: var(--esp-size-tiny) 0; } /* ── Day grid ────────────────────────────────────── */ .day-grid { display: flex; flex-direction: column; } /* Show the focused-day ring only when the grid itself contains focus, so it disappears when the user tabs to the nav arrows or time picker. */ .day-grid:focus-within .day-cell.focused { outline: 2px solid var(--esp-color-text); outline-offset: -2px; z-index: 1; } .day-row { display: grid; grid-template-columns: repeat(7, 1fr); } /* ── Day, month, and year cells ──────────────────── */ /* The month and year surfaces reuse the day cell's rest, hover, selected, disabled, and focus treatments so they read as the same calendar; only the geometry and the day-specific states differ. */ .day-cell, .jump-cell { position: relative; display: grid; place-content: center; border: none; border-radius: var(--esp-size-border-radius); background: none; cursor: pointer; font-family: inherit; font-size: inherit; color: var(--esp-color-text); transition: background-color 100ms ease, color 100ms ease; &:hover:not(.disabled):not(.selected) { background-color: var(--esp-color-layer-3); } &:focus-visible { outline: 2px solid var(--esp-color-text); outline-offset: -2px; z-index: 1; } /* Selected / range endpoints. A selected cell is a filled state, so it paints the one surface/ink pair the engine guarantees: APCA enforces --esp-color-action-text against --esp-color-action-background and nothing against --esp-color-layer-3, which this rule used to paint. Under a layer-3 ground the label had no enforced partner at all — it read at Lc 24.6 in light once ESP0223 moved the action surface, and 60.4 after taking --esp-color-text, still under the Lc 75 that ADR-002 sets for content text. On the action pair it is 76.0 and 81.6. In the dark scheme layer-3 and the action surface resolve to the same value, so that scheme is unchanged. */ &.selected { background-color: var(--esp-color-action-background); color: var(--esp-color-action-text); font-weight: bold; } } /* Everything drawn ON a selected cell reads against the action surface that cell paints, not against the calendar ground, so it takes that surface's own ink. These are declared at top level rather than nested under .selected because the focused-day ring is .day-grid:focus-within .day-cell.focused — specificity (0,4,0). A nested &:focus-visible compiles to (0,3,0) and loses to it, which is how a first attempt at this left a keyboard-focused selected day ringed in --esp-color-text at Lc 9.1 while looking correct on the month and year surfaces, whose cells carry no .focused rule. Naming the same selector plus .selected is what actually wins. A .day-cell.selected:focus-visible form would be unreachable: _renderDayCell drives .focused and tabindex=\"0\" off one flag and a day cell only renders inside .day-grid, so the grid selector always matches first. The jump surfaces have no .focused rule, so they take the :focus-visible form. */ .day-grid:focus-within .day-cell.focused.selected, .jump-cell.selected:focus-visible { outline-color: var(--esp-color-action-text); } .day-cell.today.selected { box-shadow: inset 0 0 0 1px var(--esp-color-action-text); } .day-cell { width: var(--_cell-size); height: var(--_cell-size); /* Outside month — dim */ &.outside-month { opacity: 0.38; } /* Today ring */ &.today { font-weight: bold; box-shadow: inset 0 0 0 1px var(--esp-color-border); border-radius: var(--esp-size-border-radius); } /* Range span highlight — washed-out brand colour */ &.in-range { background-color: oklch(from var(--esp-color-layer-3) calc(l + 0.15) calc(c * 0.4) h); border-radius: 0; } &.range-start { border-radius: var(--esp-size-border-radius) 0 0 var(--esp-size-border-radius); } &.range-end { border-radius: 0 var(--esp-size-border-radius) var(--esp-size-border-radius) 0; } } /* Disabled — declared after the day-specific states at the same specificity, so a disabled day that is also outside the month keeps the disabled opacity, as it did before the surfaces shared the cell rules. */ .day-cell.disabled, .jump-cell.disabled { opacity: 0.3; cursor: not-allowed; } /* ── Month and year jump surfaces ────────────────── */ /* A surface fills the body, which is pinned to the day grid's measured height while the surface is showing (see _applySurfaceChange), so the popover keeps its footprint and position. */ .jump-surface { box-sizing: border-box; block-size: 100%; } .month-surface { display: grid; grid-template-columns: repeat(3, 1fr); grid-auto-rows: 1fr; gap: var(--esp-size-tiny); } .year-surface { /* Relative so each year's offsetTop is measured against the list, which is what _scrollYearIntoView positions by. */ position: relative; display: flex; flex-direction: column; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; } .year-cell { flex: 0 0 auto; min-block-size: var(--_cell-size); } /* ── Time picker ─────────────────────────────────── */ .time-picker { display: flex; align-items: center; flex-wrap: nowrap; gap: var(--esp-size-tiny); padding-top: var(--esp-size-small); border-top: 1px solid var(--esp-color-border); margin-top: var(--esp-size-small); white-space: nowrap; /* Push the action buttons to the right edge */ > esp-button:first-of-type { margin-left: auto; } } /* Only-time mode: no top border */ :host([mode=\"time\"]) .time-picker { border-top: none; margin-top: 0; padding-top: 0; } .popover-actions { display: flex; gap: var(--esp-size-tiny); align-items: center; justify-content: flex-end; padding-top: var(--esp-size-small); border-top: 1px solid var(--esp-color-border); margin-top: var(--esp-size-small); } .time-sep { font-size: var(--esp-type-medium); font-weight: bold; color: var(--esp-color-headings); padding: 0 2px; } `",
99686
100406
  "description": "Component-specific styles for `esp-date-picker`."
99687
100407
  }
99688
100408
  ],