@taprootio/espalier 3.7.0 → 3.9.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.
@@ -1 +1 @@
1
- const T=.9,N=.88,_=.8;export{_ as BUTTON_ICON_INTERACTION_LIGHTNESS_FACTOR,T as BUTTON_ICON_SPLIT_LIGHTNESS_FACTOR,N as BUTTON_INTERACTION_LIGHTNESS_FACTOR};
1
+ const T=.88;export{T as BUTTON_INTERACTION_LIGHTNESS_FACTOR};
@@ -1,5 +1,6 @@
1
1
  import { type TemplateResult } from "lit";
2
2
  import { EspalierElementBase } from "../shared/esp-element-base.js";
3
+ export type ButtonIconPosition = "left" | "right";
3
4
  /**
4
5
  * Used to initiate a user action or link to another page.
5
6
  *
@@ -29,6 +30,27 @@ import { EspalierElementBase } from "../shared/esp-element-base.js";
29
30
  * </div>
30
31
  * ```
31
32
  *
33
+ * Use `icon-position="left"` for a leading icon. The default `"right"`
34
+ * position keeps the icon trailing; both positions follow writing direction
35
+ * and therefore mirror in RTL.
36
+ *
37
+ * ```html
38
+ * <div style="display: flex; gap: var(--esp-size-padding);">
39
+ * <esp-button
40
+ * collapsed
41
+ * label="Save"
42
+ * icon="save"
43
+ * icon-position="left">
44
+ * </esp-button>
45
+ * <esp-button
46
+ * collapsed
47
+ * label="Next"
48
+ * icon="arrow-big-right"
49
+ * icon-position="right">
50
+ * </esp-button>
51
+ * </div>
52
+ * ```
53
+ *
32
54
  * @customElement esp-button
33
55
  * @slot - Optional custom SVG icon. Slotted icons override the
34
56
  * generated SVG from the `icon` attribute.
@@ -180,6 +202,18 @@ export declare class EspalierButton extends EspalierElementBase {
180
202
  * @type {string}
181
203
  */
182
204
  icon: string;
