@taprootio/espalier 2.16.0 → 2.18.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.
- package/custom-elements.json +1416 -1065
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/pickers/esp-picker-menu.js +1 -1
- package/dist/root/helpers/compute-theme-properties.js +1 -1
- package/dist/root/helpers/compute-variants.js +1 -1
- package/dist/shared/color-engine.js +1 -1
- package/dist/shared/esp-element-base.js +1 -1
- package/dist/shared/theme-fit-report.d.ts +162 -0
- package/dist/shared/theme-fit-report.js +1 -0
- package/dist/shared/theme-fit-trace.js +0 -0
- package/dist/shared/theme.d.ts +220 -5
- package/dist/shared/theme.js +1 -1
- package/dist/shared/variant-engine.js +1 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -73,3 +73,4 @@ export { type TypeaheadFetchItems } from "./pickers/types.js";
|
|
|
73
73
|
export { type EspalierTheme, type PartialTheme, type VariantColorSource, encodeTheme, parseTheme, mergePartials, layerThemes, buildTaprootLightTheme, buildTaprootDarkTheme, NESTED_THEME_KEYS, } from "./shared/theme.js";
|
|
74
74
|
export { WEIGHT_LABELS, extractWeights, normalizeWeight, bestAvailableWeight, extractFamily, getFallbackFont, } from "./shared/font-helpers.js";
|
|
75
75
|
export { getGoogleFonts } from "./font-picker/esp-font-picker.js";
|
|
76
|
+
export { themeFitReport, themeFitRows, printThemeFitReport, type ThemeFitReport, type ThemeFitToken, type ThemeFitApcaAction, type ThemeFitAnchor, type ThemeFitRow, } from "./shared/theme-fit-report.js";
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export*from"./box/esp-box.js";export*from"./action-menu/esp-action-menu.js";export*from"./action-menu/esp-action-menu-item.js";export*from"./avatar/esp-avatar.js";export*from"./avatar/esp-profile-chip.js";export*from"./badge/esp-badge.js";export*from"./checkbox/esp-checkbox.js";export*from"./checkbox/esp-checkbox-group.js";export*from"./data-cell/esp-data-cell.js";export*from"./radio-button/esp-radio-button.js";export*from"./radio-button/esp-radio-button-group.js";export*from"./repeater/esp-repeater.js";export*from"./breadcrumbs/esp-breadcrumbs.js";export*from"./button/esp-button.js";export*from"./button/esp-button-group.js";export*from"./color-picker/esp-color-picker.js";export*from"./flyout/esp-flyout.js";export*from"./form/esp-form.js";export*from"./form-item/esp-form-item.js";export*from"./help/esp-help-button.js";export*from"./help/esp-help-provider.js";export*from"./header/index.js";export*from"./page/esp-page.js";export*from"./popover/esp-popover.js";export*from"./dialog/esp-dialog.js";export*from"./empty-state/esp-empty-state.js";export*from"./image-upload/esp-image-upload.js";export*from"./file-upload/esp-file-upload.js";export*from"./font-picker/esp-font-picker.js";export*from"./focus-picker/esp-focus-picker.js";export*from"./footer/index.js";export*from"./grid/esp-grid.js";export*from"./image/esp-image.js";export*from"./image/image-focus.js";export*from"./lightbox/esp-lightbox.js";export*from"./info/esp-info.js";export*from"./input/esp-input.js";export*from"./textarea/esp-textarea.js";export*from"./pickers/esp-pick-one.js";export*from"./pickers/esp-pick-some.js";export*from"./pickers/esp-picker-menu.js";export*from"./pickers/esp-picker-item.js";export*from"./root/esp-root.js";export*from"./menu/index.js";export*from"./burger/esp-burger.js";export*from"./date-picker/esp-date-picker.js";export*from"./details/esp-details.js";export*from"./details/esp-details-group.js";export*from"./tabs/esp-tab.js";export*from"./tabs/esp-tab-group.js";export*from"./slider/esp-slider.js";export*from"./switch/esp-switch.js";export*from"./toaster/esp-toaster.js";export*from"./tooltip/esp-tooltip.js";export*from"./progress/esp-progress.js";export*from"./search/esp-search.js";export*from"./status-indicator/esp-status-indicator.js";export*from"./tree/esp-tree.js";export*from"./tree/esp-tree-item.js";import{DEFAULT_ICON_SPRITE_URL as xo,DEFAULT_ICON_VIEW_BOX as ao,INTENT_VARIANTS as lo,normalizeIntentVariant as so,getIconHref as
|
|
1
|
+
export*from"./box/esp-box.js";export*from"./action-menu/esp-action-menu.js";export*from"./action-menu/esp-action-menu-item.js";export*from"./avatar/esp-avatar.js";export*from"./avatar/esp-profile-chip.js";export*from"./badge/esp-badge.js";export*from"./checkbox/esp-checkbox.js";export*from"./checkbox/esp-checkbox-group.js";export*from"./data-cell/esp-data-cell.js";export*from"./radio-button/esp-radio-button.js";export*from"./radio-button/esp-radio-button-group.js";export*from"./repeater/esp-repeater.js";export*from"./breadcrumbs/esp-breadcrumbs.js";export*from"./button/esp-button.js";export*from"./button/esp-button-group.js";export*from"./color-picker/esp-color-picker.js";export*from"./flyout/esp-flyout.js";export*from"./form/esp-form.js";export*from"./form-item/esp-form-item.js";export*from"./help/esp-help-button.js";export*from"./help/esp-help-provider.js";export*from"./header/index.js";export*from"./page/esp-page.js";export*from"./popover/esp-popover.js";export*from"./dialog/esp-dialog.js";export*from"./empty-state/esp-empty-state.js";export*from"./image-upload/esp-image-upload.js";export*from"./file-upload/esp-file-upload.js";export*from"./font-picker/esp-font-picker.js";export*from"./focus-picker/esp-focus-picker.js";export*from"./footer/index.js";export*from"./grid/esp-grid.js";export*from"./image/esp-image.js";export*from"./image/image-focus.js";export*from"./lightbox/esp-lightbox.js";export*from"./info/esp-info.js";export*from"./input/esp-input.js";export*from"./textarea/esp-textarea.js";export*from"./pickers/esp-pick-one.js";export*from"./pickers/esp-pick-some.js";export*from"./pickers/esp-picker-menu.js";export*from"./pickers/esp-picker-item.js";export*from"./root/esp-root.js";export*from"./menu/index.js";export*from"./burger/esp-burger.js";export*from"./date-picker/esp-date-picker.js";export*from"./details/esp-details.js";export*from"./details/esp-details-group.js";export*from"./tabs/esp-tab.js";export*from"./tabs/esp-tab-group.js";export*from"./slider/esp-slider.js";export*from"./switch/esp-switch.js";export*from"./toaster/esp-toaster.js";export*from"./tooltip/esp-tooltip.js";export*from"./progress/esp-progress.js";export*from"./search/esp-search.js";export*from"./status-indicator/esp-status-indicator.js";export*from"./tree/esp-tree.js";export*from"./tree/esp-tree-item.js";import{DEFAULT_ICON_SPRITE_URL as xo,DEFAULT_ICON_VIEW_BOX as ao,INTENT_VARIANTS as lo,normalizeIntentVariant as so,getIconHref as io,getIconHrefForHost as To,getIconSpriteUrl as Eo}from"./shared/intent-values.js";import{EspalierElementBase as go}from"./shared/esp-element-base.js";import{VALIDITY_CHANGED_EVENT as Fo}from"./shared/validation.js";import{FormFieldController as _o}from"./shared/form-field-controller.js";import{traverseToClosest as uo}from"./shared/utilities.js";import{showToast as Lo}from"./shared/toast-events.js";import{showFlyout as Do,closeFlyout as Ho}from"./shared/flyout-events.js";import{requestHelp as So}from"./shared/help-events.js";import{getEspBus as yo}from"./shared/bus-events.js";export*from"./shared/events.js";import{getImageDetails as Vo,releasePreviewUrl as Wo}from"./image-upload/image-helpers.js";import{calculatePhotoLayout as Bo}from"./shared/justified-layout.js";import{encodeTheme as vo,parseTheme as Go,mergePartials as Oo,layerThemes as ko,buildTaprootLightTheme as zo,buildTaprootDarkTheme as Yo,NESTED_THEME_KEYS as qo}from"./shared/theme.js";import{WEIGHT_LABELS as Mo,extractWeights as Xo,normalizeWeight as jo,bestAvailableWeight as Jo,extractFamily as Qo,getFallbackFont as Zo}from"./shared/font-helpers.js";import{getGoogleFonts as or}from"./font-picker/esp-font-picker.js";import{themeFitReport as er,themeFitRows as tr,printThemeFitReport as mr}from"./shared/theme-fit-report.js";export{xo as DEFAULT_ICON_SPRITE_URL,ao as DEFAULT_ICON_VIEW_BOX,go as EspalierElementBase,_o as FormFieldController,lo as INTENT_VARIANTS,qo as NESTED_THEME_KEYS,Fo as VALIDITY_CHANGED_EVENT,Mo as WEIGHT_LABELS,Jo as bestAvailableWeight,Yo as buildTaprootDarkTheme,zo as buildTaprootLightTheme,Bo as calculatePhotoLayout,Ho as closeFlyout,vo as encodeTheme,Qo as extractFamily,Xo as extractWeights,yo as getEspBus,Zo as getFallbackFont,or as getGoogleFonts,io as getIconHref,To as getIconHrefForHost,Eo as getIconSpriteUrl,Vo as getImageDetails,ko as layerThemes,Oo as mergePartials,so as normalizeIntentVariant,jo as normalizeWeight,Go as parseTheme,mr as printThemeFitReport,Wo as releasePreviewUrl,So as requestHelp,Do as showFlyout,Lo as showToast,er as themeFitReport,tr as themeFitRows,uo as traverseToClosest};
|
|
@@ -9,7 +9,7 @@ var n=function(d,t,e,i){var l=arguments.length,s=l<3?t:i===null?i=Object.getOwnP
|
|
|
9
9
|
style=${P(i)}
|
|
10
10
|
>
|
|
11
11
|
${t.slotNodes?.map(l=>l.cloneNode(!0))}
|
|
12
|
-
</esp-picker-item>`},this.internals.role="listbox"}connectedCallback(){super.connectedCallback(),this.setAttribute("popover","manual"),this.hasAttribute("role")||this.setAttribute("role","listbox")}updated(t){super.updated(t),t.has("multiSelect")&&(this.internals.ariaMultiSelectable=String(this.multiSelect),this.setAttribute("aria-multiselectable",String(this.multiSelect))),t.has("label")&&(this.internals.ariaLabel=this.label,this.label?this.setAttribute("aria-label",this.label):this.removeAttribute("aria-label")),(t.has("virtualizedItemsPool")||t.has("loading"))&&this.maxAvailableHeight>0&&this.matches(":popover-open")&&this.resizeAndHighlight(t.has("virtualizedItemsPool"))}get pickerItems(){return this.virtualizedItemsPool}set pickerItems(t){const e=this.virtualizedItemsPool.filter(s=>s.selected).map(s=>s.value);this.virtualizedItemsPool=t;const i=t.filter(s=>s.selected),l=i.map(s=>s.value);(e.length!==l.length||e.some((s,r)=>s!==l[r]))&&this.dispatchEvent(new CustomEvent("selection-changed",{detail:i,bubbles:!0,composed:!0}))}setHighlight(t){const e=this.virtualizedItemsPool;if(e.length===0)return;(this.highlightIndex<0||this.highlightIndex>e.length-1)&&(this.highlightIndex=t);const i=this.virtualizerRef.value;i&&i.scrollToIndex(this.highlightIndex,"nearest"),this.applyHighlightClass()}async setHighlightAsync(t){const e=this.virtualizedItemsPool;if(e.length===0)return;(this.highlightIndex<0||this.highlightIndex>e.length-1)&&(this.highlightIndex=t);const i=this.virtualizerRef.value;i&&
|
|
12
|
+
</esp-picker-item>`},this.internals.role="listbox"}connectedCallback(){super.connectedCallback(),this.setAttribute("popover","manual"),this.hasAttribute("role")||this.setAttribute("role","listbox")}updated(t){super.updated(t),t.has("multiSelect")&&(this.internals.ariaMultiSelectable=String(this.multiSelect),this.setAttribute("aria-multiselectable",String(this.multiSelect))),t.has("label")&&(this.internals.ariaLabel=this.label,this.label?this.setAttribute("aria-label",this.label):this.removeAttribute("aria-label")),(t.has("virtualizedItemsPool")||t.has("loading"))&&this.maxAvailableHeight>0&&this.matches(":popover-open")&&this.resizeAndHighlight(t.has("virtualizedItemsPool"))}get pickerItems(){return this.virtualizedItemsPool}set pickerItems(t){const e=this.virtualizedItemsPool.filter(s=>s.selected).map(s=>s.value);this.virtualizedItemsPool=t;const i=t.filter(s=>s.selected),l=i.map(s=>s.value);(e.length!==l.length||e.some((s,r)=>s!==l[r]))&&this.dispatchEvent(new CustomEvent("selection-changed",{detail:i,bubbles:!0,composed:!0}))}setHighlight(t){const e=this.virtualizedItemsPool;if(e.length===0)return;(this.highlightIndex<0||this.highlightIndex>e.length-1)&&(this.highlightIndex=t);const i=this.virtualizerRef.value;i&&i.scrollToIndex(this.highlightIndex,"nearest"),this.applyHighlightClass()}async setHighlightAsync(t){const e=this.virtualizedItemsPool;if(e.length===0)return;(this.highlightIndex<0||this.highlightIndex>e.length-1)&&(this.highlightIndex=t);const i=this.virtualizerRef.value;if(i)try{const l=i.layoutComplete;l&&await l,i.scrollToIndex(this.highlightIndex,"nearest");const s=i.layoutComplete;s&&await s}catch{return}this.applyHighlightClass()}applyHighlightClass(){this.clearHighlight();const t=this.getHighlightedElement();t&&t.classList.add(c)}clearHighlight(){const t=this.virtualizerRef.value;if(t)for(const e of t.querySelectorAll(`esp-picker-item.${c}`))e.classList.remove(c)}resetHighlight(){this.clearHighlight();const t=this.virtualizedItemsPool.find(e=>e.selected);this.highlightIndex=t?this.virtualizedItemsPool.indexOf(t):-1}applyHighlightWithRetry(t){if(!this.isConnected||!this.matches(":popover-open"))return;this.clearHighlight();const e=this.getHighlightedElement();if(e){e.classList.add(c);return}t>0&&requestAnimationFrame(()=>this.applyHighlightWithRetry(t-1))}getHighlightedElement(){if(this.highlightIndex<0)return null;const t=this.virtualizerRef.value;if(!t)return null;const e=t.querySelectorAll("esp-picker-item");for(const i of e)if(this.virtualizedItemsPool.findIndex(s=>s.value===i.value&&s.text===i.text)===this.highlightIndex)return i;return null}pickItem(t){for(const i of this.pickerItems)i===t?i.selected=!i.selected:this.multiSelect||(i.selected=!1);const e=this.virtualizedItemsPool.indexOf(t);if(e!==-1){this.highlightIndex=e,this.clearHighlight();const i=this.getHighlightedElement();i&&i.classList.add(c)}this.pickerItems=[...this.pickerItems],this.dispatchEvent(new CustomEvent("selection-changed",{detail:this.pickerItems.filter(i=>i.selected),bubbles:!0,composed:!0})),this.multiSelect||this.dispatchEvent(new CustomEvent("close-menu",{detail:this.pickerItems.filter(i=>i.selected),bubbles:!0,composed:!0}))}selectItemByIndex(t){const e=Number.isInteger(t)?this.virtualizedItemsPool[t]:void 0;return e?(this.pickItem(e),!0):!1}getPickerItemElementAtPoint(t,e){const i=this.shadowRoot?.querySelectorAll("esp-picker-item[data-picker-index]");if(!i)return null;for(const l of i){const s=l.getBoundingClientRect();if(t>=s.left&&t<=s.right&&e>=s.top&&e<=s.bottom)return l}return null}selectPickerItemElement(t){return t?this.selectItemByIndex(Number(t.dataset.pickerIndex)):!1}selectItemAtPoint(t,e){return this.selectPickerItemElement(this.getPickerItemElementAtPoint(t,e))}selectItemFromPointerEvent(t){const e=t.composedPath().find(l=>l instanceof HTMLElement&&l.matches("esp-picker-item[data-picker-index]"));return this.selectPickerItemElement(e??this.getPickerItemElementAtPoint(t.clientX,t.clientY))?(t.preventDefault(),t.stopPropagation(),!0):!1}positionSelf(t){const e=t.getBoundingClientRect(),{height:i}=u(),{spaceAbove:l,spaceBelow:s}=f(e,i),r=Math.max(Math.max(l,s)-75,0);this.maxAvailableHeight=r,this.lastViewportHeight=i,this.style.setProperty("height",`${r}px`),this.style.setProperty("min-width",`${e.width}px`),this.style.setProperty("left",`${e.left}px`),this.style.removeProperty("top"),this.style.removeProperty("bottom"),l>s?this.style.setProperty("bottom",`${i-e.top}px`):this.style.setProperty("top",`${e.bottom}px`),this.classList.remove("visible"),this.matches(":popover-open")||this.showPopover();const o=this.pickerItems.find(a=>a.selected);this.highlightIndex=o?this.pickerItems.indexOf(o):0,this.setHighlightAsync(0).then(()=>{requestAnimationFrame(()=>{this.shrinkToFit(r),this.widenToFitContent(e),this.classList.add("visible")})})}async resizeAndHighlight(t){try{await this.updateComplete}catch{return}if(!this.isConnected||!this.matches(":popover-open"))return;const e=this.virtualizerRef.value;if(!e){requestAnimationFrame(()=>{if(!this.isConnected)return;const s=this.shadowRoot?.querySelector(".status-message");if(s){const r=Math.min(s.scrollHeight,this.maxAvailableHeight);this.style.setProperty("height",`${r}px`)}});return}try{const s=e.layoutComplete;s&&await s}catch{return}if(!this.isConnected||!this.matches(":popover-open")||(e.scrollTop=0,this.sizeToContent(this.maxAvailableHeight),!t))return;try{const s=e.layoutComplete;s&&await s}catch{return}if(!this.isConnected||!this.matches(":popover-open"))return;const i=this.virtualizedItemsPool;if(i.length===0)return;const l=i.findIndex(s=>s.selected);l>=0?this.highlightIndex=l:this.highlightIndex>=i.length?this.highlightIndex=i.length-1:this.highlightIndex<0&&(this.highlightIndex=0);try{e.scrollToIndex(this.highlightIndex,"nearest")}catch{}try{const s=e.layoutComplete;s&&await s}catch{return}!this.isConnected||!this.matches(":popover-open")||this.applyHighlightWithRetry(3)}sizeToContent(t){const e=this.measureContentHeight();e>0&&this.style.setProperty("height",`${Math.min(e,t)}px`)}shrinkToFit(t){const e=this.measureContentHeight();e>2&&e<t&&this.style.setProperty("height",`${e}px`)}measureContentHeight(){const t=this.virtualizerRef.value;if(!t)return 0;const e=t.querySelectorAll("esp-picker-item");if(e.length===0)return 0;const i=getComputedStyle(this),l=(parseFloat(i.borderTopWidth)||0)+(parseFloat(i.borderBottomWidth)||0),s=this.virtualizedItemsPool.length;if(e.length<s)return t.scrollHeight+l;t.scrollTop=0;const r=t.getBoundingClientRect();return e[e.length-1].getBoundingClientRect().bottom-r.top+l}widenToFitContent(t){const e=this.virtualizerRef.value;if(!e)return;const i=e.querySelectorAll("esp-picker-item");let l=t.width;for(const s of i)l=Math.max(l,s.scrollWidth);if(l+=2,l>t.width){const{width:s}=u(),r=Math.min(l,s-t.left);this.style.setProperty("width",`${r}px`)}}updatePosition(t){const e=t.getBoundingClientRect(),{height:i}=u();if(i!==this.lastViewportHeight){const{spaceAbove:r,spaceBelow:o}=f(e,i),a=Math.max(Math.max(r,o)-75,0);this.maxAvailableHeight=a,this.lastViewportHeight=i,(parseFloat(this.style.getPropertyValue("height"))||0)>a&&this.style.setProperty("height",`${a}px`),this.sizeToContent(a)}this.style.setProperty("left",`${e.left}px`);const l=e.top,s=i-e.bottom;l>s?(this.style.removeProperty("top"),this.style.setProperty("bottom",`${i-e.top}px`)):(this.style.removeProperty("bottom"),this.style.setProperty("top",`${e.bottom}px`))}hideMenu(){this.classList.remove("visible");try{this.hidePopover()}catch{}}doKeyboardNav(t){const e=this.virtualizedItemsPool;if(e.length!==0)switch(t){case"ArrowDown":this.highlightIndex+=1,this.setHighlight(0);break;case"ArrowUp":this.highlightIndex-=1,this.setHighlight(e.length-1);break;case"Home":this.highlightIndex=0,this.setHighlight(0);break;case"End":this.highlightIndex=e.length-1,this.setHighlight(e.length-1);break;case"Enter":if(this.highlightIndex===-1)return;this.pickItem(e[this.highlightIndex]);break}}render(){return this.loading?p`<div class="status-message">Searching…</div>`:this.virtualizedItemsPool.length===0&&this.emptyMessage?p`<div class="status-message">${this.emptyMessage}</div>`:p`<lit-virtualizer
|
|
13
13
|
${b(this.virtualizerRef)}
|
|
14
14
|
scroller
|
|
15
15
|
.items=${this.virtualizedItemsPool}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{parseCssColor as
|
|
1
|
+
import{parseCssColor as I,serializeOklch as b,gamutMapToSRGB as M,deriveSemantic as O,deriveSemanticWithContrast as R}from"../../shared/color-engine.js";import{generateScaleProperties as x}from"../../shared/scale-engine.js";import{SEMANTIC_COLOR_NAMES as B,LIGHTNESS_KEYS as w,TOKEN_PAIRINGS as T,semanticToCSS as u}from"../../shared/theme.js";import{lightnessKeyToCSS as E}from"./lightness-key-to-css.js";import{computeVariants as L,resolveMappingSource as N}from"./compute-variants.js";function W(o){return{rootFontSize:o.rootFontSize,typeRatio:o.typeRatio,spaceRatio:o.spaceRatio,borderRadius:o.borderRadius,viewportMin:o.viewportMin,viewportMax:o.viewportMax}}function K(o,C,e){const v=I(o.seedColor);if(!v)return{};const n={};n["--esp-scheme"]=C,n["--esp-seed-color"]=o.seedColor;const f=L(v,o);for(const[a,s]of Object.entries(f))n[`--esp-color-${a}`]=b(M(s));for(const a of w)n[E(a)]=`${Math.round(o.lightness[a]*100)}%`;const m=T,c=new Set,k=a=>{const s=N(a,f,o.anchors);return s||(c.has(a)||(c.add(a),console.warn(`Espalier theme: color source "${a}" cannot be resolved \u2014 falling back to the seed. Run validateTheme for the declared anchors.`)),f.primary)},l={};for(const a of B){if(m[a])continue;const s=o.semanticMappings[a],r=o.chroma[a],i=k(s.source),g=o.lightness[s.lightness],p=O(i,g,r.min,r.max);if(l[a]=p,n[u(a)]=b(p),e){const t=u(a);e.push({token:a,property:t,resolved:n[t],source:s.source,lightness:s.lightness,sourceResolved:!c.has(s.source),base:i})}}for(const a of B){const s=m[a];if(!s)continue;const r=o.semanticMappings[a],i=o.chroma[a],g=k(r.source),p=o.lightness[r.lightness],t=l[s.bg];let y;const S=R(g,p,i.min,i.max,t,s.targetLc,e&&(d=>{y=d}));if(l[a]=S,n[u(a)]=b(S),e){const d=u(a);e.push({token:a,property:d,resolved:n[d],source:r.source,lightness:r.lightness,sourceResolved:!c.has(r.source),base:g,requested:y,contrast:{against:s.bg,background:t,targetLc:s.targetLc}})}}return Object.assign(n,x(W(o))),o.fontBody&&(n["--esp-font-body"]=o.fontBody),o.fontHeadings&&(n["--esp-font-headings"]=o.fontHeadings),o.fontBrand&&(n["--esp-font-brand"]=o.fontBrand),o.fontMonospace&&(n["--esp-font-monospace"]=o.fontMonospace),n["--esp-font-weight-body"]=o.fontWeightBody,n["--esp-font-weight-headings"]=o.fontWeightHeadings,n["--esp-font-weight-brand"]=o.fontWeightBrand,n["--esp-font-weight-monospace"]=o.fontWeightMonospace,o.pageBackgroundImage&&(n["--esp-page-background-image"]=o.pageBackgroundImage),o.pageBackgroundImageOpacity!==void 0&&(n["--esp-page-background-image-opacity"]=o.pageBackgroundImageOpacity.toString()),o.boxBackgroundImage&&(n["--esp-box-background-image"]=o.boxBackgroundImage),o.boxBackgroundImageOpacity!==void 0&&(n["--esp-box-background-image-opacity"]=o.boxBackgroundImageOpacity.toString()),o.vellumOpacity!==void 0&&(n["--esp-vellum-opacity"]=o.vellumOpacity.toString()),o.vellumBackgroundImage&&(n["--esp-vellum-background-image"]=o.vellumBackgroundImage),o.vellumBackgroundImageOpacity!==void 0&&(n["--esp-vellum-background-image-opacity"]=o.vellumBackgroundImageOpacity.toString()),n}export{K as computeThemeProperties};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{computeVariants as e}from"../../shared/variant-engine.js";import{resolveMappingSource as t}from"../../shared/variant-engine.js";export{e as computeVariants,t as resolveMappingSource};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const S=Math.PI/180,h=1e-4
|
|
1
|
+
const S=Math.PI/180,h=1e-4,X=50,m="[+-]?(?:\\d+|\\d*\\.\\d+)(?:[eE][+-]?\\d+)?",i="[\\t\\n\\f\\r ]",y=new RegExp(i+"*,"+i+"*"),H=new RegExp(i+"+");function G(t){return t.replace(/\/\*[\s\S]*?(?:\*\/|$)/g," ").replace(/^[\t\n\f\r ]+|[\t\n\f\r ]+$/g,"")}const b={"":1,deg:1,grad:.9,rad:180/Math.PI,turn:360},U=.4,V=new RegExp("^oklch\\("+i+"*("+m+")(%?)"+i+"+("+m+")(%?)"+i+"+("+m+")(deg|grad|rad|turn)?"+i+"*\\)$","i");function x(t){const n=t.h*S;return{L:t.l,a:t.c*Math.cos(n),b:t.c*Math.sin(n)}}function W(t){const n=t.L+.3963377774*t.a+.2158037573*t.b,r=t.L-.1055613458*t.a-.0638541728*t.b,e=t.L-.0894841775*t.a-1.291485548*t.b,o=n*n*n,c=r*r*r,u=e*e*e;return{r:4.0767416621*o-3.3077115913*c+.2309699292*u,g:-1.2684380046*o+2.6097574011*c-.3413193965*u,b:-.0041960863*o-.7034186147*c+1.707614701*u}}function A(t){return t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055}function z(t){return{r:A(t.r),g:A(t.g),b:A(t.b)}}function Y(t){return t.r>=-h&&t.r<=1+h&&t.g>=-h&&t.g<=1+h&&t.b>=-h&&t.b<=1+h}function g(t){return Math.max(0,Math.min(1,t))}function q(t){const n=G(t).match(V);if(!n)return null;let r=parseFloat(n[1]);n[2]==="%"&&(r/=100);let e=parseFloat(n[3]);n[4]==="%"&&(e=e/100*U);const o=parseFloat(n[5])*b[(n[6]??"").toLowerCase()];return!isFinite(r)||!isFinite(e)||!isFinite(o)?null:{l:Math.max(0,Math.min(1,r)),c:Math.max(0,e),h:(o%360+360)%360}}function ut(t){const n=p(t.l,6),r=p(t.c,6),e=p((t.h%360+360)%360,4);return`oklch(${n} ${r} ${e})`}function _(t){return z(W(x(t)))}function T(t){return Y(_(t))}function M(t){if(t.c<=h||T(t))return{...t};let n=0,r=t.c,e={...t};for(let o=0;o<X;o++){const c=(n+r)/2;if(e={l:t.l,c,h:t.h},T(e)?n=c:r=c,r-n<h)break}return{l:t.l,c:n,h:t.h}}function K(t,n,r){const e={l:t.l,c:Math.max(n,Math.min(r,t.c)),h:t.h};return M(e)}const C=.022,O=.012,w=.022,k=.57,B=.56,P=1.14,d=.001,R=.027;function I(t){const n=Math.pow(g(t.r),2.4),r=Math.pow(g(t.g),2.4),e=Math.pow(g(t.b),2.4);return .2126729*n+.7151522*r+.072175*e}function F(t,n){const r=_(M(t)),e=_(M(n));let o=I(r),c=I(e);if(o=o>C?o:o+Math.pow(C-o,1.414),c=c>C?c:c+Math.pow(C-c,1.414),Math.abs(c-o)<d)return 0;let u;if(c>o){const s=Math.pow(c,B),a=Math.pow(o,k);if(u=(s-a)*P,u<d)return 0;u=u<R?u-u*w*R:u-w}else{const s=Math.pow(c,B),a=Math.pow(o,k);if(u=(s-a)*P,u>-d)return 0;u=u>-R?u-u*O*R:u+O}return u*100}function j(t,n,r=60){let e=M({...t}),o=Math.abs(F(e,n));if(o>=r)return e;const c=n.l>.5,u=.01,s=c?0:1;for(let a=0;a<100;a++){if(c?e.l=Math.max(s,e.l-u):e.l=Math.min(s,e.l+u),e=M(e),o=Math.abs(F(e,n)),o>=r)return e;if(e.l<=0||e.l>=1)break}return e}function st(t,n){return((t+n)%360+360)%360}function at(t,n){const r=x(t),e=x(n),o=r.L-e.L,c=r.a-e.a,u=r.b-e.b;return Math.sqrt(o*o+c*c+u*u)}function J(t,n,r,e){return K({l:n,c:t.c,h:t.h},r,e)}function it(t,n,r,e,o,c=60,u){const s=J(t,n,r,e);return u?.(s),j(s,o,c)}function E(t){return t<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4)}function Q(t){const n=E(t.r),r=E(t.g),e=E(t.b),o=Math.cbrt(.4122214708*n+.5363325363*r+.0514459929*e),c=Math.cbrt(.2119034982*n+.6806995451*r+.1073969566*e),u=Math.cbrt(.0883024619*n+.2817188376*r+.6299787005*e);return{L:.2104542553*o+.793617785*c-.0040720468*u,a:1.9779984951*o-2.428592205*c+.4505937099*u,b:.0259040371*o+.7827717662*c-.808675766*u}}function Z(t){const n=Math.sqrt(t.a*t.a+t.b*t.b),r=(Math.atan2(t.b,t.a)/S%360+360)%360;return{l:t.L,c:n,h:r}}function tt(t){const n=p(t.c,4);return{l:p(t.l,3),c:n,h:n===0?0:p(t.h,1)}}function f(t){return Math.round(g(t)*255)}function L(t){const n=Z(Q(t)),r=t.r===t.g&&t.g===t.b?{l:n.l,c:0,h:0}:n,e=tt(r);if(T(e)){const o=_(e);if(f(o.r)===f(t.r)&&f(o.g)===f(t.g)&&f(o.b)===f(t.b))return e}return r}function nt(t){const n=t.slice(1);if(!/^[0-9a-f]+$/i.test(n))return null;if(n.length===3){const[r,e,o]=n.split("").map(c=>parseInt(c+c,16));return{r:r/255,g:e/255,b:o/255}}return n.length===6?{r:parseInt(n.slice(0,2),16)/255,g:parseInt(n.slice(2,4),16)/255,b:parseInt(n.slice(4,6),16)/255}:null}function D(t,n){const r=t.match(new RegExp("^"+n+"\\("+i+"*([^)]*?)"+i+"*\\)$","i"));if(!r)return null;const e=r[1];if(e.includes("/"))return null;const o=e.includes(","),c=e.split(o?y:H);return c.length!==3||c.some(u=>u.length===0)?null:{args:c,legacy:o}}const rt=new RegExp("^("+m+")([a-z%]*)$","i");function N(t){const n=t.match(rt);if(!n)return null;const r=parseFloat(n[1]);return isFinite(r)?{value:r,unit:n[2].toLowerCase()}:null}function et(t){if(t.unit!==""&&t.unit!=="%")return null;const n=t.unit==="%"?t.value/100:t.value/255;return Math.max(0,Math.min(1,n))}function ot(t){const n=b[t.unit];return n===void 0?null:t.value*n}function v(t){return t.unit!==""&&t.unit!=="%"?null:Math.max(0,Math.min(1,t.value/100))}function ct(t,n,r){const e=(t%360+360)%360,o=c=>{const u=(c+e/30)%12,s=n*Math.min(r,1-r);return r-s*Math.max(-1,Math.min(u-3,9-u,1))};return{r:o(0),g:o(8),b:o(4)}}const lt='"#rrggbb", "#rgb", "rgb(\u2026)", "hsl(\u2026)", "oklch(\u2026)"';function ht(t){const n=G(t);if(n.startsWith("#")){const r=nt(n);return r?L(r):null}if(/^oklch\(/i.test(n))return q(n);if(/^rgba?\(/i.test(n)){const r=D(n,"rgba?");if(!r)return null;const e=r.args.map(N);if(e.some(l=>l===null))return null;const o=e;if(r.legacy&&new Set(o.map(l=>l.unit)).size>1)return null;const c=o.map(et);if(c.some(l=>l===null))return null;const[u,s,a]=c;return L({r:u,g:s,b:a})}if(/^hsla?\(/i.test(n)){const r=D(n,"hsla?");if(!r)return null;const e=r.args.map(N);if(e.some($=>$===null))return null;const[o,c,u]=e;if(r.legacy&&(c.unit!=="%"||u.unit!=="%"))return null;const s=ot(o),a=v(c),l=v(u);return s===null||a===null||l===null?null:L(ct(s,a,l))}return null}function p(t,n){const r=Math.pow(10,n);return Math.round(t*r)/r}export{lt as ACCEPTED_COLOR_FORMS,F as apcaContrast,K as clampChromaAndGamutMap,at as deltaEOK,J as deriveSemantic,it as deriveSemanticWithContrast,j as ensureContrast,M as gamutMapToSRGB,T as isInSRGBGamut,_ as oklchToSRGB,ht as parseCssColor,q as parseOklch,st as rotateHue,ut as serializeOklch};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var p=function(
|
|
1
|
+
var p=function(v,e,t,o){var r=arguments.length,s=r<3?e:o===null?o=Object.getOwnPropertyDescriptor(e,t):o,a;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")s=Reflect.decorate(v,e,t,o);else for(var c=v.length-1;c>=0;c--)(a=v[c])&&(s=(r<3?a(s):r>3?a(e,t,s):a(e,t))||s);return r>3&&s&&Object.defineProperty(e,t,s),s};import{css as x,LitElement as A}from"lit";import{property as y,state as U}from"lit/decorators.js";import{subscribeToRootEvent as f}from"./root-event-subscription.js";import{traverseToClosest as T}from"./utilities.js";import{parseCssColor as O,serializeOklch as k,gamutMapToSRGB as F,deriveSemantic as _,deriveSemanticWithContrast as z}from"./color-engine.js";import{computeVariants as S,resolveMappingSource as D}from"../root/helpers/compute-variants.js";import{SEMANTIC_COLOR_NAMES as P,TOKEN_PAIRINGS as I,semanticToCSS as w}from"./theme.js";import{alignAttributeTextInheritance as M,focusRing as B}from"./style-fragments.js";class n extends A{constructor(){super(...arguments),this.seedColorBacker="oklch(0.7 0.125 216)",this.espRoot=null,this.variantTokenOriginalValues=new Map,this.rootEventSubscriptionsActive=!1,this.subscribedRoot=null,this.rootEventUnsubscribers=[],this.variantBacker="primary",this.correlationId=globalThis.crypto?.randomUUID?.()??Math.random().toString(36),this.scheme="light",this.handleSeedColorChanged=e=>{this.syncRootFromDom(!1)&&(this.seedColor=e.seedColor,this.applyVariantTokens())},this.handleSchemeChanged=e=>{!this.syncRootFromDom(!1)||this.scheme===e.scheme||(this.scheme=e.scheme,this.applyVariantTokens())},this.handleThemeChanged=()=>{this.syncRootFromDom(!1)&&this.applyVariantTokens()},this.handleIconSpriteUrlChanged=()=>{this.syncRootFromDom(!1)&&this.requestUpdate()}}get seedColor(){return this.seedColorBacker}set seedColor(e){this.seedColorBacker=e}focusResolvedElementAfterUpdate(e,t){const o=()=>{const r=e();return r?(r.focus(t),!0):!1};o()||this.updateComplete.then(()=>{o()})}focusShadowElementAfterUpdate(e,t){this.focusResolvedElementAfterUpdate(()=>this.shadowRoot?.querySelector(e),t)}emitValueChanged(e){this.dispatchEvent(new CustomEvent("value-changed",{detail:e,bubbles:!0,composed:!0}))}get variant(){return this.variantBacker}set variant(e){this.variantBacker=e,this.applyVariantTokens()}connectedCallback(){super.connectedCallback();const e=this.syncRootFromDom(!1);e&&this.subscribeToRootEvents(e)}disconnectedCallback(){super.disconnectedCallback(),this.unsubscribeFromRootEvents()}firstUpdated(e){this.syncRootFromDom(!0)}syncRootFromDom(e){const t=T(this,"esp-root");if(!t){if(this.espRoot&&this.clearVariantTokens(),this.unsubscribeFromRootEvents(),this.espRoot=null,!e)return null;throw new Error("No esp-root ancestor found. Espalier components must be placed inside an <esp-root> element.")}const o=t!==this.espRoot,s=t.scheme==="dark"?"dark":"light",a=this.scheme!==s,c=this.seedColor!==t.seedColor;return this.espRoot=t,o&&this.isConnected&&this.subscribeToRootEvents(t),(o||a||c)&&(this.scheme=s,this.seedColor=t.seedColor,this.applyVariantTokens()),o&&this.requestUpdate(),t}subscribeToRootEvents(e){this.rootEventSubscriptionsActive&&this.subscribedRoot===e||(this.unsubscribeFromRootEvents(),this.subscribedRoot=e,this.rootEventUnsubscribers=[f(e,"seed-color-changed",this.handleSeedColorChanged),f(e,"scheme-changed",this.handleSchemeChanged),f(e,"theme-changed",this.handleThemeChanged),f(e,"icon-sprite-url-changed",this.handleIconSpriteUrlChanged)],this.rootEventSubscriptionsActive=!0)}unsubscribeFromRootEvents(){if(this.rootEventSubscriptionsActive){for(const e of this.rootEventUnsubscribers)e();this.rootEventUnsubscribers=[],this.subscribedRoot=null,this.rootEventSubscriptionsActive=!1}}traverseToClosest(e){return T(this,e)}applyVariantTokens(){if(!this.espRoot)return;const e=this.getVariantColorSource();if(!e){this.clearVariantTokens();return}const t=this.espRoot.activeTheme,o=O(t.seedColor);if(!o)return;const s=S(o,t)[e];if(!s){this.clearVariantTokens();return}this.setVariantTokenProperty("--esp-color-primary",k(F(s)));const a=S(s,t),c=n.statusVariantSources.has(e),{textContrast:C}=n,g={},R=i=>D(i,a,t.anchors)??a.primary;for(const i of P){if(C[i])continue;const l=t.semanticMappings[i],h=t.chroma[i],d=this.effectiveVariantTokenSource(i,l.source,c),b=R(d),m=t.lightness[l.lightness],u=_(b,m,h.min,h.max);g[i]=u,this.setVariantTokenProperty(w(i),k(u))}for(const i of P){const l=C[i];if(!l)continue;const h=t.semanticMappings[i],d=t.chroma[i],b=this.effectiveVariantTokenSource(i,h.source,c),m=R(b),u=t.lightness[h.lightness],E=g[l.bg],V=z(m,u,d.min,d.max,E,l.targetLc);g[i]=V,this.setVariantTokenProperty(w(i),k(V))}}effectiveVariantTokenSource(e,t,o){return o&&n.semanticActionTokens.has(e)?"primary":t}getVariantColorSource(){switch(this.variant){case"":case"primary":case"neutral":return"";case"info":return"complementary";default:return this.variant}}setVariantTokenProperty(e,t){const o=this.variantTokenOriginalValues.get(e);if(o){const r=this.style.getPropertyValue(e),s=this.style.getPropertyPriority(e);(r!==o.generatedValue||s!==o.generatedPriority)&&(o.originalValue=r.length?r:null,o.originalPriority=s),o.generatedValue=t,o.generatedPriority=""}else{const r=this.style.getPropertyValue(e);this.variantTokenOriginalValues.set(e,{generatedPriority:"",generatedValue:t,originalPriority:this.style.getPropertyPriority(e),originalValue:r.length?r:null})}this.style.setProperty(e,t)}clearVariantTokens(){for(const[e,t]of this.variantTokenOriginalValues){const o=this.style.getPropertyValue(e),r=this.style.getPropertyPriority(e);(o!==t.generatedValue||r!==t.generatedPriority)&&(t.originalValue=o.length?o:null,t.originalPriority=r),t.originalValue===null?this.style.removeProperty(e):this.style.setProperty(e,t.originalValue,t.originalPriority)}this.variantTokenOriginalValues.clear()}}n.textContrast=I,n.statusVariantSources=new Set(["danger","success","warning"]),n.semanticActionTokens=new Set(["actionBackground","actionText"]),n.styles=[B(".esp-field:focus-within","--esp-field-focus-shadow"),M,x`
|
|
2
2
|
:host {
|
|
3
3
|
|
|
4
4
|
--_esp-field-resolved-hover-bg: var(
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module theme-fit-report
|
|
3
|
+
*
|
|
4
|
+
* Dev-mode explanation of what the theme pipeline did to a theme
|
|
5
|
+
* (ESP0163).
|
|
6
|
+
*
|
|
7
|
+
* Two pipeline mechanisms move a token away from the value a designer
|
|
8
|
+
* wrote down, and both are silent:
|
|
9
|
+
*
|
|
10
|
+
* - **APCA enforcement** (ADR-002) shifts the lightness of a text token
|
|
11
|
+
* until it clears its Lc target against its background pair. Fitting
|
|
12
|
+
* the SHY brand moved three tokens and reported none of them.
|
|
13
|
+
* - **Brand anchors** (ADR-015) resolve a `anchor:<name>[.<slot>]`
|
|
14
|
+
* source to a declared swatch, then re-light it from the mapping's
|
|
15
|
+
* ramp stop — so the emitted color is a relative of the swatch, not
|
|
16
|
+
* the swatch.
|
|
17
|
+
*
|
|
18
|
+
* The result is a mock that says one color and a render that says
|
|
19
|
+
* another, with no record of the step in between. This module produces
|
|
20
|
+
* that record.
|
|
21
|
+
*
|
|
22
|
+
* ## Sharing the derivation, not repeating it
|
|
23
|
+
*
|
|
24
|
+
* The report **cannot** disagree with the pipeline, because it does not
|
|
25
|
+
* compute any token value:
|
|
26
|
+
*
|
|
27
|
+
* - `resolved` is read back out of the property record
|
|
28
|
+
* `computeThemeProperties` returns — it is the emitted custom
|
|
29
|
+
* property's value, not a re-derivation of it.
|
|
30
|
+
* - `requested` is the pre-enforcement binding `deriveSemanticWithContrast`
|
|
31
|
+
* hands to its `onRequested` observer, i.e. the exact value
|
|
32
|
+
* `ensureContrast` then consumed.
|
|
33
|
+
*
|
|
34
|
+
* Only the *descriptive* numbers are computed here — the achieved Lc
|
|
35
|
+
* scores and the ΔE to a declared anchor — and each of those calls the
|
|
36
|
+
* same engine function the pipeline uses.
|
|
37
|
+
*
|
|
38
|
+
* Nothing runs unless a report is asked for: `computeThemeProperties`
|
|
39
|
+
* takes the trace sink as an optional argument and does no reporting
|
|
40
|
+
* work when it is absent.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* import { themeFitReport, printThemeFitReport } from './theme-fit-report.js';
|
|
45
|
+
*
|
|
46
|
+
* const report = themeFitReport(mergeTheme(DEFAULT_LIGHT_THEME, brand), 'light');
|
|
47
|
+
* printThemeFitReport(report); // console.table
|
|
48
|
+
* report.adjustedTokens; // ['linkHover']
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
import { type EspalierTheme, type LightnessKey, type MappingSource, type SemanticColorName } from "./theme.js";
|
|
52
|
+
/** What APCA enforcement did to one token. */
|
|
53
|
+
export interface ThemeFitApcaAction {
|
|
54
|
+
/** The requested value, as an `oklch()` string. */
|
|
55
|
+
from: string;
|
|
56
|
+
/** The emitted value. */
|
|
57
|
+
to: string;
|
|
58
|
+
/** The minimum |Lc| the pairing requires. */
|
|
59
|
+
targetLc: number;
|
|
60
|
+
/** The semantic token supplying the background. */
|
|
61
|
+
against: SemanticColorName;
|
|
62
|
+
/** That background's value. */
|
|
63
|
+
againstColor: string;
|
|
64
|
+
/** |Lc| the requested value achieved against that background. */
|
|
65
|
+
fromLc: number;
|
|
66
|
+
/** |Lc| the emitted value achieves against that background. */
|
|
67
|
+
toLc: number;
|
|
68
|
+
}
|
|
69
|
+
/** How far the emitted token landed from the brand swatch it names. */
|
|
70
|
+
export interface ThemeFitAnchor {
|
|
71
|
+
/** The declared reference — `rose` or `rose.hover`. */
|
|
72
|
+
reference: string;
|
|
73
|
+
/** The anchor's declared color, as an `oklch()` string. */
|
|
74
|
+
color: string;
|
|
75
|
+
/**
|
|
76
|
+
* ΔE-OK between the declared anchor and the emitted token. Expect a
|
|
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).
|
|
80
|
+
*/
|
|
81
|
+
deltaE: number;
|
|
82
|
+
}
|
|
83
|
+
/** One semantic token's trip through the pipeline. */
|
|
84
|
+
export interface ThemeFitToken {
|
|
85
|
+
/** The semantic token name. */
|
|
86
|
+
token: SemanticColorName;
|
|
87
|
+
/** The custom property it is emitted as. */
|
|
88
|
+
property: string;
|
|
89
|
+
/** The mapping's declared source. */
|
|
90
|
+
source: MappingSource;
|
|
91
|
+
/** The lightness ramp stop the mapping selected. */
|
|
92
|
+
lightness: LightnessKey;
|
|
93
|
+
/** The value the mapping asked for, before contrast enforcement. */
|
|
94
|
+
requested: string;
|
|
95
|
+
/** The value actually emitted. */
|
|
96
|
+
resolved: string;
|
|
97
|
+
/** `true` when enforcement moved the token — `requested !== resolved`. */
|
|
98
|
+
adjusted: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* `false` when the source could not be resolved and the seed stood in.
|
|
101
|
+
* A theme that passes `validateTheme` never reports this.
|
|
102
|
+
*/
|
|
103
|
+
sourceResolved: boolean;
|
|
104
|
+
/** Present when the source is a resolved anchor reference. */
|
|
105
|
+
anchor?: ThemeFitAnchor;
|
|
106
|
+
/** Present for the text tokens APCA enforcement guards. */
|
|
107
|
+
apca?: ThemeFitApcaAction;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* A theme's fit report — the stable JSON shape the docs site renders.
|
|
111
|
+
*/
|
|
112
|
+
export interface ThemeFitReport {
|
|
113
|
+
/** The scheme the theme was computed for. */
|
|
114
|
+
scheme: "light" | "dark";
|
|
115
|
+
/** The theme's seed color, as declared. */
|
|
116
|
+
seedColor: string;
|
|
117
|
+
/**
|
|
118
|
+
* The theme's declared anchors, flattened to the reference strings a
|
|
119
|
+
* mapping uses: `{ "rose": "oklch(…)", "rose.hover": "oklch(…)" }`.
|
|
120
|
+
*/
|
|
121
|
+
anchors: Record<string, string>;
|
|
122
|
+
/** Every semantic token, in the pipeline's derivation order. */
|
|
123
|
+
tokens: ThemeFitToken[];
|
|
124
|
+
/** The tokens APCA enforcement moved — the headline of the report. */
|
|
125
|
+
adjustedTokens: SemanticColorName[];
|
|
126
|
+
}
|
|
127
|
+
/** One `console.table` row. */
|
|
128
|
+
export interface ThemeFitRow {
|
|
129
|
+
token: string;
|
|
130
|
+
source: string;
|
|
131
|
+
requested: string;
|
|
132
|
+
resolved: string;
|
|
133
|
+
/** The APCA pairing and what it did; empty for unenforced tokens. */
|
|
134
|
+
apca: string;
|
|
135
|
+
/** The anchor reference and its ΔE; empty for non-anchor sources. */
|
|
136
|
+
anchor: string;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Build the fit report for a resolved theme.
|
|
140
|
+
*
|
|
141
|
+
* Runs the real pipeline once with a trace sink attached and describes
|
|
142
|
+
* what it did. An unparseable seed produces an empty token list, exactly
|
|
143
|
+
* as it produces an empty property record.
|
|
144
|
+
*
|
|
145
|
+
* @param theme A resolved theme (as `mergeTheme` returns).
|
|
146
|
+
* @param scheme The scheme to compute.
|
|
147
|
+
*/
|
|
148
|
+
export declare function themeFitReport(theme: EspalierTheme, scheme: "light" | "dark"): ThemeFitReport;
|
|
149
|
+
/**
|
|
150
|
+
* Render a report as `console.table` rows.
|
|
151
|
+
*
|
|
152
|
+
* Exposed separately from {@link printThemeFitReport} so a host can put
|
|
153
|
+
* the same rows somewhere other than the console.
|
|
154
|
+
*/
|
|
155
|
+
export declare function themeFitRows(report: ThemeFitReport): ThemeFitRow[];
|
|
156
|
+
/**
|
|
157
|
+
* Print a fit report to the console — the common case.
|
|
158
|
+
*
|
|
159
|
+
* One summary line naming the scheme and the tokens enforcement moved,
|
|
160
|
+
* then a `console.table` of every token.
|
|
161
|
+
*/
|
|
162
|
+
export declare function printThemeFitReport(report: ThemeFitReport): void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{apcaContrast as d,deltaEOK as f,parseOklch as u,serializeOklch as n}from"./color-engine.js";import{parseAnchorSource as l}from"./theme.js";import{computeThemeProperties as h}from"../root/helpers/compute-theme-properties.js";function A(e,o){const s=[];h(e,o,s);const r=s.map(v);return{scheme:o,seedColor:e.seedColor,anchors:m(e.anchors),tokens:r,adjustedTokens:r.filter(t=>t.adjusted).map(t=>t.token)}}function p(e){return e.tokens.map(o=>({token:o.token,source:o.source,requested:o.requested,resolved:o.resolved,apca:$(o),anchor:g(o)}))}function R(e){const o=e.adjustedTokens.length>0?`APCA moved ${e.adjustedTokens.join(", ")}`:"APCA moved nothing";console.info(`Espalier theme fit \u2014 ${e.scheme} scheme, seed ${e.seedColor}. ${o}.`),console.table(p(e))}function a(e,o){const s=Math.pow(10,o);return Math.round(e*s)/s}function i(e,o){return o===void 0?e:`${e}.${o}`}function m(e){const o={};for(const[s,r]of Object.entries(e)){if(typeof r=="string"){o[s]=r;continue}for(const[t,c]of Object.entries(r))o[t==="color"?s:i(s,t)]=c}return o}function v(e){const o=u(e.resolved),s=e.requested===void 0?e.resolved:n(e.requested),r={token:e.token,property:e.property,source:e.source,lightness:e.lightness,requested:s,resolved:e.resolved,adjusted:s!==e.resolved,sourceResolved:e.sourceResolved},t=l(e.source);if(t&&e.sourceResolved&&(r.anchor={reference:i(t.name,t.slot),color:n(e.base),deltaE:a(f(e.base,o),4)}),e.contrast){const c=e.contrast.background;r.apca={from:s,to:e.resolved,targetLc:e.contrast.targetLc,against:e.contrast.against,againstColor:n(c),fromLc:a(Math.abs(d(u(s),c)),1),toLc:a(Math.abs(d(o,c)),1)}}return r}function $(e){if(!e.apca)return"";const{targetLc:o,against:s,fromLc:r,toLc:t}=e.apca;return e.adjusted?`Lc ${o} vs ${s} \u2014 pushed from Lc ${r} to Lc ${t}`:`Lc ${o} vs ${s} \u2014 met at Lc ${r}`}function g(e){return e.anchor?`${e.anchor.reference} \u0394E ${e.anchor.deltaE}`:e.sourceResolved?"":"unresolved \u2014 seed used"}export{R as printThemeFitReport,A as themeFitReport,p as themeFitRows};
|
|
File without changes
|
package/dist/shared/theme.d.ts
CHANGED
|
@@ -50,6 +50,79 @@ export type ColorSource = "primary" | "analogous-left" | "analogous-right" | "co
|
|
|
50
50
|
* color's chroma.
|
|
51
51
|
*/
|
|
52
52
|
export type VariantColorSource = Exclude<ColorSource, "primary">;
|
|
53
|
+
/**
|
|
54
|
+
* The object form of an anchor: a required base `color` plus named
|
|
55
|
+
* sub-slots — per-family variants the way designers already specify
|
|
56
|
+
* them (`rose.text` for the deepened rose that may carry copy).
|
|
57
|
+
*/
|
|
58
|
+
export type ThemeAnchorSlots = {
|
|
59
|
+
color: string;
|
|
60
|
+
} & Record<string, string>;
|
|
61
|
+
/**
|
|
62
|
+
* A named brand color in a **resolved** theme.
|
|
63
|
+
*
|
|
64
|
+
* Either a color string, or the slot object — which always carries its
|
|
65
|
+
* base `color`, because a partial that names only slots refines an
|
|
66
|
+
* anchor rather than replacing it. Like `seedColor`, this type states
|
|
67
|
+
* the contract `validateTheme` enforces: a theme assembled from an
|
|
68
|
+
* unvalidated partial that never declared a base color reports an error
|
|
69
|
+
* there, and degrades to the seed at runtime.
|
|
70
|
+
*
|
|
71
|
+
* Every value accepts any CSS color form (ESP0161) and is normalized to
|
|
72
|
+
* OKLCH at the merge boundary.
|
|
73
|
+
*/
|
|
74
|
+
export type ThemeAnchor = string | ThemeAnchorSlots;
|
|
75
|
+
/**
|
|
76
|
+
* A named brand color in a **partial** theme, where an object form may
|
|
77
|
+
* name slots alone — `{ rose: { hover: "#88404c" } }` refines the rose
|
|
78
|
+
* anchor a base theme already declared.
|
|
79
|
+
*/
|
|
80
|
+
export type PartialThemeAnchor = string | Record<string, string>;
|
|
81
|
+
/** Anchors as they appear in a partial theme. */
|
|
82
|
+
export type PartialThemeAnchors = Record<string, PartialThemeAnchor>;
|
|
83
|
+
/**
|
|
84
|
+
* Named brand colors, keyed by slug.
|
|
85
|
+
*
|
|
86
|
+
* Anchors are free-standing color sources for semantic mappings —
|
|
87
|
+
* `source: "anchor:<name>"` — so a multi-color brand is declared as the
|
|
88
|
+
* swatches the designer handed over instead of being bent through hue
|
|
89
|
+
* geometry and per-token chroma clamps. See ADR-015.
|
|
90
|
+
*/
|
|
91
|
+
export type ThemeAnchors = Record<string, ThemeAnchor>;
|
|
92
|
+
/**
|
|
93
|
+
* A semantic mapping's color source: a geometric or status family from
|
|
94
|
+
* {@link ColorSource}, or a declared anchor — `anchor:<name>` for its
|
|
95
|
+
* base color, `anchor:<name>.<slot>` for a sub-slot.
|
|
96
|
+
*
|
|
97
|
+
* An anchor source contributes its own hue **and chroma**; lightness
|
|
98
|
+
* always comes from the mapping's ramp stop, and APCA enforcement
|
|
99
|
+
* applies unchanged. Under an element variant, anchor sources stay
|
|
100
|
+
* absolute — a brand color is a color, not a relationship to the seed,
|
|
101
|
+
* so variant re-seeding does not rotate it.
|
|
102
|
+
*/
|
|
103
|
+
export type MappingSource = ColorSource | `anchor:${string}`;
|
|
104
|
+
/** Anchor names and slot names share one slug grammar. */
|
|
105
|
+
export declare const ANCHOR_SLUG_PATTERN: RegExp;
|
|
106
|
+
/** A parsed `anchor:` mapping source. */
|
|
107
|
+
export interface AnchorReference {
|
|
108
|
+
name: string;
|
|
109
|
+
slot?: string;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Parse an `anchor:` mapping source into its reference, or return
|
|
113
|
+
* `null` for anything that is not anchor syntax at all. (A reference
|
|
114
|
+
* with an invalid or undeclared name parses fine here — declaration
|
|
115
|
+
* checks belong to validateTheme and resolution.)
|
|
116
|
+
*/
|
|
117
|
+
export declare function parseAnchorSource(source: string): AnchorReference | null;
|
|
118
|
+
/**
|
|
119
|
+
* Resolve an anchor reference to its declared color string.
|
|
120
|
+
*
|
|
121
|
+
* `anchor:<name>` on an object-form anchor resolves the `color` key; a
|
|
122
|
+
* slot reference into a string-form anchor has nowhere to point and
|
|
123
|
+
* resolves `null`, as do unknown names and slots.
|
|
124
|
+
*/
|
|
125
|
+
export declare function resolveAnchorColor(anchors: ThemeAnchors, reference: AnchorReference): string | null;
|
|
53
126
|
/** Every semantic color token the system computes. */
|
|
54
127
|
export type SemanticColorName = "background" | "layer1" | "layer2" | "layer3" | "layer4" | "actionBackground" | "actionText" | "border" | "shadow" | "text" | "dangerText" | "headings" | "headingsHover" | "link" | "linkHover" | "linkHoverBg" | "inputCaret" | "inputSelection" | "inputSelectionBg";
|
|
55
128
|
/** Chroma range enforced on a semantic token before gamut mapping. */
|
|
@@ -65,13 +138,110 @@ export interface ChromaRange {
|
|
|
65
138
|
* lightness ramp position sets the perceived brightness.
|
|
66
139
|
*/
|
|
67
140
|
export interface SemanticMapping {
|
|
68
|
-
/**
|
|
69
|
-
|
|
141
|
+
/**
|
|
142
|
+
* The color source to sample: a geometric/status family, or a
|
|
143
|
+
* declared anchor as `anchor:<name>` / `anchor:<name>.<slot>`.
|
|
144
|
+
*/
|
|
145
|
+
source: MappingSource;
|
|
70
146
|
/** The lightness ramp position to apply. */
|
|
71
147
|
lightness: LightnessKey;
|
|
72
148
|
}
|
|
73
149
|
/** Full mapping table — one entry per semantic color token. */
|
|
74
150
|
export type SemanticMappings = Record<SemanticColorName, SemanticMapping>;
|
|
151
|
+
/**
|
|
152
|
+
* The designer-facing color roles.
|
|
153
|
+
*
|
|
154
|
+
* `semantic-groups.md` has always described these groups and then warned
|
|
155
|
+
* that exposing the nineteen raw tokens is "handing users the engine
|
|
156
|
+
* room instead of the dashboard" — but the groups were documentation
|
|
157
|
+
* only. Roles make them addressable: a sentence a designer would say
|
|
158
|
+
* becomes a line of theme.
|
|
159
|
+
*
|
|
160
|
+
* - `canvas` — the page and its raised surfaces
|
|
161
|
+
* - `ink` — body copy, with a `heading` slot for titles
|
|
162
|
+
* - `accent` — decoration (rules, icons, hover washes), with a `text`
|
|
163
|
+
* slot for the deepened variant that may carry copy
|
|
164
|
+
* - `action` — filled action surfaces; its paired ink derives
|
|
165
|
+
* automatically (see {@link ROLE_PAIRED_INK})
|
|
166
|
+
* - `structure` — borders and shadows
|
|
167
|
+
*
|
|
168
|
+
* `status` is deliberately absent: the danger/success/warning families
|
|
169
|
+
* carry meaning through fixed hues (ADR-004) and are not a brand
|
|
170
|
+
* decision.
|
|
171
|
+
*/
|
|
172
|
+
export type RoleName = "canvas" | "ink" | "accent" | "action" | "structure";
|
|
173
|
+
/**
|
|
174
|
+
* A role binding: a color source, or an object with the base `color`
|
|
175
|
+
* plus role-specific slots.
|
|
176
|
+
*
|
|
177
|
+
* Sources are the same vocabulary mappings use — a geometric family, a
|
|
178
|
+
* status family, or an `anchor:<name>` reference (ADR-015).
|
|
179
|
+
*/
|
|
180
|
+
export type RoleSlotName = {
|
|
181
|
+
canvas: never;
|
|
182
|
+
ink: "heading";
|
|
183
|
+
accent: "text";
|
|
184
|
+
action: "ink";
|
|
185
|
+
structure: never;
|
|
186
|
+
};
|
|
187
|
+
export type RoleBinding<R extends RoleName = RoleName> = MappingSource | ({
|
|
188
|
+
color: MappingSource;
|
|
189
|
+
} & Partial<Record<RoleSlotName[R], MappingSource>>);
|
|
190
|
+
/**
|
|
191
|
+
* The roles a theme declares. Every role is optional, and each one
|
|
192
|
+
* admits only its own slots — `ink.heading`, `accent.text`,
|
|
193
|
+
* `action.ink` — so the structural claims the roles layer makes are
|
|
194
|
+
* checked by the type system, not only by validateTheme.
|
|
195
|
+
*/
|
|
196
|
+
export type ThemeRoles = {
|
|
197
|
+
[R in RoleName]?: RoleBinding<R>;
|
|
198
|
+
};
|
|
199
|
+
/** Ordered role names, for validation and iteration. */
|
|
200
|
+
export declare const ROLE_NAMES: readonly RoleName[];
|
|
201
|
+
/**
|
|
202
|
+
* Which semantic tokens each role slot paints, and at which ramp stop.
|
|
203
|
+
*
|
|
204
|
+
* This is the compilation table: it turns five designer-facing roles
|
|
205
|
+
* into the nineteen engine-room tokens. `dangerText` is absent by
|
|
206
|
+
* design — the status family is reserved and keeps its own source.
|
|
207
|
+
*
|
|
208
|
+
* The `accent` role has no body-text slot on purpose. "Rose is
|
|
209
|
+
* decorative only" is a structural guarantee here, not a comment: text
|
|
210
|
+
* that must come from the accent family comes through `accent.text`,
|
|
211
|
+
* the deepened variant a designer picked for legibility.
|
|
212
|
+
*/
|
|
213
|
+
export declare const ROLE_TOKEN_PLAN: Readonly<Record<RoleName, Readonly<Record<string, ReadonlyArray<[SemanticColorName, LightnessKey]>>>>>;
|
|
214
|
+
/**
|
|
215
|
+
* Ink slots that derive from another role when the theme does not
|
|
216
|
+
* declare them.
|
|
217
|
+
*
|
|
218
|
+
* A role that paints a surface must also answer "what writes on it".
|
|
219
|
+
* `action.ink` defaults to the canvas color: the ink paired with a
|
|
220
|
+
* filled action is the page ground, and APCA enforcement against
|
|
221
|
+
* `actionBackground` then guarantees it is legible — which is exactly
|
|
222
|
+
* how "white on plum for reversed sections" becomes one theme line
|
|
223
|
+
* instead of a hand-picked hex.
|
|
224
|
+
*/
|
|
225
|
+
export declare const ROLE_PAIRED_INK: Readonly<Record<string, {
|
|
226
|
+
role: RoleName;
|
|
227
|
+
slot: string;
|
|
228
|
+
}>>;
|
|
229
|
+
interface CompileRoleOptions {
|
|
230
|
+
/** Token mappings declared explicitly at this merge boundary. */
|
|
231
|
+
explicitMappings?: Partial<SemanticMappings>;
|
|
232
|
+
/** Exact maximum paired-ink contrast of an action surface candidate. */
|
|
233
|
+
actionSurfaceContrast?: (source: MappingSource, stop: LightnessKey) => number;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Compile roles into semantic mappings.
|
|
237
|
+
*
|
|
238
|
+
* Only the tokens a declared role owns are emitted, so a theme that
|
|
239
|
+
* declares no roles compiles to nothing and every existing theme keeps
|
|
240
|
+
* its exact output. Token-level `semanticMappings` layer over the
|
|
241
|
+
* result — the engine room stays available for the cases roles do not
|
|
242
|
+
* reach.
|
|
243
|
+
*/
|
|
244
|
+
export declare function compileRoles(roles: ThemeRoles, lightness: LightnessMap, baseMappings: SemanticMappings, options?: CompileRoleOptions): Partial<SemanticMappings>;
|
|
75
245
|
/** The complete, resolved Espalier theme. */
|
|
76
246
|
export interface EspalierTheme {
|
|
77
247
|
/**
|
|
@@ -201,6 +371,20 @@ export interface EspalierTheme {
|
|
|
201
371
|
chroma: Record<SemanticColorName, ChromaRange>;
|
|
202
372
|
/** Maps each semantic token to its color source + lightness. */
|
|
203
373
|
semanticMappings: SemanticMappings;
|
|
374
|
+
/**
|
|
375
|
+
* Named brand colors available to semantic mappings as
|
|
376
|
+
* `anchor:<name>` sources. Empty by default — a single-color brand
|
|
377
|
+
* needs none; a multi-color brand declares its swatches here instead
|
|
378
|
+
* of bending hue angles. See {@link ThemeAnchors} and ADR-015.
|
|
379
|
+
*/
|
|
380
|
+
anchors: ThemeAnchors;
|
|
381
|
+
/**
|
|
382
|
+
* Designer-facing color roles, compiled into
|
|
383
|
+
* {@link EspalierTheme.semanticMappings} at merge time. Empty by
|
|
384
|
+
* default; token-level mappings layer over whatever roles produce.
|
|
385
|
+
* See {@link ThemeRoles} and ADR-016.
|
|
386
|
+
*/
|
|
387
|
+
roles: ThemeRoles;
|
|
204
388
|
/** Optional CSS `background-image` for the page surface. */
|
|
205
389
|
pageBackgroundImage?: string;
|
|
206
390
|
/** Opacity (0–1) for the page background image. */
|
|
@@ -216,8 +400,16 @@ export interface EspalierTheme {
|
|
|
216
400
|
/** Opacity (0–1) for the vellum background image layer. */
|
|
217
401
|
vellumBackgroundImageOpacity?: number;
|
|
218
402
|
}
|
|
219
|
-
/**
|
|
220
|
-
|
|
403
|
+
/**
|
|
404
|
+
* Recursively-partial version of {@link EspalierTheme}.
|
|
405
|
+
*
|
|
406
|
+
* `anchors` is stated explicitly rather than derived: a partial may
|
|
407
|
+
* carry slot-only anchor overrides, which the resolved
|
|
408
|
+
* {@link ThemeAnchor} deliberately does not permit.
|
|
409
|
+
*/
|
|
410
|
+
export type PartialTheme = Omit<DeepPartial<EspalierTheme>, "anchors"> & {
|
|
411
|
+
anchors?: PartialThemeAnchors;
|
|
412
|
+
};
|
|
221
413
|
/** Validation result returned by {@link validateTheme}. */
|
|
222
414
|
export interface ThemeValidationResult {
|
|
223
415
|
/** `true` when there are zero errors (warnings are allowed). */
|
|
@@ -227,6 +419,29 @@ export interface ThemeValidationResult {
|
|
|
227
419
|
/** Soft issues — the theme will work but values are unusual. */
|
|
228
420
|
warnings: string[];
|
|
229
421
|
}
|
|
422
|
+
/**
|
|
423
|
+
* Which semantic tokens are ink, what surface each sits on, and the
|
|
424
|
+
* APCA contrast each must clear.
|
|
425
|
+
*
|
|
426
|
+
* This pairing lived as an identical hardcoded table inside both
|
|
427
|
+
* `compute-theme-properties.ts` and `esp-element-base.ts` — invisible to
|
|
428
|
+
* themes and duplicated in exactly the way this repository's adoption
|
|
429
|
+
* rules exist to prevent. It belongs to the model: every serious system
|
|
430
|
+
* makes the surface/ink pair the unit designers see (Material's
|
|
431
|
+
* `on-primary`, shadcn's `-foreground`), and the roles layer derives an
|
|
432
|
+
* action's ink from it rather than asking a designer to hand-pick a
|
|
433
|
+
* legible color.
|
|
434
|
+
*
|
|
435
|
+
* Lc targets follow the APCA guidelines (ADR-002):
|
|
436
|
+
* 90 → body text at any size
|
|
437
|
+
* 75 → 16 px+ text (UI labels, links, body)
|
|
438
|
+
* 60 → 24 px+ bold / 28 px+ normal (headings, carets, decorative)
|
|
439
|
+
*/
|
|
440
|
+
type TokenPairing = Readonly<{
|
|
441
|
+
bg: SemanticColorName;
|
|
442
|
+
targetLc: number;
|
|
443
|
+
}>;
|
|
444
|
+
export declare const TOKEN_PAIRINGS: Readonly<Record<"actionText", TokenPairing> & Partial<Record<SemanticColorName, TokenPairing>>>;
|
|
230
445
|
/** Ordered list of all semantic color token names. */
|
|
231
446
|
export declare const SEMANTIC_COLOR_NAMES: readonly SemanticColorName[];
|
|
232
447
|
/** Valid color-source identifiers for {@link SemanticMapping.source}. */
|
|
@@ -347,7 +562,7 @@ export declare function validateTheme(base64: string): ThemeValidationResult;
|
|
|
347
562
|
* and therefore need key-by-key merging instead of wholesale
|
|
348
563
|
* replacement when combining two {@link PartialTheme} objects.
|
|
349
564
|
*/
|
|
350
|
-
export declare const NESTED_THEME_KEYS: readonly ["angles", "chroma", "lightness", "semanticHues", "semanticMappings", "variantChroma"];
|
|
565
|
+
export declare const NESTED_THEME_KEYS: readonly ["anchors", "angles", "roles", "chroma", "lightness", "semanticHues", "semanticMappings", "variantChroma"];
|
|
351
566
|
/**
|
|
352
567
|
* Deep-merge two {@link PartialTheme} objects.
|
|
353
568
|
*
|