205
+ /**
206
+ * Places the optional icon on the logical leading (`"left"`) or trailing
207
+ * (`"right"`) side of the label. Logical placement mirrors in RTL.
208
+ *
209
+ * ```html
210
+ * <esp-button label="Save" icon="save" icon-position="left"></esp-button>
211
+ * <esp-button label="Next" icon="arrow-big-right" icon-position="right"></esp-button>
212
+ * ```
213
+ *
214
+ * @type {"left" | "right"}
215
+ */
216
+ iconPosition: ButtonIconPosition;
183
217
  /**
184
218
  * The label of the button.
185
219
  *
@@ -1,4 +1,4 @@
1
- var n=function(h,t,i,e){var a=arguments.length,r=a<3?t:e===null?e=Object.getOwnPropertyDescriptor(t,i):e,c;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")r=Reflect.decorate(h,t,i,e);else for(var d=h.length-1;d>=0;d--)(c=h[d])&&(r=(a<3?c(r):a>3?c(t,i,r):c(t,i))||r);return a>3&&r&&Object.defineProperty(t,i,r),r};import{css as f,html as p,nothing as l}from"lit";import{customElement as m,eventOptions as v,property as s,state as y}from"lit/decorators.js";import{classMap as u}from"lit/directives/class-map.js";import{EspalierElementBase as b}from"../shared/esp-element-base.js";import{ref as k}from"lit/directives/ref.js";import{getIconHrefForHost as x}from"../shared/intent-values.js";import{SlottedIconController as T}from"../shared/slotted-icon-controller.js";import{renderSpriteIcon as w}from"../shared/svgs/render-sprite-icon.js";import{BUTTON_ICON_INTERACTION_LIGHTNESS_FACTOR as S,BUTTON_ICON_SPLIT_LIGHTNESS_FACTOR as g,BUTTON_INTERACTION_LIGHTNESS_FACTOR as $}from"./action-state-lightness.js";const N=p`<svg
1
+ var n=function(h,t,r,o){var a=arguments.length,i=a<3?t:o===null?o=Object.getOwnPropertyDescriptor(t,r):o,d;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")i=Reflect.decorate(h,t,r,o);else for(var c=h.length-1;c>=0;c--)(d=h[c])&&(i=(a<3?d(i):a>3?d(t,r,i):d(t,r))||i);return a>3&&i&&Object.defineProperty(t,r,i),i};import{css as f,html as p,nothing as l}from"lit";import{customElement as g,eventOptions as m,property as s,state as v}from"lit/decorators.js";import{classMap as u}from"lit/directives/class-map.js";import{EspalierElementBase as b}from"../shared/esp-element-base.js";import{ref as y}from"lit/directives/ref.js";import{getIconHrefForHost as w}from"../shared/intent-values.js";import{SlottedIconController as x}from"../shared/slotted-icon-controller.js";import{renderSpriteIcon as k}from"../shared/svgs/render-sprite-icon.js";import{BUTTON_INTERACTION_LIGHTNESS_FACTOR as T}from"./action-state-lightness.js";const S=p`<svg
2
2
  xmlns="http://www.w3.org/2000/svg"
3
3
  viewBox="0 0 24 24"
4
4
  fill="none"
@@ -10,40 +10,40 @@ var n=function(h,t,i,e){var a=arguments.length,r=a<3?t:e===null?e=Object.getOwnP
10
10
  >
11
11
  <path stroke="none" d="M0 0h24v24H0z" fill="none" />
12
12
  <path d="M4.05 11a8 8 0 1 1 .5 4m-.5 5v-5h5" />
13
- </svg>`;let o=class extends b{constructor(){super(...arguments),this.iconSlot=new T(this),this.innerTabindex=null,this.neutralizingTabindex=!1,this.buttonType="button",this.collapsed=!1,this.disabled=!1,this.href="",this.iconOnly=!1,this.incognito=!1,this.icon="",this.label="",this.loading=!1,this.target="",this.rel="",this.formNoValidate=!1,this.buildIcon=t=>this.loading?p` <span>${N}</span> `:p`
13
+ </svg>`;let e=class extends b{constructor(){super(...arguments),this.iconSlot=new x(this),this.innerTabindex=null,this.neutralizingTabindex=!1,this.buttonType="button",this.collapsed=!1,this.disabled=!1,this.href="",this.iconOnly=!1,this.incognito=!1,this.icon="",this.iconPosition="right",this.label="",this.loading=!1,this.target="",this.rel="",this.formNoValidate=!1,this.buildIcon=t=>this.loading?p` <span>${S}</span> `:p`
14
14
  <span>
15
- <slot ${k(this.iconSlot.slotRef)} @slotchange=${this.iconSlot.handleSlotChange}></slot>
16
- ${!this.iconSlot.hasSlottedIcon(":scope > *")&&t?w(t):l}
15
+ <slot ${y(this.iconSlot.slotRef)} @slotchange=${this.iconSlot.handleSlotChange}></slot>
16
+ ${!this.iconSlot.hasSlottedIcon(":scope > *")&&t?k(t):l}
17
17
  </span>
18
- `}static get observedAttributes(){return[...super.observedAttributes,"tabindex","aria-pressed"]}attributeChangedCallback(t,i,e){if(super.attributeChangedCallback(t,i,e),t==="aria-pressed"){this.requestUpdate();return}if(t!=="tabindex"||this.neutralizingTabindex)return;const a=e===null?null:Number(e),r=a===null||Number.isNaN(a)?null:a;r!==null&&r>=0?(this.innerTabindex=r,this.writeHostTabindex(null)):r===null&&e!==null?(this.innerTabindex=null,this.writeHostTabindex(null)):this.innerTabindex=r}writeHostTabindex(t){this.getAttribute("tabindex")!==t&&(this.neutralizingTabindex=!0,t===null?this.removeAttribute("tabindex"):this.setAttribute("tabindex",t),this.neutralizingTabindex=!1)}removeAttribute(t){super.removeAttribute(t),t==="tabindex"&&!this.neutralizingTabindex&&(this.innerTabindex=null)}focus(t){this.focusShadowElementAfterUpdate("button, a",t)}handleClick(t){if(this.dispatchEvent(new CustomEvent("clicked",{detail:{},bubbles:!0,composed:!0})),this.buttonType==="submit"){const i=this.closest("form");if(i){const e=document.createElement("button");e.type="submit",e.hidden=!0,this.formNoValidate&&(e.formNoValidate=!0),i.appendChild(e),i.requestSubmit(e),e.remove()}}else this.buttonType==="reset"&&this.closest("form")?.reset()}render(){const t=x(this.icon,this),i=this.loading||this.iconSlot.hasSlottedIcon(":scope > *")||t.length>0,{collapsed:e,iconOnly:a}=this,r=a?this.getAttribute("aria-label")||this.label||l:l,c=this.getAttribute("aria-pressed")||l,d={"esp-field":!0,"icon-only":a,incognito:this.incognito,"is-collapsed":e,"has-icon":i};return this.href.length?p`
19
- <div part="wrapper" class=${u(d)}>
18
+ `}static get observedAttributes(){return[...super.observedAttributes,"tabindex","aria-pressed"]}attributeChangedCallback(t,r,o){if(super.attributeChangedCallback(t,r,o),t==="aria-pressed"){this.requestUpdate();return}if(t!=="tabindex"||this.neutralizingTabindex)return;const a=o===null?null:Number(o),i=a===null||Number.isNaN(a)?null:a;i!==null&&i>=0?(this.innerTabindex=i,this.writeHostTabindex(null)):i===null&&o!==null?(this.innerTabindex=null,this.writeHostTabindex(null)):this.innerTabindex=i}writeHostTabindex(t){this.getAttribute("tabindex")!==t&&(this.neutralizingTabindex=!0,t===null?this.removeAttribute("tabindex"):this.setAttribute("tabindex",t),this.neutralizingTabindex=!1)}removeAttribute(t){super.removeAttribute(t),t==="tabindex"&&!this.neutralizingTabindex&&(this.innerTabindex=null)}focus(t){this.focusShadowElementAfterUpdate("button, a",t)}handleClick(t){if(this.dispatchEvent(new CustomEvent("clicked",{detail:{},bubbles:!0,composed:!0})),this.buttonType==="submit"){const r=this.closest("form");if(r){const o=document.createElement("button");o.type="submit",o.hidden=!0,this.formNoValidate&&(o.formNoValidate=!0),r.appendChild(o),r.requestSubmit(o),o.remove()}}else this.buttonType==="reset"&&this.closest("form")?.reset()}render(){const t=w(this.icon,this),r=this.loading||this.iconSlot.hasSlottedIcon(":scope > *")||t.length>0,{collapsed:o,iconOnly:a}=this,i=a?this.getAttribute("aria-label")||this.label||l:l,d=this.getAttribute("aria-pressed")||l,c={"esp-field":!0,"icon-only":a,incognito:this.incognito,"is-collapsed":o,"has-icon":r,"icon-left":r&&!a&&this.iconPosition==="left"};return this.href.length?p`
19
+ <div part="wrapper" class=${u(c)}>
20
20
  <a
21
21
  ?disabled=${this.disabled}
22
22
  href=${this.href}
23
23
  target=${this.target}
24
24
  rel=${this.rel||l}
25
25
  tabindex=${this.innerTabindex??l}
26
- aria-label=${r}
26
+ aria-label=${i}
27
27
  >
28
28
  <span>${this.label??""}</span>
29
29
  ${this.buildIcon(t)}
30
30
  </a>
31
31
  </div>
32
32
  `:p`
33
- <div part="wrapper" class=${u(d)}>
33
+ <div part="wrapper" class=${u(c)}>
34
34
  <button
35
35
  ?disabled=${this.disabled||this.loading}
36
36
  @click=${this.handleClick}
37
37
  type=${this.buttonType}
38
38
  tabindex=${this.innerTabindex??l}
39
- aria-label=${r}
40
- aria-pressed=${c}
39
+ aria-label=${i}
40
+ aria-pressed=${d}
41
41
  >
42
42
  <span>${this.label??""}</span>
43
43
  ${this.buildIcon(t)}
44
44
  </button>
45
45
  </div>
46
- `}};o.styles=[...b.styles,f`
46
+ `}};e.styles=[...b.styles,f`
47
47
  :host {
48
48
  --_esp-button-radius: var(--esp-size-border-radius);
49
49
  }
@@ -81,15 +81,8 @@ var n=function(h,t,i,e){var a=arguments.length,r=a<3?t:e===null?e=Object.getOwnP
81
81
  }
82
82
 
83
83
  > span:nth-child(2) {
84
- background-color: oklch(
85
- from var(--esp-color-action-background)
86
- calc(l * ${g}) c h
87
- );
88
84
  display: none;
89
85
  place-content: center;
90
- border-left: 1px dotted var(--esp-color-border);
91
- border-bottom-right-radius: max(0px, calc(var(--_esp-button-radius) - 0.1rem));
92
- border-top-right-radius: max(0px, calc(var(--_esp-button-radius) - 0.1rem));
93
86
  padding: var(--esp-button-padding, var(--esp-size-padding));
94
87
 
95
88
  .generated-icon,
@@ -101,6 +94,17 @@ var n=function(h,t,i,e){var a=arguments.length,r=a<3?t:e===null?e=Object.getOwnP
101
94
  }
102
95
  }
103
96
 
97
+ &.icon-left {
98
+ button,
99
+ a {
100
+ grid-template-columns: min-content auto;
101
+
102
+ > span:nth-child(2) {
103
+ order: -1;
104
+ }
105
+ }
106
+ }
107
+
104
108
  &.has-icon {
105
109
  button,
106
110
  a {
@@ -123,7 +127,6 @@ var n=function(h,t,i,e){var a=arguments.length,r=a<3?t:e===null?e=Object.getOwnP
123
127
  }
124
128
 
125
129
  > span:nth-child(2) {
126
- background-color: var(--esp-color-action-background);
127
130
  border: none;
128
131
  border-radius: var(--_esp-button-radius);
129
132
  }
@@ -136,24 +139,12 @@ var n=function(h,t,i,e){var a=arguments.length,r=a<3?t:e===null?e=Object.getOwnP
136
139
  &:hover,
137
140
  &:focus-within {
138
141
  background-color: oklch(
139
- from var(--esp-color-action-background) calc(l * ${$})
142
+ from var(--esp-color-action-background) calc(l * ${T})
140
143
  c h
141
144
  );
142
-
143
- button,
144
- a {
145
- > span:nth-child(2) {
146
- background-color: oklch(
147
- from var(--esp-color-action-background)
148
- calc(l * ${S}) c h
149
- );
150
- border-color: oklch(from var(--esp-color-border) calc(l * 1.2) c h);
151
- }
152
- }
153
145
  }
154
146
 
155
147
  &.incognito {
156
- grid-template-columns: min-content auto;
157
148
  background-color: transparent;
158
149
  border-color: transparent;
159
150
  color: var(--esp-color-text);
@@ -162,17 +153,27 @@ var n=function(h,t,i,e){var a=arguments.length,r=a<3?t:e===null?e=Object.getOwnP
162
153
  button,
163
154
  a {
164
155
  > span:nth-child(1) {
165
- grid-column: 2;
166
- justify-content: left;
156
+ justify-content: start;
167
157
  padding: var(--esp-button-incognito-padding, var(--esp-size-tiny)) 0;
168
- padding-right: calc(2 * var(--esp-button-incognito-padding, var(--esp-size-tiny)));
158
+ padding-inline-end: calc(
159
+ 2 * var(--esp-button-incognito-padding, var(--esp-size-tiny))
160
+ );
169
161
  }
170
162
 
171
163
  > span:nth-child(2) {
172
- grid-row: 1;
173
- grid-column: 1;
174
164
  background-color: transparent;
175
- border-color: transparent;
165
+ }
166
+ }
167
+
168
+ &.icon-left {
169
+ button,
170
+ a {
171
+ > span:nth-child(1) {
172
+ padding-inline-start: calc(
173
+ 2 * var(--esp-button-incognito-padding, var(--esp-size-tiny))
174
+ );
175
+ padding-inline-end: 0;
176
+ }
176
177
  }
177
178
  }
178
179
 
@@ -193,17 +194,6 @@ var n=function(h,t,i,e){var a=arguments.length,r=a<3?t:e===null?e=Object.getOwnP
193
194
  &:has(button:disabled):hover,
194
195
  &:has(button:disabled):focus-within {
195
196
  background-color: var(--esp-color-action-background);
196
-
197
- button,
198
- a {
199
- > span:nth-child(2) {
200
- background-color: oklch(
201
- from var(--esp-color-action-background)
202
- calc(l * ${g}) c h
203
- );
204
- border-color: var(--esp-color-border);
205
- }
206
- }
207
197
  }
208
198
 
209
199
  svg.rotate {
@@ -223,4 +213,4 @@ var n=function(h,t,i,e){var a=arguments.length,r=a<3?t:e===null?e=Object.getOwnP
223
213
  transform: rotate(360deg);
224
214
  }
225
215
  }
226
- `],n([y()],o.prototype,"innerTabindex",void 0),n([s({attribute:"button-type",type:String})],o.prototype,"buttonType",void 0),n([s({type:Boolean})],o.prototype,"collapsed",void 0),n([s({type:Boolean,reflect:!0})],o.prototype,"disabled",void 0),n([s({type:String})],o.prototype,"href",void 0),n([s({attribute:"icon-only",type:Boolean})],o.prototype,"iconOnly",void 0),n([s({type:Boolean,reflect:!0})],o.prototype,"incognito",void 0),n([s({type:String})],o.prototype,"icon",void 0),n([s({type:String})],o.prototype,"label",void 0),n([s({type:Boolean,reflect:!0})],o.prototype,"loading",void 0),n([s({type:String})],o.prototype,"target",void 0),n([s({type:String})],o.prototype,"rel",void 0),n([s({attribute:"formnovalidate",type:Boolean})],o.prototype,"formNoValidate",void 0),n([v({capture:!1,passive:!0})],o.prototype,"handleClick",null),o=n([m("esp-button")],o);export{o as EspalierButton};
216
+ `],n([v()],e.prototype,"innerTabindex",void 0),n([s({attribute:"button-type",type:String})],e.prototype,"buttonType",void 0),n([s({type:Boolean})],e.prototype,"collapsed",void 0),n([s({type:Boolean,reflect:!0})],e.prototype,"disabled",void 0),n([s({type:String})],e.prototype,"href",void 0),n([s({attribute:"icon-only",type:Boolean})],e.prototype,"iconOnly",void 0),n([s({type:Boolean,reflect:!0})],e.prototype,"incognito",void 0),n([s({type:String})],e.prototype,"icon",void 0),n([s({attribute:"icon-position",type:String})],e.prototype,"iconPosition",void 0),n([s({type:String})],e.prototype,"label",void 0),n([s({type:Boolean,reflect:!0})],e.prototype,"loading",void 0),n([s({type:String})],e.prototype,"target",void 0),n([s({type:String})],e.prototype,"rel",void 0),n([s({attribute:"formnovalidate",type:Boolean})],e.prototype,"formNoValidate",void 0),n([m({capture:!1,passive:!0})],e.prototype,"handleClick",null),e=n([g("esp-button")],e);export{e as EspalierButton};
package/dist/index.d.ts CHANGED
@@ -73,7 +73,7 @@ export * from "./shared/events.js";
73
73
  export { getImageDetails, releasePreviewUrl, type EspalierUploadImage, type ImageDetailsOptions, type SelectedUploadImage, type ExistingUploadImage, type ExistingImage, type ResponsiveImageUrl, type UploadCallbacks, type UploadEventDetail, } from "./image-upload/image-helpers.js";
74
74
  export { calculatePhotoLayout, type LayoutImage, type PhotoRow, } from "./shared/justified-layout.js";
75
75
  export { type TypeaheadFetchItems } from "./pickers/types.js";
76
- export { type EspalierTheme, type PartialTheme, type PartialThemeContexts, type ThemeContext, type ThemeContexts, type VariantColorSource, encodeTheme, parseTheme, mergePartials, layerThemes, validateThemePair, resolveContextTheme, buildTaprootLightTheme, buildTaprootDarkTheme, NESTED_THEME_KEYS, } from "./shared/theme.js";
76
+ export { type EspalierTheme, type LightnessKey, type LightnessReference, type PartialTheme, type PartialThemeContexts, type PartialThemeTones, type ThemeContext, type ThemeContexts, type ThemeTones, type ToneReference, type VariantColorSource, encodeTheme, parseTheme, mergePartials, layerThemes, validateThemePair, resolveContextTheme, buildTaprootLightTheme, buildTaprootDarkTheme, NESTED_THEME_KEYS, } from "./shared/theme.js";
77
77
  export { auditDataPalette, generateSequentialRamp, generateDivergingRamp, COLOR_VISION_SIMULATIONS, DATA_SERIES_KEYS, DEFAULT_DATA_PALETTE, DEFAULT_DATA_RAMP_STEPS, DEFAULT_DIVERGING_NEUTRAL, MAX_DATA_RAMP_STEPS, MIN_DATA_COLOR_DISTANCE, MIN_DATA_RAMP_LIGHTNESS_STEP, MIN_DATA_RAMP_STEPS, type ColorVisionSimulation, type DataPalette, type DataPaletteIssue, type DataRamp, type DataRamps, type DataSeriesKey, type DivergingDataRamp, type DivergingRampOptions, type PartialDataRamp, type PartialDataRamps, type SequentialDataRamp, type SequentialRampOptions, } from "./shared/data-colors.js";
78
78
  export { WEIGHT_LABELS, extractWeights, normalizeWeight, bestAvailableWeight, extractFamily, getFallbackFont, } from "./shared/font-helpers.js";
79
79
  export { getGoogleFonts } from "./font-picker/esp-font-picker.js";
@@ -1 +1 @@
1
- import{deriveSemantic as O,deriveSemanticWithContrast as k,serializeOklch as C}from"../../shared/color-engine.js";import{SEMANTIC_COLOR_NAMES as M,TOKEN_PAIRINGS as R,semanticToCSS as y}from"../../shared/theme.js";import{resolveMappingSource as A}from"./compute-variants.js";function E(s,u,r,n,i,g){return[s.l,s.c,s.h,u,r,n,i.l,i.c,i.h,g].join("|")}function q(s,u,r={}){const n={},i={},g=new Map,h=(e,o)=>{const t=r.effectiveSource?.(e,o)??o,c=A(t,u,s.anchors);return c||r.onUnresolvedSource?.(t),{color:c??u.primary,resolved:c!==null,source:t}};for(const e of M){if(R[e])continue;const o=s.semanticMappings[e],t=s.chroma[e],c=h(e,o.source),a=s.lightness[o.lightness],m=O(c.color,a,t.min,t.max),l=y(e);i[e]=m,n[l]=C(m),r.trace&&r.trace.push({token:e,property:l,resolved:n[l],source:c.source,lightness:o.lightness,sourceResolved:c.resolved,base:c.color})}for(const e of M){const o=R[e];if(!o)continue;const t=s.semanticMappings[e],c=s.chroma[e],a=h(e,t.source),m=s.lightness[t.lightness],l=i[o.bg],v=E(a.color,m,c.min,c.max,l,o.targetLc);let f=g.get(v);if(!f){let S;const x=k(a.color,m,c.min,c.max,l,o.targetLc,r.trace&&(N=>{S=N}));f={requested:S,semantic:x},g.set(v,f)}const{requested:L,semantic:d}=f,p=y(e);i[e]=d,n[p]=C(d),r.trace&&r.trace.push({token:e,property:p,resolved:n[p],source:a.source,lightness:t.lightness,sourceResolved:a.resolved,base:a.color,requested:L,contrast:{against:o.bg,background:l,targetLc:o.targetLc}})}return n}export{q as computeSemanticProperties};
1
+ import{deriveSemantic as k,deriveSemanticWithContrast as A,serializeOklch as R}from"../../shared/color-engine.js";import{SEMANTIC_COLOR_NAMES as C,TOKEN_PAIRINGS as L,resolveLightnessReference as M,semanticToCSS as y}from"../../shared/theme.js";import{resolveMappingSource as E}from"./compute-variants.js";function I(o,g,r,t,i,m){return[o.l,o.c,o.h,g,r,t,i.l,i.c,i.h,m].join("|")}function K(o,g,r={}){const t={},i={},m=new Map,h=(e,s)=>{const n=r.effectiveSource?.(e,s)??s,c=E(n,g,o.anchors);return c||r.onUnresolvedSource?.(n),{color:c??g.primary,resolved:c!==null,source:n}};for(const e of C){if(L[e])continue;const s=o.semanticMappings[e],n=o.chroma[e],c=h(e,s.source),a=M(o.lightness,o.tones,s.lightness),u=k(c.color,a,n.min,n.max),l=y(e);i[e]=u,t[l]=R(u),r.trace&&r.trace.push({token:e,property:l,resolved:t[l],source:c.source,lightness:s.lightness,sourceResolved:c.resolved,base:c.color})}for(const e of C){const s=L[e];if(!s)continue;const n=o.semanticMappings[e],c=o.chroma[e],a=h(e,n.source),u=M(o.lightness,o.tones,n.lightness),l=i[s.bg],v=I(a.color,u,c.min,c.max,l,s.targetLc);let f=m.get(v);if(!f){let S;const N=A(a.color,u,c.min,c.max,l,s.targetLc,r.trace&&(O=>{S=O}));f={requested:S,semantic:N},m.set(v,f)}const{requested:x,semantic:d}=f,p=y(e);i[e]=d,t[p]=R(d),r.trace&&r.trace.push({token:e,property:p,resolved:t[p],source:a.source,lightness:n.lightness,sourceResolved:a.resolved,base:a.color,requested:x,contrast:{against:s.bg,background:l,targetLc:s.targetLc}})}return t}export{K as computeSemanticProperties};
@@ -48,7 +48,7 @@
48
48
  * report.adjustedTokens; // ['linkHover']
49
49
  * ```
50
50
  */
51
- import { type EspalierTheme, type LightnessKey, type MappingSource, type PartialTheme, type SemanticColorName } from "./theme.js";
51
+ import { type EspalierTheme, type LightnessReference, type MappingSource, type PartialTheme, type SemanticColorName } from "./theme.js";
52
52
  /** What APCA enforcement did to one token. */
53
53
  export interface ThemeFitApcaAction {
54
54
  /** The requested value, as an `oklch()` string. */
@@ -75,8 +75,8 @@ export interface ThemeFitAnchor {
75
75
  /**
76
76
  * ΔE-OK between the declared anchor and the emitted token. Expect a
77
77
  * non-zero number even on a perfect fit: lightness always comes from
78
- * the mapping's ramp stop, so a swatch used as a source is re-lit by
79
- * design (ADR-015).
78
+ * the mapping's built-in stop or custom tone, so a swatch used as a
79
+ * source is re-lit by design (ADR-015, ADR-020).
80
80
  */
81
81
  deltaE: number;
82
82
  }
@@ -88,8 +88,8 @@ export interface ThemeFitToken {
88
88
  property: string;
89
89
  /** The mapping's declared source. */
90
90
  source: MappingSource;
91
- /** The lightness ramp stop the mapping selected. */
92
- lightness: LightnessKey;
91
+ /** The built-in ramp stop or custom tone reference the mapping selected. */
92
+ lightness: LightnessReference;
93
93
  /** The value the mapping asked for, before contrast enforcement. */
94
94
  requested: string;
95
95
  /** The value actually emitted. */
@@ -35,6 +35,14 @@ import { type DataPalette, type DataRamps, type PartialDataRamps } from "./data-
35
35
  export type LightnessKey = "surface" | "raised1" | "raised2" | "raised3" | "raised4" | "accent" | "muted" | "text" | "border" | "ink" | "shadow";
36
36
  /** Lightness values (0–1) for every ramp position. */
37
37
  export type LightnessMap = Record<LightnessKey, number>;
38
+ /** A named, opt-in lightness value outside the engine-owned ramp. */
39
+ export type ToneReference = `tone:${string}`;
40
+ /** A built-in ramp stop or an explicitly declared custom tone. */
41
+ export type LightnessReference = LightnessKey | ToneReference;
42
+ /** Named custom lightness values. They are inert until a mapping references one. */
43
+ export type ThemeTones = Record<string, number>;
44
+ /** Tone declarations while partial themes are being layered. */
45
+ export type PartialThemeTones = Record<string, number | undefined>;
38
46
  /**
39
47
  * A geometric color-theory variant **or** a fixed status family.
40
48
  *
@@ -110,11 +118,11 @@ export type ThemeAnchors = Record<string, ThemeAnchor>;
110
118
  * base color, `anchor:<name>.<slot>` for a sub-slot.
111
119
  *
112
120
  * An anchor source contributes its own hue **and chroma**; lightness
113
- * always comes from the mapping's ramp stop, and APCA enforcement
114
- * applies unchanged. Anchors are absolute sources rather than seed
115
- * rotations — a brand color is a color, not a relationship to the
116
- * seed — so they hold steady while contexts rebind roles around them
117
- * and intents replace the filled-action source.
121
+ * comes from the mapping's built-in stop or custom tone, and APCA
122
+ * enforcement applies unchanged. Anchors are absolute sources rather
123
+ * than seed rotations — a brand color is a color, not a relationship
124
+ * to the seed — so they hold steady while contexts rebind roles around
125
+ * them and intents replace the filled-action source.
118
126
  */
119
127
  export type MappingSource = ColorSource | `anchor:${string}`;
120
128
  /** Anchor names and slot names share one slug grammar. */
@@ -167,8 +175,8 @@ export interface SemanticMapping {
167
175
  * declared anchor as `anchor:<name>` / `anchor:<name>.<slot>`.
168
176
  */
169
177
  source: MappingSource;
170
- /** The lightness ramp position to apply. */
171
- lightness: LightnessKey;
178
+ /** A built-in ramp stop, or a named custom tone as `tone:<name>`. */
179
+ lightness: LightnessReference;
172
180
  }
173
181
  /** Full mapping table — one entry per semantic color token. */
174
182
  export type SemanticMappings = Record<SemanticColorName, SemanticMapping>;
@@ -229,14 +237,14 @@ export type ThemeRoles = {
229
237
  * cannot make a dark zone inside a light scheme because semantic derivation
230
238
  * deliberately takes lightness from the ramp rather than from an anchor.
231
239
  *
232
- * A context may also carry token-level `semanticMappings` (ESP0175).
233
- * Root-level explicit mappings survive into every context by design —
234
- * they are deliberate token pins so a zone that needs a different
235
- * value for one of them declares its own mapping here, which layers over
236
- * the inherited pin exactly as a root-level mapping layers over roles.
240
+ * A context may also carry custom `tones` and token-level
241
+ * `semanticMappings` (ESP0175, ESP0180). Root-level tones and explicit
242
+ * mappings survive into every context by design, while a zone may add or
243
+ * override a tone and pin a token locally. Each record layers by name.
237
244
  */
238
245
  export type ThemeContext = ThemeRoles & {
239
246
  lightness?: Partial<LightnessMap>;
247
+ tones?: PartialThemeTones;
240
248
  semanticMappings?: Partial<SemanticMappings>;
241
249
  };
242
250
  /** Named role-rebinding zones available to `context` attributes. */
@@ -279,6 +287,8 @@ export declare const ROLE_PAIRED_INK: Readonly<Record<string, {
279
287
  interface CompileRoleOptions {
280
288
  /** Token mappings declared explicitly at this merge boundary. */
281
289
  explicitMappings?: Partial<SemanticMappings>;
290
+ /** Custom tones available to explicit surface mappings. */
291
+ tones?: ThemeTones;
282
292
  /** Exact maximum paired-ink contrast of an action surface candidate. */
283
293
  actionSurfaceContrast?: (source: MappingSource, stop: LightnessKey) => number;
284
294
  /** ΔE-OK between an action surface candidate and the theme's background. */
@@ -458,6 +468,14 @@ export interface EspalierTheme {
458
468
  variantChroma: Partial<Record<VariantColorSource, number>>;
459
469
  /** Lightness values (0–1) for the eleven ramp positions. */
460
470
  lightness: LightnessMap;
471
+ /**
472
+ * Named custom lightness values for explicit semantic mappings.
473
+ *
474
+ * Refer to one as `tone:<name>` from `semanticMappings`. Tones do not
475
+ * become built-in ramp stops, emit `--esp-l-*` properties, or participate
476
+ * in automatic role and action-stop selection.
477
+ */
478
+ tones: ThemeTones;
461
479
  /** Per-semantic-token chroma min / max. */
462
480
  chroma: Record<SemanticColorName, ChromaRange>;
463
481
  /** Maps each semantic token to its color source + lightness. */
@@ -515,10 +533,11 @@ export interface EspalierTheme {
515
533
  * carry slot-only anchor overrides, which the resolved
516
534
  * {@link ThemeAnchor} deliberately does not permit.
517
535
  */
518
- export type PartialTheme = Omit<DeepPartial<EspalierTheme>, "anchors" | "contexts" | "dataRamps"> & {
536
+ export type PartialTheme = Omit<DeepPartial<EspalierTheme>, "anchors" | "contexts" | "dataRamps" | "tones"> & {
519
537
  anchors?: PartialThemeAnchors;
520
538
  contexts?: PartialThemeContexts;
521
539
  dataRamps?: PartialDataRamps;
540
+ tones?: PartialThemeTones;
522
541
  };
523
542
  /** Validation result returned by {@link validateTheme}. */
524
543
  export interface ThemeValidationResult {
@@ -566,6 +585,19 @@ export declare const STATUS_COLOR_SOURCES: readonly StatusColorSource[];
566
585
  export declare const VARIANT_COLOR_SOURCES: readonly VariantColorSource[];
567
586
  /** Valid keys for the lightness ramp. */
568
587
  export declare const LIGHTNESS_KEYS: readonly LightnessKey[];
588
+ /** Return the custom tone name carried by a valid `tone:<slug>` reference. */
589
+ export declare function toneReferenceName(reference: unknown): string | null;
590
+ /** True when a string names one of the eleven engine-owned ramp stops. */
591
+ export declare function isLightnessKey(reference: unknown): reference is LightnessKey;
592
+ /**
593
+ * Resolve the lightness an explicit semantic mapping requested.
594
+ *
595
+ * Custom tones are a lookup namespace only: this function never adds them to
596
+ * {@link LIGHTNESS_KEYS}, so automatic role and action-stop algorithms continue
597
+ * to consider exactly the built-in eleven. An invalid runtime reference falls
598
+ * back to `surface`; validation reports the authoring error before mounting.
599
+ */
600
+ export declare function resolveLightnessReference(lightness: LightnessMap, tones: ThemeTones | undefined, reference: LightnessReference): number;
569
601
  /**
570
602
  * Default lightness ramp for the **light** scheme.
571
603
  *
@@ -660,7 +692,7 @@ export declare function explicitMappingTokens(theme: EspalierTheme): Set<Semanti
660
692
  * Deep-merge a {@link PartialTheme} over a set of defaults.
661
693
  *
662
694
  * Primitive fields are replaced; nested objects (`angles`,
663
- * `semanticHues`, `lightness`, `chroma`, `semanticMappings`, `contexts`)
695
+ * `semanticHues`, `lightness`, `tones`, `chroma`, `semanticMappings`, `contexts`)
664
696
  * are merged key-by-key. Arrays (`stylesheets`) are replaced
665
697
  * wholesale.
666
698
  *
@@ -675,10 +707,10 @@ export declare function mergeTheme(defaults: EspalierTheme, overrides: PartialTh
675
707
  *
676
708
  * This is the same compilation `esp-element-base` performs for a zone
677
709
  * host, exported so a check script or the fit report can inspect a
678
- * context surface without a DOM: role rebindings and the partial
679
- * lightness ramp merge over the root theme, and any context-level
680
- * `semanticMappings` layer on top as explicit token pins. Returns `null`
681
- * when the theme does not define the context.
710
+ * context surface without a DOM: role rebindings, the partial lightness ramp,
711
+ * and custom tones merge over the root theme, and any context-level
712
+ * `semanticMappings` layer on top as explicit token pins. Returns `null` when
713
+ * the theme does not define the context.
682
714
  *
683
715
  * @param theme A resolved theme (as `mergeTheme` returns).
684
716
  * @param name The context name a zone would set in its attribute.
@@ -711,7 +743,7 @@ export declare function validateThemePair(lightBase64: string, darkBase64: strin
711
743
  * and therefore need key-by-key merging instead of wholesale
712
744
  * replacement when combining two {@link PartialTheme} objects.
713
745
  */
714
- export declare const NESTED_THEME_KEYS: readonly ["anchors", "angles", "contexts", "dataPalette", "dataRamps", "intents", "roles", "chroma", "lightness", "semanticHues", "semanticMappings", "variantChroma"];
746
+ export declare const NESTED_THEME_KEYS: readonly ["anchors", "angles", "contexts", "dataPalette", "dataRamps", "intents", "roles", "chroma", "lightness", "tones", "semanticHues", "semanticMappings", "variantChroma"];
715
747
  /**
716
748
  * Deep-merge two {@link PartialTheme} objects.
717
749
  *