@taprootio/espalier 3.2.0 → 3.3.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.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import{readFileSync as o,writeFileSync as i}from"node:fs";import{runEspalierCli as l}from"./theme-check.js";process.exitCode=l(process.argv.slice(2),{readFile:e=>o(e,"utf8"),writeFile:(e,r)=>i(e,r),log:e=>console.log(e),error:e=>console.error(e)});
@@ -0,0 +1,2 @@
1
+ import{encodeTheme as h,validateThemePair as p}from"../shared/theme.js";import{themeFitReportSuite as m}from"../shared/theme-fit-report.js";const u="usage: espalier theme check <theme.json> [--json] [--expect <fit.json>] [--update-expect <fit.json>]";function S(t,e){const[n,o,...r]=t;if(n!=="theme"||o!=="check")return e.error(u),2;const i=g(r,e);return i?k(i,e):2}function g(t,e){const n={file:"",json:!1};for(let o=0;o<t.length;o+=1){const r=t[o];if(r==="--json")n.json=!0;else if(r==="--expect"||r==="--update-expect"){const i=t[o+1];if(i===void 0)return e.error(`${r} requires a file path. ${u}`),null;r==="--expect"?n.expect=i:n.updateExpect=i,o+=1}else if(!r.startsWith("--")&&n.file==="")n.file=r;else return e.error(`Unknown argument "${r}". ${u}`),null}return n.file===""?(e.error(u),null):n.expect!==void 0&&n.updateExpect!==void 0?(e.error(`--expect checks the committed fit tables and --update-expect rewrites them \u2014 pick one. ${u}`),null):n}function $(t,e){let n;try{n=e.readFile(t)}catch(i){return e.error(`Cannot read ${t}: ${i instanceof Error?i.message:String(i)}`),null}let o;try{o=JSON.parse(n)}catch(i){return e.error(`${t} is not JSON: ${i instanceof Error?i.message:String(i)}`),null}if(typeof o!="object"||o===null||Array.isArray(o))return e.error(`${t} must contain a theme object.`),null;const r=o;if("light"in r||"dark"in r){for(const i of["light","dark"]){const s=r[i];if(s!==void 0&&(typeof s!="object"||s===null||Array.isArray(s)))return e.error(`${t}: "${i}" must be a theme object.`),null}return{light:r.light??{},dark:r.dark??{}}}return{light:r,dark:r}}function k(t,e){const n=$(t.file,e);if(!n)return 2;const o=p(h(n.light),h(n.dark));for(const s of o.warnings)e.error(`warning: ${s}`);if(!o.valid){for(const s of o.errors)e.error(`error: ${s}`);return e.error(`${t.file}: ${o.errors.length} validation error(s).`),1}const r=m(n.light,n.dark),i=t.json?e.error:e.log;if(t.updateExpect!==void 0){try{e.writeFile(t.updateExpect,`${JSON.stringify(r,null,2)}
2
+ `)}catch(s){return e.error(`Cannot write ${t.updateExpect}: ${s instanceof Error?s.message:String(s)}`),2}return i(`Wrote fit tables to ${t.updateExpect}.`),t.json&&e.log(JSON.stringify(r,null,2)),0}if(t.expect!==void 0){let s;try{s=e.readFile(t.expect)}catch{return e.error(`Expected fit tables not found at ${t.expect} \u2014 create them with --update-expect ${t.expect}.`),2}let f;try{f=JSON.parse(s)}catch{return e.error(`${t.expect} is not JSON.`),2}if(!y(f))return e.error(`${t.expect} is not a fit-table suite \u2014 regenerate it with --update-expect ${t.expect}.`),2;const a=x(f,r);if(a.length>0){for(const l of a)e.error(`drift: ${l}`);return e.error(`Fit tables drifted from ${t.expect} (${a.length} difference(s)). Review, then refresh deliberately with --update-expect.`),1}i(`Fit tables match ${t.expect}.`)}return t.json?(e.log(JSON.stringify(r,null,2)),0):(b(r,e),0)}function y(t){if(typeof t!="object"||t===null||Array.isArray(t))return!1;const e=t;if(!Array.isArray(e.surfaces)||!e.surfaces.every(n=>typeof n=="string"))return!1;for(const n of["light","dark"]){const o=e[n];if(!Array.isArray(o))return!1;for(const r of o){if(typeof r!="object"||r===null||Array.isArray(r))return!1;const i=r;if(typeof i.surface!="string"||!Array.isArray(i.tokens))return!1;for(const s of i.tokens){if(typeof s!="object"||s===null||Array.isArray(s))return!1;const f=s;if(typeof f.token!="string"||typeof f.resolved!="string")return!1}}}return!0}function x(t,e){const n=[];if(JSON.stringify(t)===JSON.stringify(e))return n;for(const o of["light","dark"]){const r=new Map((t[o]??[]).map(s=>[s.surface,s])),i=new Map((e[o]??[]).map(s=>[s.surface,s]));for(const s of new Set([...r.keys(),...i.keys()])){const f=r.get(s),a=i.get(s);if(!f||!a){n.push(`${o}/${s}: surface ${f?"removed":"added"}`);continue}const l=new Map(f.tokens.map(c=>[c.token,c]));for(const c of a.tokens){const d=l.get(c.token);d?d.resolved!==c.resolved&&n.push(`${o}/${s}/${c.token}: ${d.resolved} \u2192 ${c.resolved}`):n.push(`${o}/${s}/${c.token}: token added`),l.delete(c.token)}for(const c of l.keys())n.push(`${o}/${s}/${c}: token removed`)}}return n.length===0&&n.push("fit-table metadata changed (lints or provenance marks)"),n}function b(t,e){for(const o of["light","dark"])for(const r of t[o]){e.log(j(o,r));for(const i of r.lints)e.log(` lint(${i.id}): ${i.message}`)}const n=[...t.light,...t.dark].reduce((o,r)=>o+r.lints.length,0);e.log(`Checked ${t.surfaces.length} surface(s) \xD7 2 schemes \u2014 `+(n===0?"no lints.":`${n} lint(s), listed above.`))}function j(t,e){const n=e.adjustedTokens.length===0?"contrast enforcement moved nothing":`contrast enforcement moved ${e.adjustedTokens.join(", ")}`,o=e.tokens.filter(i=>i.explicit==="inherited").length,r=o===0?"":`; ${o} inherited explicit pin(s) \u2014 see the fit tables`;return`${t}/${e.surface}: ${e.tokens.length} tokens, ${n}${r}.`}export{S as runEspalierCli};
package/dist/index.d.ts CHANGED
@@ -73,8 +73,9 @@ 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, buildTaprootLightTheme, buildTaprootDarkTheme, NESTED_THEME_KEYS, } from "./shared/theme.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";
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";
80
- export { themeFitReport, themeFitRows, printThemeFitReport, type ThemeFitReport, type ThemeFitToken, type ThemeFitApcaAction, type ThemeFitAnchor, type ThemeFitRow, } from "./shared/theme-fit-report.js";
80
+ export { ROOT_SURFACE, themeFitReport, themeFitRows, themeFitLints, themeFitReportSuite, printThemeFitReport, type ThemeFitReport, type ThemeFitReportOptions, type ThemeFitToken, type ThemeFitApcaAction, type ThemeFitAnchor, type ThemeFitRow, type ThemeFitLint, type ThemeFitSurfaceReport, type ThemeFitSuite, } from "./shared/theme-fit-report.js";
81
+ export { deriveLightnessRamp, themeFromSwatches, type ThemeFromSwatchesOptions, type ThemeFromSwatchesResult, } from "./shared/theme-swatches.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"./section/esp-section.js";export*from"./stack/esp-stack.js";export*from"./row/esp-row.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 _r,DEFAULT_ICON_VIEW_BOX as Ar,INTENT_VARIANTS as Er,normalizeIntentVariant as lr,getIconHref as ir,getIconHrefForHost as Ir,getIconSpriteUrl as Sr}from"./shared/intent-values.js";import{EspalierElementBase as Dr}from"./shared/esp-element-base.js";import{VALIDITY_CHANGED_EVENT as nr}from"./shared/validation.js";import{FormFieldController as Lr}from"./shared/form-field-controller.js";import{traverseToClosest as Rr}from"./shared/utilities.js";import{showToast as Pr}from"./shared/toast-events.js";import{showFlyout as ur,closeFlyout as Mr}from"./shared/flyout-events.js";import{requestHelp as Or}from"./shared/help-events.js";import{getEspBus as Hr}from"./shared/bus-events.js";export*from"./shared/events.js";import{getImageDetails as Gr,releasePreviewUrl as br}from"./image-upload/image-helpers.js";import{calculatePhotoLayout as yr}from"./shared/justified-layout.js";import{encodeTheme as wr,parseTheme as Br,mergePartials as Yr,layerThemes as kr,validateThemePair as qr,buildTaprootLightTheme as zr,buildTaprootDarkTheme as Kr,NESTED_THEME_KEYS as Xr}from"./shared/theme.js";import{auditDataPalette as Jr,generateSequentialRamp as Qr,generateDivergingRamp as Zr,COLOR_VISION_SIMULATIONS as $r,DATA_SERIES_KEYS as ro,DEFAULT_DATA_PALETTE as oo,DEFAULT_DATA_RAMP_STEPS as eo,DEFAULT_DIVERGING_NEUTRAL as to,MAX_DATA_RAMP_STEPS as mo,MIN_DATA_COLOR_DISTANCE as po,MIN_DATA_RAMP_LIGHTNESS_STEP as fo,MIN_DATA_RAMP_STEPS as xo}from"./shared/data-colors.js";import{WEIGHT_LABELS as ao,extractWeights as _o,normalizeWeight as Ao,bestAvailableWeight as Eo,extractFamily as lo,getFallbackFont as io}from"./shared/font-helpers.js";import{getGoogleFonts as So}from"./font-picker/esp-font-picker.js";import{themeFitReport as Do,themeFitRows as go,printThemeFitReport as no}from"./shared/theme-fit-report.js";export{$r as COLOR_VISION_SIMULATIONS,ro as DATA_SERIES_KEYS,oo as DEFAULT_DATA_PALETTE,eo as DEFAULT_DATA_RAMP_STEPS,to as DEFAULT_DIVERGING_NEUTRAL,_r as DEFAULT_ICON_SPRITE_URL,Ar as DEFAULT_ICON_VIEW_BOX,Dr as EspalierElementBase,Lr as FormFieldController,Er as INTENT_VARIANTS,mo as MAX_DATA_RAMP_STEPS,po as MIN_DATA_COLOR_DISTANCE,fo as MIN_DATA_RAMP_LIGHTNESS_STEP,xo as MIN_DATA_RAMP_STEPS,Xr as NESTED_THEME_KEYS,nr as VALIDITY_CHANGED_EVENT,ao as WEIGHT_LABELS,Jr as auditDataPalette,Eo as bestAvailableWeight,Kr as buildTaprootDarkTheme,zr as buildTaprootLightTheme,yr as calculatePhotoLayout,Mr as closeFlyout,wr as encodeTheme,lo as extractFamily,_o as extractWeights,Zr as generateDivergingRamp,Qr as generateSequentialRamp,Hr as getEspBus,io as getFallbackFont,So as getGoogleFonts,ir as getIconHref,Ir as getIconHrefForHost,Sr as getIconSpriteUrl,Gr as getImageDetails,kr as layerThemes,Yr as mergePartials,lr as normalizeIntentVariant,Ao as normalizeWeight,Br as parseTheme,no as printThemeFitReport,br as releasePreviewUrl,Or as requestHelp,ur as showFlyout,Pr as showToast,Do as themeFitReport,go as themeFitRows,Rr as traverseToClosest,qr as validateThemePair};
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"./section/esp-section.js";export*from"./stack/esp-stack.js";export*from"./row/esp-row.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 _r,DEFAULT_ICON_VIEW_BOX as Ar,INTENT_VARIANTS as Er,normalizeIntentVariant as ir,getIconHref as lr,getIconHrefForHost as sr,getIconSpriteUrl as Ir}from"./shared/intent-values.js";import{EspalierElementBase as hr}from"./shared/esp-element-base.js";import{VALIDITY_CHANGED_EVENT as Dr}from"./shared/validation.js";import{FormFieldController as Rr}from"./shared/form-field-controller.js";import{traverseToClosest as Lr}from"./shared/utilities.js";import{showToast as Pr}from"./shared/toast-events.js";import{showFlyout as ur,closeFlyout as Or}from"./shared/flyout-events.js";import{requestHelp as Cr}from"./shared/help-events.js";import{getEspBus as Hr}from"./shared/bus-events.js";export*from"./shared/events.js";import{getImageDetails as Vr,releasePreviewUrl as Gr}from"./image-upload/image-helpers.js";import{calculatePhotoLayout as wr}from"./shared/justified-layout.js";import{encodeTheme as Wr,parseTheme as Br,mergePartials as Yr,layerThemes as kr,validateThemePair as qr,resolveContextTheme as zr,buildTaprootLightTheme as Kr,buildTaprootDarkTheme as Xr,NESTED_THEME_KEYS as jr}from"./shared/theme.js";import{auditDataPalette as Qr,generateSequentialRamp as Zr,generateDivergingRamp as $r,COLOR_VISION_SIMULATIONS as ro,DATA_SERIES_KEYS as oo,DEFAULT_DATA_PALETTE as eo,DEFAULT_DATA_RAMP_STEPS as to,DEFAULT_DIVERGING_NEUTRAL as mo,MAX_DATA_RAMP_STEPS as po,MIN_DATA_COLOR_DISTANCE as xo,MIN_DATA_RAMP_LIGHTNESS_STEP as fo,MIN_DATA_RAMP_STEPS as To}from"./shared/data-colors.js";import{WEIGHT_LABELS as _o,extractWeights as Ao,normalizeWeight as Eo,bestAvailableWeight as io,extractFamily as lo,getFallbackFont as so}from"./shared/font-helpers.js";import{getGoogleFonts as So}from"./font-picker/esp-font-picker.js";import{ROOT_SURFACE as no,themeFitReport as Do,themeFitRows as Fo,themeFitLints as Ro,themeFitReportSuite as go,printThemeFitReport as Lo}from"./shared/theme-fit-report.js";import{deriveLightnessRamp as Po,themeFromSwatches as co}from"./shared/theme-swatches.js";export{ro as COLOR_VISION_SIMULATIONS,oo as DATA_SERIES_KEYS,eo as DEFAULT_DATA_PALETTE,to as DEFAULT_DATA_RAMP_STEPS,mo as DEFAULT_DIVERGING_NEUTRAL,_r as DEFAULT_ICON_SPRITE_URL,Ar as DEFAULT_ICON_VIEW_BOX,hr as EspalierElementBase,Rr as FormFieldController,Er as INTENT_VARIANTS,po as MAX_DATA_RAMP_STEPS,xo as MIN_DATA_COLOR_DISTANCE,fo as MIN_DATA_RAMP_LIGHTNESS_STEP,To as MIN_DATA_RAMP_STEPS,jr as NESTED_THEME_KEYS,no as ROOT_SURFACE,Dr as VALIDITY_CHANGED_EVENT,_o as WEIGHT_LABELS,Qr as auditDataPalette,io as bestAvailableWeight,Xr as buildTaprootDarkTheme,Kr as buildTaprootLightTheme,wr as calculatePhotoLayout,Or as closeFlyout,Po as deriveLightnessRamp,Wr as encodeTheme,lo as extractFamily,Ao as extractWeights,$r as generateDivergingRamp,Zr as generateSequentialRamp,Hr as getEspBus,so as getFallbackFont,So as getGoogleFonts,lr as getIconHref,sr as getIconHrefForHost,Ir as getIconSpriteUrl,Vr as getImageDetails,kr as layerThemes,Yr as mergePartials,ir as normalizeIntentVariant,Eo as normalizeWeight,Br as parseTheme,Lo as printThemeFitReport,Gr as releasePreviewUrl,Cr as requestHelp,zr as resolveContextTheme,ur as showFlyout,Pr as showToast,Ro as themeFitLints,Do as themeFitReport,go as themeFitReportSuite,Fo as themeFitRows,co as themeFromSwatches,Lr as traverseToClosest,qr as validateThemePair};
@@ -71,6 +71,22 @@ export type { SchemeEvents } from "../shared/bus-events.js";
71
71
  * @cssprop --esp-color-series-6 - Sixth categorical data-series color.
72
72
  * @cssprop --esp-color-series-7 - Seventh categorical data-series color.
73
73
  * @cssprop --esp-color-series-8 - Eighth categorical data-series color.
74
+ * @cssprop --esp-color-series-1-wash - First series washed toward the local background (75% identity in OKLab) for large fills; theme zones re-emit it against their own canvas.
75
+ * @cssprop --esp-color-series-1-ink - First series pushed to the text contrast tier against the local background (best-contrast pole when a mid-lightness surface makes the tier unreachable).
76
+ * @cssprop --esp-color-series-2-wash - Second series washed toward the local background (75% identity in OKLab) for large fills; theme zones re-emit it against their own canvas.
77
+ * @cssprop --esp-color-series-2-ink - Second series pushed to the text contrast tier against the local background (best-contrast pole when a mid-lightness surface makes the tier unreachable).
78
+ * @cssprop --esp-color-series-3-wash - Third series washed toward the local background (75% identity in OKLab) for large fills; theme zones re-emit it against their own canvas.
79
+ * @cssprop --esp-color-series-3-ink - Third series pushed to the text contrast tier against the local background (best-contrast pole when a mid-lightness surface makes the tier unreachable).
80
+ * @cssprop --esp-color-series-4-wash - Fourth series washed toward the local background (75% identity in OKLab) for large fills; theme zones re-emit it against their own canvas.
81
+ * @cssprop --esp-color-series-4-ink - Fourth series pushed to the text contrast tier against the local background (best-contrast pole when a mid-lightness surface makes the tier unreachable).
82
+ * @cssprop --esp-color-series-5-wash - Fifth series washed toward the local background (75% identity in OKLab) for large fills; theme zones re-emit it against their own canvas.
83
+ * @cssprop --esp-color-series-5-ink - Fifth series pushed to the text contrast tier against the local background (best-contrast pole when a mid-lightness surface makes the tier unreachable).
84
+ * @cssprop --esp-color-series-6-wash - Sixth series washed toward the local background (75% identity in OKLab) for large fills; theme zones re-emit it against their own canvas.
85
+ * @cssprop --esp-color-series-6-ink - Sixth series pushed to the text contrast tier against the local background (best-contrast pole when a mid-lightness surface makes the tier unreachable).
86
+ * @cssprop --esp-color-series-7-wash - Seventh series washed toward the local background (75% identity in OKLab) for large fills; theme zones re-emit it against their own canvas.
87
+ * @cssprop --esp-color-series-7-ink - Seventh series pushed to the text contrast tier against the local background (best-contrast pole when a mid-lightness surface makes the tier unreachable).
88
+ * @cssprop --esp-color-series-8-wash - Eighth series washed toward the local background (75% identity in OKLab) for large fills; theme zones re-emit it against their own canvas.
89
+ * @cssprop --esp-color-series-8-ink - Eighth series pushed to the text contrast tier against the local background (best-contrast pole when a mid-lightness surface makes the tier unreachable).
74
90
  *
75
91
  * @docPageTitle Root
76
92
  * @docUrl /components/root
@@ -1 +1 @@
1
- import{parseCssColor as g,serializeOklch as l,gamutMapToSRGB as f}from"../../shared/color-engine.js";import{generateScaleProperties as k}from"../../shared/scale-engine.js";import{DATA_SERIES_KEYS as u,DEFAULT_DATA_PALETTE as v,generateDivergingRamp as B,generateSequentialRamp as R}from"../../shared/data-colors.js";import{resolveDataColorSource as w}from"../../shared/theme.js";import{lightnessRampProperties as O}from"./lightness-ramp-properties.js";import{computeVariants as $}from"./compute-variants.js";import{computeSemanticProperties as E}from"./compute-semantic-properties.js";function I(o){return{rootFontSize:o.rootFontSize,typeRatio:o.typeRatio,spaceRatio:o.spaceRatio,borderRadius:o.borderRadius,viewportMin:o.viewportMin,viewportMax:o.viewportMax}}function j(o,b,m){const i=g(o.seedColor);if(!i)return{};const a={};a["--esp-scheme"]=b,a["--esp-seed-color"]=o.seedColor;const c=$(i,o);for(const[e,n]of Object.entries(c))a[`--esp-color-${e}`]=l(f(n));const p=new Set,t=(e,n)=>{const r=typeof e=="string"?w(e,o.anchors):null;if(r)return r;const s=typeof e=="string"?e:String(e);return p.has(s)||(p.add(s),console.warn(`Espalier theme: ${n} color source "${s}" cannot be resolved \u2014 falling back to the seed. Run validateTheme for the declared data colors.`)),o.seedColor},y=o.dataPalette??v;for(let e=0;e<u.length;e+=1){const n=u[e],r=g(t(y[n],`dataPalette.${n}`))??i;a[`--esp-color-series-${e+1}`]=l(f(r))}for(const[e,n]of Object.entries(o.dataRamps??{}))try{(n.type==="sequential"?R(t(n.source,`dataRamps.${e}.source`),{steps:n.steps,lightnessStart:n.lightnessStart,lightnessEnd:n.lightnessEnd}):B(t(n.start,`dataRamps.${e}.start`),t(n.end,`dataRamps.${e}.end`),{steps:n.steps,neutral:n.neutral===void 0?void 0:t(n.neutral,`dataRamps.${e}.neutral`)})).forEach((s,S)=>{a[`--esp-color-ramp-${e}-${S+1}`]=s})}catch(r){console.warn(`Espalier theme: dataRamps.${e} cannot be generated \u2014 ${r instanceof Error?r.message:String(r)}`)}Object.assign(a,O(o));const d=new Set;return Object.assign(a,E(o,c,{trace:m,onUnresolvedSource:e=>{d.has(e)||(d.add(e),console.warn(`Espalier theme: color source "${e}" cannot be resolved \u2014 falling back to the seed. Run validateTheme for the declared anchors.`))}})),Object.assign(a,k(I(o))),o.fontBody&&(a["--esp-font-body"]=o.fontBody),o.fontHeadings&&(a["--esp-font-headings"]=o.fontHeadings),o.fontBrand&&(a["--esp-font-brand"]=o.fontBrand),o.fontMonospace&&(a["--esp-font-monospace"]=o.fontMonospace),a["--esp-font-weight-body"]=o.fontWeightBody,a["--esp-font-weight-headings"]=o.fontWeightHeadings,a["--esp-font-weight-brand"]=o.fontWeightBrand,a["--esp-font-weight-monospace"]=o.fontWeightMonospace,o.pageBackgroundImage&&(a["--esp-page-background-image"]=o.pageBackgroundImage),o.pageBackgroundImageOpacity!==void 0&&(a["--esp-page-background-image-opacity"]=o.pageBackgroundImageOpacity.toString()),o.boxBackgroundImage&&(a["--esp-box-background-image"]=o.boxBackgroundImage),o.boxBackgroundImageOpacity!==void 0&&(a["--esp-box-background-image-opacity"]=o.boxBackgroundImageOpacity.toString()),o.vellumOpacity!==void 0&&(a["--esp-vellum-opacity"]=o.vellumOpacity.toString()),o.vellumBackgroundImage&&(a["--esp-vellum-background-image"]=o.vellumBackgroundImage),o.vellumBackgroundImageOpacity!==void 0&&(a["--esp-vellum-background-image-opacity"]=o.vellumBackgroundImageOpacity.toString()),a}export{j as computeThemeProperties};
1
+ import{parseCssColor as f,serializeOklch as u,gamutMapToSRGB as m}from"../../shared/color-engine.js";import{generateScaleProperties as y}from"../../shared/scale-engine.js";import{DATA_SERIES_KEYS as b,DEFAULT_DATA_PALETTE as k,generateDivergingRamp as v,generateSequentialRamp as B}from"../../shared/data-colors.js";import{resolveDataColorSource as R}from"../../shared/theme.js";import{dataCompanionProperties as O}from"./data-companion-properties.js";import{lightnessRampProperties as w}from"./lightness-ramp-properties.js";import{computeVariants as $}from"./compute-variants.js";import{computeSemanticProperties as E}from"./compute-semantic-properties.js";function I(o){return{rootFontSize:o.rootFontSize,typeRatio:o.typeRatio,spaceRatio:o.spaceRatio,borderRadius:o.borderRadius,viewportMin:o.viewportMin,viewportMax:o.viewportMax}}function x(o,d,g){const i={},a=o.dataPalette??k;for(let r=0;r<b.length;r+=1){const c=b[r],t=f(g(a[c],`dataPalette.${c}`))??d;i[`--esp-color-series-${r+1}`]=u(m(t))}return i}function _(o,d,g){const i=f(o.seedColor);if(!i)return{};const a={};a["--esp-scheme"]=d,a["--esp-seed-color"]=o.seedColor;const r=$(i,o);for(const[e,n]of Object.entries(r))a[`--esp-color-${e}`]=u(m(n));const c=new Set,t=(e,n)=>{const s=typeof e=="string"?R(e,o.anchors):null;if(s)return s;const p=typeof e=="string"?e:String(e);return c.has(p)||(c.add(p),console.warn(`Espalier theme: ${n} color source "${p}" cannot be resolved \u2014 falling back to the seed. Run validateTheme for the declared data colors.`)),o.seedColor};Object.assign(a,x(o,i,t));for(const[e,n]of Object.entries(o.dataRamps??{}))try{(n.type==="sequential"?B(t(n.source,`dataRamps.${e}.source`),{steps:n.steps,lightnessStart:n.lightnessStart,lightnessEnd:n.lightnessEnd}):v(t(n.start,`dataRamps.${e}.start`),t(n.end,`dataRamps.${e}.end`),{steps:n.steps,neutral:n.neutral===void 0?void 0:t(n.neutral,`dataRamps.${e}.neutral`)})).forEach((p,S)=>{a[`--esp-color-ramp-${e}-${S+1}`]=p})}catch(s){console.warn(`Espalier theme: dataRamps.${e} cannot be generated \u2014 ${s instanceof Error?s.message:String(s)}`)}Object.assign(a,w(o));const l=new Set;return Object.assign(a,E(o,r,{trace:g,onUnresolvedSource:e=>{l.has(e)||(l.add(e),console.warn(`Espalier theme: color source "${e}" cannot be resolved \u2014 falling back to the seed. Run validateTheme for the declared anchors.`))}})),Object.assign(a,O(a)),Object.assign(a,y(I(o))),o.fontBody&&(a["--esp-font-body"]=o.fontBody),o.fontHeadings&&(a["--esp-font-headings"]=o.fontHeadings),o.fontBrand&&(a["--esp-font-brand"]=o.fontBrand),o.fontMonospace&&(a["--esp-font-monospace"]=o.fontMonospace),a["--esp-font-weight-body"]=o.fontWeightBody,a["--esp-font-weight-headings"]=o.fontWeightHeadings,a["--esp-font-weight-brand"]=o.fontWeightBrand,a["--esp-font-weight-monospace"]=o.fontWeightMonospace,o.pageBackgroundImage&&(a["--esp-page-background-image"]=o.pageBackgroundImage),o.pageBackgroundImageOpacity!==void 0&&(a["--esp-page-background-image-opacity"]=o.pageBackgroundImageOpacity.toString()),o.boxBackgroundImage&&(a["--esp-box-background-image"]=o.boxBackgroundImage),o.boxBackgroundImageOpacity!==void 0&&(a["--esp-box-background-image-opacity"]=o.boxBackgroundImageOpacity.toString()),o.vellumOpacity!==void 0&&(a["--esp-vellum-opacity"]=o.vellumOpacity.toString()),o.vellumBackgroundImage&&(a["--esp-vellum-background-image"]=o.vellumBackgroundImage),o.vellumBackgroundImageOpacity!==void 0&&(a["--esp-vellum-background-image-opacity"]=o.vellumBackgroundImageOpacity.toString()),a}export{_ as computeThemeProperties,x as dataSeriesProperties};
@@ -0,0 +1 @@
1
+ import{apcaContrast as s,ensureContrast as m,gamutMapToSRGB as i,oklabToOklch as E,oklchToOklab as l,parseOklch as p,serializeOklch as S}from"../../shared/color-engine.js";import{DATA_SERIES_KEYS as b}from"../../shared/data-colors.js";import{TOKEN_PAIRINGS as u}from"../../shared/theme.js";const A=.75,f=u.text?.targetLc??75;function _(c,n){const t=m({...c},n,f);if(Math.abs(s(t,n))>=f)return t;const o=i({l:n.l>.5?1:0,c:c.c,h:c.h});return Math.abs(s(o,n))>Math.abs(s(t,n))?o:t}function k(c,n,t){const o=l(c),r=l(n);return E({L:o.L*t+r.L*(1-t),a:o.a*t+r.a*(1-t),b:o.b*t+r.b*(1-t)})}function h(c,n){const t=p(c["--esp-color-background"]??"");if(!t)return{};const o={};for(let r=1;r<=b.length;r+=1){const a=`--esp-color-series-${r}`,e=p(c[a]??n?.[a]??"");e&&(o[`${a}-wash`]=S(i(k(e,t,A))),o[`${a}-ink`]=S(_(e,t)))}return o}export{A as DATA_WASH_SERIES_SHARE,h as dataCompanionProperties};
@@ -1,4 +1,4 @@
1
- var h=function(f,e,t,r){var o=arguments.length,s=o<3?e:r===null?r=Object.getOwnPropertyDescriptor(e,t):r,a;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")s=Reflect.decorate(f,e,t,r);else for(var i=f.length-1;i>=0;i--)(a=f[i])&&(s=(o<3?a(s):o>3?a(e,t,s):a(e,t))||s);return o>3&&s&&Object.defineProperty(e,t,s),s};import{css as R,LitElement as C}from"lit";import{property as p,state as S}from"lit/decorators.js";import{subscribeToRootEvent as u}from"./root-event-subscription.js";import{traverseToClosest as b}from"./utilities.js";import{parseCssColor as w,serializeOklch as x,gamutMapToSRGB as P}from"./color-engine.js";import{computeVariants as v}from"../root/helpers/compute-variants.js";import{computeSemanticProperties as m}from"../root/helpers/compute-semantic-properties.js";import{lightnessRampProperties as E}from"../root/helpers/lightness-ramp-properties.js";import{ELEVATION_PROPERTIES as V}from"./scale-engine.js";import{ROLE_NAMES as U,mergeTheme as I,semanticToCSS as A}from"./theme.js";import{alignAttributeTextInheritance as O,focusRing as D}from"./style-fragments.js";import{syncNormalizedAttribute as N}from"./attribute-helpers.js";import{INTENT_VARIANTS as y,normalizeIntentVariant as z}from"./intent-values.js";const k=new WeakMap;class n extends C{constructor(){super(...arguments),this.seedColorBacker="oklch(0.7 0.125 216)",this.espRoot=null,this.contextBacker="",this.scopedTokenOriginalValues=new Map,this.warnedUnknownContexts=new Set,this.pendingInitialThemeRoot=null,this.rootEventSubscriptionsActive=!1,this.subscribedRoot=null,this.rootEventUnsubscribers=[],this.intentBacker="neutral",this.intentEmitsTokens=!0,this.warnedUnknownIntents=new Set,this.correlationId=globalThis.crypto?.randomUUID?.()??Math.random().toString(36),this.scheme="light",this.handleSeedColorChanged=e=>{this.syncRootFromDom(!1)&&(this.seedColor=e.seedColor,this.applyScopedColorTokens())},this.handleSchemeChanged=e=>{!this.syncRootFromDom(!1)||this.scheme===e.scheme||(this.scheme=e.scheme,this.applyScopedColorTokens())},this.handleThemeChanged=()=>{this.syncRootFromDom(!1)&&this.applyScopedColorTokens()},this.handleIconSpriteUrlChanged=()=>{this.syncRootFromDom(!1)&&this.requestUpdate()}}get seedColor(){return this.seedColorBacker}set seedColor(e){this.seedColorBacker=e}focusResolvedElementAfterUpdate(e,t){const r=()=>{const o=e();return o?(o.focus(t),!0):!1};r()||this.updateComplete.then(()=>{r()})}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 intent(){return this.intentBacker}set intent(e){const t=typeof e=="string"?e.trim():"";t!==""&&!y.includes(t)&&!this.warnedUnknownIntents.has(t)&&(this.warnedUnknownIntents.add(t),console.warn(`Espalier intent: "${t}" is not an intent; expected one of ${y.join(", ")}. Treating it as "neutral".`));const r=this.intentBacker,o=z(e);N(this,"intent",o),o!==r&&(this.intentBacker=o,this.requestUpdate("intent",r),this.applyTokensWhenRootReady())}get context(){return this.contextBacker}set context(e){const t=e??"";t!==this.contextBacker&&(this.contextBacker=t,this.applyTokensWhenRootReady(),this.refreshDescendantIntentDerivations())}refreshDescendantIntentDerivations(){const e=new Set,t=o=>{for(const s of Array.from(o.children))r(s)},r=o=>{if(!e.has(o)){if(e.add(o),o instanceof n&&o.applyTokensWhenRootReady(),o.shadowRoot&&t(o.shadowRoot),o instanceof HTMLSlotElement)for(const s of o.assignedElements({flatten:!0}))r(s);t(o)}};t(this),this.shadowRoot&&t(this.shadowRoot)}static flattenedParent(e){return e instanceof Element&&e.assignedSlot?e.assignedSlot:e instanceof ShadowRoot?e.host:e.parentNode}resolveAncestorZoneTheme(e){let t=n.flattenedParent(this);for(;t;){if(t instanceof Element&&t.localName==="esp-root")return null;if(t instanceof n){const r=t.contextBacker.trim();if(r){const o=this.resolveNamedContextTheme(e,r);if(o)return o}}t=n.flattenedParent(t)}return null}connectedCallback(){super.connectedCallback();const e=this.syncRootFromDom(!1);e&&this.subscribeToRootEvents(e)}disconnectedCallback(){super.disconnectedCallback(),this.unsubscribeFromRootEvents(),this.clearScopedTokens(),this.espRoot=null}firstUpdated(e){this.syncRootFromDom(!0),this.applyTokensWhenRootReady()}syncRootFromDom(e){const t=b(this,"esp-root");if(!t){if(this.espRoot&&this.clearScopedTokens(),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 r=t!==this.espRoot,s=t.scheme==="dark"?"dark":"light",a=this.scheme!==s,i=this.seedColor!==t.seedColor;return this.espRoot=t,r&&this.isConnected&&this.subscribeToRootEvents(t),(r||a||i)&&(this.scheme=s,this.seedColor=t.seedColor,this.applyTokensWhenRootReady()),r&&this.requestUpdate(),t}applyTokensWhenRootReady(){const e=this.espRoot;if(e&&(!e.hasUpdated||e.isUpdatePending)){this.applyTokensAfterInitialRootUpdate(e);return}this.applyScopedColorTokens()}applyTokensAfterInitialRootUpdate(e){if(this.pendingInitialThemeRoot===e)return;this.pendingInitialThemeRoot=e;const t=e.updateComplete;if(!t){this.pendingInitialThemeRoot=null,this.applyScopedColorTokens();return}t.then(()=>{this.pendingInitialThemeRoot===e&&(this.pendingInitialThemeRoot=null),!(!this.isConnected||this.espRoot!==e)&&(this.scheme=e.scheme==="dark"?"dark":"light",this.seedColor=e.seedColor,this.applyScopedColorTokens())})}subscribeToRootEvents(e){this.rootEventSubscriptionsActive&&this.subscribedRoot===e||(this.unsubscribeFromRootEvents(),this.subscribedRoot=e,this.rootEventUnsubscribers=[u(e,"seed-color-changed",this.handleSeedColorChanged),u(e,"scheme-changed",this.handleSchemeChanged),u(e,"theme-changed",this.handleThemeChanged),u(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 b(this,e)}applyScopedColorTokens(){if(!this.espRoot)return;const e=this.intentEmitsTokens?n.intentColorSources[this.intentBacker]:"";if(!e&&!this.context.trim()){this.clearScopedTokens();return}const t=this.espRoot.activeTheme;if(!t){this.clearScopedTokens();return}const r=this.resolveContextTheme(t),o=r.theme,s=w(o.seedColor);if(!s)return;const a=v(s,o),i={};if(r.applied&&(Object.assign(i,m(o,a)),Object.assign(i,E(o)),Object.assign(i,V)),e){let c=o;r.applied||(c=this.resolveAncestorZoneTheme(t)??o);const l=c===o?a:v(s,c),T=m(c,l,{effectiveSource:(g,d)=>n.semanticActionTokens.has(g)?e:d});i["--esp-color-primary"]=x(P(l[e]));for(const g of n.semanticActionTokens){const d=A(g);i[d]=T[d]}}this.applyScopedTokenProperties(i)}resolveContextTheme(e){const t=this.context.trim();if(!t)return{applied:!1,theme:e};const r=this.resolveNamedContextTheme(e,t);return r?{applied:!0,theme:r}:(this.warnedUnknownContexts.has(t)||(this.warnedUnknownContexts.add(t),console.warn(`Espalier context: "${t}" is not defined by the active theme; inheriting root tokens.`)),{applied:!1,theme:e})}resolveNamedContextTheme(e,t){const r=e.contexts&&Object.prototype.hasOwnProperty.call(e.contexts,t)?e.contexts[t]:void 0;if(!r)return null;let o=k.get(e);const s=o?.get(t);if(s)return s;const a={};for(const c of U){const l=r[c];l!==void 0&&(a[c]=l)}const i=I(e,{lightness:r.lightness,roles:a});return o||(o=new Map,k.set(e,o)),o.set(t,i),i}applyScopedTokenProperties(e){for(const t of this.scopedTokenOriginalValues.keys())t in e||this.restoreScopedTokenProperty(t);for(const[t,r]of Object.entries(e))this.setScopedTokenProperty(t,r)}setScopedTokenProperty(e,t){const r=this.scopedTokenOriginalValues.get(e);if(r){const o=this.style.getPropertyValue(e),s=this.style.getPropertyPriority(e);(o!==r.generatedValue||s!==r.generatedPriority)&&(r.originalValue=o.length?o:null,r.originalPriority=s),r.generatedValue=t,r.generatedPriority=""}else{const o=this.style.getPropertyValue(e);this.scopedTokenOriginalValues.set(e,{generatedPriority:"",generatedValue:t,originalPriority:this.style.getPropertyPriority(e),originalValue:o.length?o:null})}this.style.setProperty(e,t)}restoreScopedTokenProperty(e){const t=this.scopedTokenOriginalValues.get(e);if(!t)return;const r=this.style.getPropertyValue(e),o=this.style.getPropertyPriority(e);(r!==t.generatedValue||o!==t.generatedPriority)&&(t.originalValue=r.length?r:null,t.originalPriority=o),t.originalValue===null?this.style.removeProperty(e):this.style.setProperty(e,t.originalValue,t.originalPriority),this.scopedTokenOriginalValues.delete(e)}clearScopedTokens(){for(const e of[...this.scopedTokenOriginalValues.keys()])this.restoreScopedTokenProperty(e)}}n.intentColorSources={danger:"danger",info:"info",neutral:"",success:"success",warning:"warning"},n.semanticActionTokens=new Set(["actionBackground","actionText"]),n.styles=[D(".esp-field:focus-within","--esp-field-focus-shadow"),O,R`
1
+ var c=function(u,e,t,r){var o=arguments.length,s=o<3?e:r===null?r=Object.getOwnPropertyDescriptor(e,t):r,a;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")s=Reflect.decorate(u,e,t,r);else for(var i=u.length-1;i>=0;i--)(a=u[i])&&(s=(o<3?a(s):o>3?a(e,t,s):a(e,t))||s);return o>3&&s&&Object.defineProperty(e,t,s),s};import{css as R,LitElement as T}from"lit";import{property as d,state as C}from"lit/decorators.js";import{subscribeToRootEvent as p}from"./root-event-subscription.js";import{traverseToClosest as b}from"./utilities.js";import{parseCssColor as S,serializeOklch as w,gamutMapToSRGB as P}from"./color-engine.js";import{computeVariants as m}from"../root/helpers/compute-variants.js";import{computeSemanticProperties as v}from"../root/helpers/compute-semantic-properties.js";import{dataCompanionProperties as x}from"../root/helpers/data-companion-properties.js";import{dataSeriesProperties as E}from"../root/helpers/compute-theme-properties.js";import{lightnessRampProperties as V}from"../root/helpers/lightness-ramp-properties.js";import{ELEVATION_PROPERTIES as U}from"./scale-engine.js";import{resolveContextTheme as I,resolveDataColorSource as A,semanticToCSS as O}from"./theme.js";import{alignAttributeTextInheritance as D,focusRing as z}from"./style-fragments.js";import{syncNormalizedAttribute as F}from"./attribute-helpers.js";import{INTENT_VARIANTS as y,normalizeIntentVariant as N}from"./intent-values.js";class n extends T{constructor(){super(...arguments),this.seedColorBacker="oklch(0.7 0.125 216)",this.espRoot=null,this.contextBacker="",this.scopedTokenOriginalValues=new Map,this.warnedUnknownContexts=new Set,this.pendingInitialThemeRoot=null,this.rootEventSubscriptionsActive=!1,this.subscribedRoot=null,this.rootEventUnsubscribers=[],this.intentBacker="neutral",this.intentEmitsTokens=!0,this.warnedUnknownIntents=new Set,this.correlationId=globalThis.crypto?.randomUUID?.()??Math.random().toString(36),this.scheme="light",this.handleSeedColorChanged=e=>{this.syncRootFromDom(!1)&&(this.seedColor=e.seedColor,this.applyScopedColorTokens())},this.handleSchemeChanged=e=>{!this.syncRootFromDom(!1)||this.scheme===e.scheme||(this.scheme=e.scheme,this.applyScopedColorTokens())},this.handleThemeChanged=()=>{this.syncRootFromDom(!1)&&this.applyScopedColorTokens()},this.handleIconSpriteUrlChanged=()=>{this.syncRootFromDom(!1)&&this.requestUpdate()}}get seedColor(){return this.seedColorBacker}set seedColor(e){this.seedColorBacker=e}focusResolvedElementAfterUpdate(e,t){const r=()=>{const o=e();return o?(o.focus(t),!0):!1};r()||this.updateComplete.then(()=>{r()})}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 intent(){return this.intentBacker}set intent(e){const t=typeof e=="string"?e.trim():"";t!==""&&!y.includes(t)&&!this.warnedUnknownIntents.has(t)&&(this.warnedUnknownIntents.add(t),console.warn(`Espalier intent: "${t}" is not an intent; expected one of ${y.join(", ")}. Treating it as "neutral".`));const r=this.intentBacker,o=N(e);F(this,"intent",o),o!==r&&(this.intentBacker=o,this.requestUpdate("intent",r),this.applyTokensWhenRootReady())}get context(){return this.contextBacker}set context(e){const t=e??"";t!==this.contextBacker&&(this.contextBacker=t,this.applyTokensWhenRootReady(),this.refreshDescendantIntentDerivations())}refreshDescendantIntentDerivations(){const e=new Set,t=o=>{for(const s of Array.from(o.children))r(s)},r=o=>{if(!e.has(o)){if(e.add(o),o instanceof n&&o.applyTokensWhenRootReady(),o.shadowRoot&&t(o.shadowRoot),o instanceof HTMLSlotElement)for(const s of o.assignedElements({flatten:!0}))r(s);t(o)}};t(this),this.shadowRoot&&t(this.shadowRoot)}static flattenedParent(e){return e instanceof Element&&e.assignedSlot?e.assignedSlot:e instanceof ShadowRoot?e.host:e.parentNode}resolveAncestorZoneTheme(e){let t=n.flattenedParent(this);for(;t;){if(t instanceof Element&&t.localName==="esp-root")return null;if(t instanceof n){const r=t.contextBacker.trim();if(r){const o=this.resolveNamedContextTheme(e,r);if(o)return o}}t=n.flattenedParent(t)}return null}connectedCallback(){super.connectedCallback();const e=this.syncRootFromDom(!1);e&&this.subscribeToRootEvents(e)}disconnectedCallback(){super.disconnectedCallback(),this.unsubscribeFromRootEvents(),this.clearScopedTokens(),this.espRoot=null}firstUpdated(e){this.syncRootFromDom(!0),this.applyTokensWhenRootReady()}syncRootFromDom(e){const t=b(this,"esp-root");if(!t){if(this.espRoot&&this.clearScopedTokens(),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 r=t!==this.espRoot,s=t.scheme==="dark"?"dark":"light",a=this.scheme!==s,i=this.seedColor!==t.seedColor;return this.espRoot=t,r&&this.isConnected&&this.subscribeToRootEvents(t),(r||a||i)&&(this.scheme=s,this.seedColor=t.seedColor,this.applyTokensWhenRootReady()),r&&this.requestUpdate(),t}applyTokensWhenRootReady(){const e=this.espRoot;if(e&&(!e.hasUpdated||e.isUpdatePending)){this.applyTokensAfterInitialRootUpdate(e);return}this.applyScopedColorTokens()}applyTokensAfterInitialRootUpdate(e){if(this.pendingInitialThemeRoot===e)return;this.pendingInitialThemeRoot=e;const t=e.updateComplete;if(!t){this.pendingInitialThemeRoot=null,this.applyScopedColorTokens();return}t.then(()=>{this.pendingInitialThemeRoot===e&&(this.pendingInitialThemeRoot=null),!(!this.isConnected||this.espRoot!==e)&&(this.scheme=e.scheme==="dark"?"dark":"light",this.seedColor=e.seedColor,this.applyScopedColorTokens())})}subscribeToRootEvents(e){this.rootEventSubscriptionsActive&&this.subscribedRoot===e||(this.unsubscribeFromRootEvents(),this.subscribedRoot=e,this.rootEventUnsubscribers=[p(e,"seed-color-changed",this.handleSeedColorChanged),p(e,"scheme-changed",this.handleSchemeChanged),p(e,"theme-changed",this.handleThemeChanged),p(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 b(this,e)}applyScopedColorTokens(){if(!this.espRoot)return;const e=this.intentEmitsTokens?n.intentColorSources[this.intentBacker]:"";if(!e&&!this.context.trim()){this.clearScopedTokens();return}const t=this.espRoot.activeTheme;if(!t){this.clearScopedTokens();return}const r=this.resolveContextTheme(t),o=r.theme,s=S(o.seedColor);if(!s)return;const a=m(s,o),i={};if(r.applied&&(Object.assign(i,v(o,a)),Object.assign(i,V(o)),Object.assign(i,U),Object.assign(i,x(i,E(o,s,l=>(typeof l=="string"?A(l,o.anchors):null)??o.seedColor)))),e){let l=o;r.applied||(l=this.resolveAncestorZoneTheme(t)??o);const g=l===o?a:m(s,l),k=v(l,g,{effectiveSource:(f,h)=>n.semanticActionTokens.has(f)?e:h});i["--esp-color-primary"]=w(P(g[e]));for(const f of n.semanticActionTokens){const h=O(f);i[h]=k[h]}}this.applyScopedTokenProperties(i)}resolveContextTheme(e){const t=this.context.trim();if(!t)return{applied:!1,theme:e};const r=this.resolveNamedContextTheme(e,t);return r?{applied:!0,theme:r}:(this.warnedUnknownContexts.has(t)||(this.warnedUnknownContexts.add(t),console.warn(`Espalier context: "${t}" is not defined by the active theme; inheriting root tokens.`)),{applied:!1,theme:e})}resolveNamedContextTheme(e,t){return I(e,t)}applyScopedTokenProperties(e){for(const t of this.scopedTokenOriginalValues.keys())t in e||this.restoreScopedTokenProperty(t);for(const[t,r]of Object.entries(e))this.setScopedTokenProperty(t,r)}setScopedTokenProperty(e,t){const r=this.scopedTokenOriginalValues.get(e);if(r){const o=this.style.getPropertyValue(e),s=this.style.getPropertyPriority(e);(o!==r.generatedValue||s!==r.generatedPriority)&&(r.originalValue=o.length?o:null,r.originalPriority=s),r.generatedValue=t,r.generatedPriority=""}else{const o=this.style.getPropertyValue(e);this.scopedTokenOriginalValues.set(e,{generatedPriority:"",generatedValue:t,originalPriority:this.style.getPropertyPriority(e),originalValue:o.length?o:null})}this.style.setProperty(e,t)}restoreScopedTokenProperty(e){const t=this.scopedTokenOriginalValues.get(e);if(!t)return;const r=this.style.getPropertyValue(e),o=this.style.getPropertyPriority(e);(r!==t.generatedValue||o!==t.generatedPriority)&&(t.originalValue=r.length?r:null,t.originalPriority=o),t.originalValue===null?this.style.removeProperty(e):this.style.setProperty(e,t.originalValue,t.originalPriority),this.scopedTokenOriginalValues.delete(e)}clearScopedTokens(){for(const e of[...this.scopedTokenOriginalValues.keys()])this.restoreScopedTokenProperty(e)}}n.intentColorSources={danger:"danger",info:"info",neutral:"",success:"success",warning:"warning"},n.semanticActionTokens=new Set(["actionBackground","actionText"]),n.styles=[z(".esp-field:focus-within","--esp-field-focus-shadow"),D,R`
2
2
  :host {
3
3
 
4
4
  --_esp-field-resolved-hover-bg: var(
@@ -122,4 +122,4 @@ var h=function(f,e,t,r){var o=arguments.length,s=o<3?e:r===null?r=Object.getOwnP
122
122
  .esp-field:has(textarea:disabled):hover {
123
123
  background-color: var(--esp-field-background, var(--esp-color-layer-2));
124
124
  }
125
- `],h([S()],n.prototype,"seedColor",null),h([p({attribute:!1})],n.prototype,"correlationId",void 0),h([p({type:String,reflect:!0})],n.prototype,"scheme",void 0),h([p({type:String,attribute:"intent",reflect:!0,useDefault:!0})],n.prototype,"intent",null),h([p({type:String,attribute:"context",reflect:!0})],n.prototype,"context",null);export{n as EspalierElementBase};
125
+ `],c([C()],n.prototype,"seedColor",null),c([d({attribute:!1})],n.prototype,"correlationId",void 0),c([d({type:String,reflect:!0})],n.prototype,"scheme",void 0),c([d({type:String,attribute:"intent",reflect:!0,useDefault:!0})],n.prototype,"intent",null),c([d({type:String,attribute:"context",reflect:!0})],n.prototype,"context",null);export{n as EspalierElementBase};
@@ -48,7 +48,7 @@
48
48
  * report.adjustedTokens; // ['linkHover']
49
49
  * ```
50
50
  */
51
- import { type EspalierTheme, type LightnessKey, type MappingSource, type SemanticColorName } from "./theme.js";
51
+ import { type EspalierTheme, type LightnessKey, 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. */
@@ -105,6 +105,14 @@ export interface ThemeFitToken {
105
105
  anchor?: ThemeFitAnchor;
106
106
  /** Present for the text tokens APCA enforcement guards. */
107
107
  apca?: ThemeFitApcaAction;
108
+ /**
109
+ * Present when the mapping was pinned token-level rather than compiled
110
+ * from roles (ESP0175). `"declared"` means this surface's own
111
+ * declaration pinned it; `"inherited"` means a root-level pin survived
112
+ * into this context — the silent case this marker exists to surface,
113
+ * since a zone's unexpected value then traces to a root theme line.
114
+ */
115
+ explicit?: "declared" | "inherited";
108
116
  }
109
117
  /**
110
118
  * A theme's fit report — the stable JSON shape the docs site renders.
@@ -135,6 +143,17 @@ export interface ThemeFitRow {
135
143
  /** The anchor reference and its ΔE; empty for non-anchor sources. */
136
144
  anchor: string;
137
145
  }
146
+ /** Options for {@link themeFitReport}. */
147
+ export interface ThemeFitReportOptions {
148
+ /**
149
+ * Report a named context's surface instead of the root (ESP0175). The
150
+ * context compiles through the exact zone path a `context` attribute
151
+ * uses — `resolveContextTheme` — so the report cannot drift from what
152
+ * a zone host renders. Unknown names throw, mirroring the zone host's
153
+ * warning for an undefined context.
154
+ */
155
+ context?: string;
156
+ }
138
157
  /**
139
158
  * Build the fit report for a resolved theme.
140
159
  *
@@ -142,10 +161,11 @@ export interface ThemeFitRow {
142
161
  * what it did. An unparseable seed produces an empty token list, exactly
143
162
  * as it produces an empty property record.
144
163
  *
145
- * @param theme A resolved theme (as `mergeTheme` returns).
146
- * @param scheme The scheme to compute.
164
+ * @param theme A resolved theme (as `mergeTheme` returns).
165
+ * @param scheme The scheme to compute.
166
+ * @param options Surface selection; omit for the root surface.
147
167
  */
148
- export declare function themeFitReport(theme: EspalierTheme, scheme: "light" | "dark"): ThemeFitReport;
168
+ export declare function themeFitReport(theme: EspalierTheme, scheme: "light" | "dark", options?: ThemeFitReportOptions): ThemeFitReport;
149
169
  /**
150
170
  * Render a report as `console.table` rows.
151
171
  *
@@ -160,3 +180,73 @@ export declare function themeFitRows(report: ThemeFitReport): ThemeFitRow[];
160
180
  * then a `console.table` of every token.
161
181
  */
162
182
  export declare function printThemeFitReport(report: ThemeFitReport): void;
183
+ /** One cross-token finding over a single surface's emitted values. */
184
+ export interface ThemeFitLint {
185
+ /** Stable lint identifier. */
186
+ id: "action-canvas-separation" | "link-hover-ordering";
187
+ /** Lints describe design-quality hazards, not validation failures. */
188
+ severity: "warning";
189
+ /** The tokens the finding is about. */
190
+ tokens: SemanticColorName[];
191
+ /** The measured number the lint fired on (ΔE-OK, or an Lc deficit). */
192
+ measured: number;
193
+ /** Human-readable finding, with the remedy phrased as a retune. */
194
+ message: string;
195
+ }
196
+ /**
197
+ * Cross-token lints over one surface's report (ESP0175).
198
+ *
199
+ * Both checks consume only the report's `resolved` strings — the emitted
200
+ * custom-property values — so the lints, like the report, cannot
201
+ * disagree with the pipeline:
202
+ *
203
+ * - **action-canvas-separation** — a filled action within ΔE-OK 0.045 of
204
+ * the surface's own background vanishes into it. The stop placement
205
+ * avoids this when any viable stop exists; the lint reports the
206
+ * residue (explicit pins, ramps with no separated stop).
207
+ * - **link-hover-ordering** — measured on the surfaces the states
208
+ * actually render: resting `link` on `background`, hovered `linkHover`
209
+ * on the `linkHoverBg` wash. When the theme paints a real wash, the
210
+ * wash itself signals the state and the ink must clear the text tier
211
+ * there; when the wash equals the background (no visible wash), hover
212
+ * must not read weaker than rest — a dimming hover feels disabled.
213
+ */
214
+ export declare function themeFitLints(report: ThemeFitReport): ThemeFitLint[];
215
+ /**
216
+ * The root surface's identifier in a suite. Deliberately not a valid
217
+ * context slug (slugs are letter-first), so no theme-declared context
218
+ * name — `root` included, which has been a legal name since contexts
219
+ * shipped — can ever collide with it.
220
+ */
221
+ export declare const ROOT_SURFACE = ":root";
222
+ /** One surface's report inside a {@link ThemeFitSuite}. */
223
+ export interface ThemeFitSurfaceReport extends ThemeFitReport {
224
+ /** {@link ROOT_SURFACE}, or the context name a zone would set. */
225
+ surface: string;
226
+ /** Cross-token lints over this surface's emitted values. */
227
+ lints: ThemeFitLint[];
228
+ }
229
+ /**
230
+ * Every surface a themed site has, both schemes, in one stable shape.
231
+ */
232
+ export interface ThemeFitSuite {
233
+ /** Surface names in report order: {@link ROOT_SURFACE}, then contexts, sorted. */
234
+ surfaces: string[];
235
+ /** One report per surface the light theme defines. */
236
+ light: ThemeFitSurfaceReport[];
237
+ /** One report per surface the dark theme defines. */
238
+ dark: ThemeFitSurfaceReport[];
239
+ }
240
+ /**
241
+ * Fit-report every surface a themed site has (ESP0175): for each scheme,
242
+ * the root plus every declared context, each with its cross-token lints.
243
+ *
244
+ * Takes the partials an `esp-root` would take (they merge over the same
245
+ * scheme defaults the root uses), so the suite describes exactly the
246
+ * site the partials produce — the 448-line hand-rolled check script,
247
+ * minus the client-specific lines. Surfaces are the union of both
248
+ * schemes' context names so an asymmetric pair still reports every
249
+ * surface it can; `validateThemePair` is the gate that rejects the
250
+ * asymmetry itself.
251
+ */
252
+ export declare function themeFitReportSuite(lightPartial: PartialTheme, darkPartial: PartialTheme): ThemeFitSuite;
@@ -1 +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};
1
+ import{apcaContrast as h,deltaEOK as f,parseOklch as p,serializeOklch as v}from"./color-engine.js";import{DEFAULT_DARK_THEME as T,DEFAULT_LIGHT_THEME as R,explicitMappingTokens as $,mergeTheme as m,parseAnchorSource as x,resolveContextTheme as A}from"./theme.js";import{computeThemeProperties as H}from"../root/helpers/compute-theme-properties.js";function _(e,t,n={}){let o=e,r=null;if(n.context!==void 0){const c=A(e,n.context);if(!c)throw new Error(`themeFitReport: context "${n.context}" is not defined by the theme.`);o=c,r=new Set(Object.keys(e.contexts[n.context]?.semanticMappings??{}))}const i=[];H(o,t,i);const l=$(o),u=i.map(c=>{const s=j(c);return l.has(s.token)&&(s.explicit=r===null||r.has(s.token)?"declared":"inherited"),s});return{scheme:t,seedColor:o.seedColor,anchors:M(o.anchors),tokens:u,adjustedTokens:u.filter(c=>c.adjusted).map(c=>c.token)}}function w(e){return e.tokens.map(t=>({token:t.token,source:t.source,requested:t.requested,resolved:t.resolved,apca:I(t),anchor:F(t)}))}function D(e){const t=e.adjustedTokens.length>0?`APCA moved ${e.adjustedTokens.join(", ")}`:"APCA moved nothing";console.info(`Espalier theme fit \u2014 ${e.scheme} scheme, seed ${e.seedColor}. ${t}.`),console.table(w(e))}const L=.045,b=.5,k=75,O=.045;function C(e){const t=[],n=new Map(e.tokens.map(s=>[s.token,s.resolved])),o=s=>{const a=n.get(s);return a===void 0?null:p(a)},r=o("background"),i=o("actionBackground");if(r&&i){const s=f(i,r);if(s<L){const a=d(s,4);t.push({id:"action-canvas-separation",severity:"warning",tokens:["actionBackground","background"],measured:a,message:`actionBackground sits \u0394E ${a} from background (floor ${L}) \u2014 filled actions vanish into this canvas. Retune the surface apart: move the background stop, or pin actionBackground to a separated stop.`})}}const l=o("link"),u=o("linkHover"),c=o("linkHoverBg");if(r&&l&&u&&c){const s=Math.abs(h(l,r)),a=Math.abs(h(u,c));f(c,r)>=O?a+b<k&&t.push({id:"link-hover-ordering",severity:"warning",tokens:["linkHover","linkHoverBg"],measured:d(k-a,1),message:`linkHover reaches only Lc ${d(a,1)} on the linkHoverBg wash it actually paints (tier ${k}) \u2014 retune linkHover or the wash apart until the hovered state clears the text tier.`}):a+b<s&&t.push({id:"link-hover-ordering",severity:"warning",tokens:["linkHover","link"],measured:d(s-a,1),message:`linkHover reads weaker than link on the same canvas (Lc ${d(a,1)} vs ${d(s,1)}) \u2014 a dimming hover feels disabled. Retune linkHover toward more contrast than the resting link.`})}return t}const g=":root";function V(e,t){const n=m(R,e),o=m(T,t),r=[g,...[...new Set([...Object.keys(n.contexts),...Object.keys(o.contexts)])].sort()],i=(l,u)=>{const c=[];for(const s of r){if(s!==g&&!(s in l.contexts))continue;const a=_(l,u,s===g?{}:{context:s});c.push({...a,surface:s,lints:C(a)})}return c};return{surfaces:r,light:i(n,"light"),dark:i(o,"dark")}}function d(e,t){const n=Math.pow(10,t);return Math.round(e*n)/n}function E(e,t){return t===void 0?e:`${e}.${t}`}function M(e){const t={};for(const[n,o]of Object.entries(e)){if(typeof o=="string"){t[n]=o;continue}for(const[r,i]of Object.entries(o))t[r==="color"?n:E(n,r)]=i}return t}function j(e){const t=p(e.resolved),n=e.requested===void 0?e.resolved:v(e.requested),o={token:e.token,property:e.property,source:e.source,lightness:e.lightness,requested:n,resolved:e.resolved,adjusted:n!==e.resolved,sourceResolved:e.sourceResolved},r=x(e.source);if(r&&e.sourceResolved&&(o.anchor={reference:E(r.name,r.slot),color:v(e.base),deltaE:d(f(e.base,t),4)}),e.contrast){const i=e.contrast.background;o.apca={from:n,to:e.resolved,targetLc:e.contrast.targetLc,against:e.contrast.against,againstColor:v(i),fromLc:d(Math.abs(h(p(n),i)),1),toLc:d(Math.abs(h(t,i)),1)}}return o}function I(e){if(!e.apca)return"";const{targetLc:t,against:n,fromLc:o,toLc:r}=e.apca;return e.adjusted?`Lc ${t} vs ${n} \u2014 pushed from Lc ${o} to Lc ${r}`:`Lc ${t} vs ${n} \u2014 met at Lc ${o}`}function F(e){return e.anchor?`${e.anchor.reference} \u0394E ${e.anchor.deltaE}`:e.sourceResolved?"":"unresolved \u2014 seed used"}export{g as ROOT_SURFACE,D as printThemeFitReport,C as themeFitLints,_ as themeFitReport,V as themeFitReportSuite,w as themeFitRows};
@@ -0,0 +1,79 @@
1
+ /**
2
+ * @module theme-swatches
3
+ *
4
+ * Brand book in, reviewable theme out (ESP0175).
5
+ *
6
+ * The lightness-ramps guide documents the fitting procedure by hand:
7
+ * convert every swatch to OKLCH, seat each on the ramp stop whose job
8
+ * matches its lightness (set the stop to the swatch's *exact* L so the
9
+ * anchor lands on its swatch instead of a relative of it), interpolate
10
+ * the unseated stops between their seated neighbors, and repeat against
11
+ * the dark scheme's inverted expectations. This module automates those
12
+ * mechanical steps — {@link deriveLightnessRamp} — and wraps them in
13
+ * {@link themeFromSwatches}, which returns a paired partial-theme
14
+ * **starter**: anchors, seated ramps, and the two role guesses that are
15
+ * safe to make from lightness alone. Everything else — the remaining
16
+ * roles, contexts, intents — is the design work the guide walks through,
17
+ * and the output is meant to be hand-tuned and reviewed, not shipped
18
+ * as-is. Verify the result the way the guide says to: a fit report per
19
+ * scheme (a seated anchor shows a near-zero ΔE), then `validateThemePair`.
20
+ */
21
+ import { type LightnessMap, type PartialTheme } from "./theme.js";
22
+ /**
23
+ * Derive a full lightness ramp by seating brand swatches on the scheme's
24
+ * default ramp — the guide's procedure, steps 1–4, automated.
25
+ *
26
+ * The stops that carry a token identity — `surface` (the canvas) and
27
+ * `text` (body copy) — seat first, each taking the nearest swatch within
28
+ * tolerance: those are the two seats where "the anchor lands on its
29
+ * swatch" matters most, and a numerically closer neighboring stop must
30
+ * not steal their swatch (the guide seats cream on `surface`, not on
31
+ * the fractionally closer `raised1`). The remaining seating is greedy
32
+ * by closeness: the (stop, swatch) pair with the smallest lightness
33
+ * distance seats first, each stop and each swatch seat at most once, a
34
+ * pair further than the seating tolerance never seats, and a seat that
35
+ * would invert the ramp's ordering (relative to the stops already
36
+ * seated) is skipped. Unseated stops interpolate between their seated
37
+ * neighbors along the default ramp; stops outside the seated span shift
38
+ * parallel to their nearest seated neighbor, so the ramp keeps the
39
+ * default's shape wherever the brand is silent.
40
+ *
41
+ * @param swatches Brand swatches, name → CSS color (any accepted form).
42
+ * @param scheme Which scheme's default ramp to seat against.
43
+ */
44
+ export declare function deriveLightnessRamp(swatches: Record<string, string>, scheme: "light" | "dark"): LightnessMap;
45
+ /** Options for {@link themeFromSwatches}. */
46
+ export interface ThemeFromSwatchesOptions {
47
+ /** Brand swatches, name → CSS color in any accepted form. */
48
+ swatches: Record<string, string>;
49
+ /** Seed override; defaults to the most chromatic swatch's color. */
50
+ seedColor?: string;
51
+ }
52
+ /** The paired starter {@link themeFromSwatches} returns. */
53
+ export interface ThemeFromSwatchesResult {
54
+ light: PartialTheme;
55
+ dark: PartialTheme;
56
+ }
57
+ /**
58
+ * Turn a brand book's swatches into a paired partial-theme starter.
59
+ *
60
+ * What it derives, and why only this much:
61
+ *
62
+ * - **anchors** — every swatch, keyed by its normalized slug: names are
63
+ * lowercased with non-alphanumeric runs collapsed to single hyphens
64
+ * ("Rose Gold" → `rose-gold`); unnormalizable or colliding names
65
+ * throw rather than silently renaming past each other.
66
+ * - **seedColor** — the most chromatic swatch unless overridden; the
67
+ * seed drives every derived family, and the brand's most saturated
68
+ * color is where that identity lives.
69
+ * - **lightness** — both schemes' ramps via {@link deriveLightnessRamp}.
70
+ * - **roles** — only the two guesses lightness alone justifies: a
71
+ * near-white swatch (L ≥ 0.9) becomes the light scheme's `canvas`, a
72
+ * near-black one (L ≤ 0.3) the dark scheme's; the most chromatic
73
+ * mid-band swatch (0.3 ≤ L ≤ 0.65) becomes `action` in both.
74
+ *
75
+ * Every other decision — ink, accent, contexts, intents — is design, not
76
+ * derivation: make them in the theming guide's order, and verify with a
77
+ * fit report per scheme.
78
+ */
79
+ export declare function themeFromSwatches(options: ThemeFromSwatchesOptions): ThemeFromSwatchesResult;
@@ -0,0 +1 @@
1
+ import{parseCssColor as M}from"./color-engine.js";import{isSafeKey as b}from"./unsafe-keys.js";import{ANCHOR_SLUG_PATTERN as L,COLOR_SOURCES as T,DEFAULT_DARK_THEME as F,DEFAULT_LIGHT_THEME as _,LIGHTNESS_KEYS as E}from"./theme.js";const $=.08;function x(i){return Object.entries(i).map(([c,n])=>{const s=M(n);if(!s)throw new Error(`themeFromSwatches: swatch "${c}" is not a parseable CSS color: "${n}".`);return{name:c,l:s.l,c:s.c}})}function O(i,c){const n=(c==="light"?_:F).lightness,s=x(i),o=[...E].sort((e,t)=>n[e]-n[t]),a=new Map,w=new Set,p=(e,t)=>{if(a.has(e)||w.has(t.name))return!1;const r=o.indexOf(e);for(const[l,h]of a){const d=o.indexOf(l);if(d<r&&h>t.l||d>r&&h<t.l)return!1}return a.set(e,t.l),w.add(t.name),!0};for(const e of["surface","text"]){const t=[...s].filter(r=>Math.abs(n[e]-r.l)<=$).sort((r,l)=>Math.abs(n[e]-r.l)-Math.abs(n[e]-l.l))[0];t&&p(e,t)}const S=o.flatMap(e=>s.map(t=>({stop:e,swatch:t,distance:Math.abs(n[e]-t.l)}))).filter(e=>e.distance<=$).sort((e,t)=>e.distance-t.distance);for(const{stop:e,swatch:t}of S)p(e,t);const f={};for(const e of E){const t=a.get(e);if(t!==void 0){f[e]=t;continue}const r=o.indexOf(e);let l,h;for(const[m]of a){const u=o.indexOf(m);u<r&&(l===void 0||u>o.indexOf(l))&&(l=m),u>r&&(h===void 0||u<o.indexOf(h))&&(h=m)}const d=n[e];if(l!==void 0&&h!==void 0){const m=n[l],u=n[h],g=a.get(l),C=a.get(h);f[e]=u===m?g:g+(d-m)/(u-m)*(C-g)}else l!==void 0?f[e]=R(d+(a.get(l)-n[l])):h!==void 0?f[e]=R(d+(a.get(h)-n[h])):f[e]=d}for(const e of E)f[e]=y(f[e]);return f}function R(i){return Math.min(1,Math.max(0,i))}function y(i){return Math.round(i*1e3)/1e3}function v(i){const c=new Map;for(const s of Object.keys(i)){const o=s.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"");if(!L.test(o))throw new Error(`themeFromSwatches: swatch name "${s}" cannot become an anchor slug (normalized to "${o}"; a slug is lowercase letters, digits, and hyphens, starting with a letter). Rename the swatch.`);if(T.includes(o))throw new Error(`themeFromSwatches: swatch name "${s}" normalizes to the reserved color source "${o}". Rename the swatch.`);if(!b(o))throw new Error(`themeFromSwatches: swatch name "${s}" normalizes to "${o}", a reserved JavaScript property name. Rename the swatch.`);const a=c.get(o);if(a!==void 0)throw new Error(`themeFromSwatches: swatch names "${a}" and "${s}" collide as "${o}". Rename one.`);c.set(o,s)}const n={};for(const[s,o]of c)n[s]=i[o];return n}function H(i){const c=v(i.swatches),n=x(c);if(n.length===0)throw new Error("themeFromSwatches: at least one swatch is required.");const s=[...n].sort((t,r)=>r.c-t.c)[0],o=i.seedColor??c[s.name],a={},w={},p=[...n].sort((t,r)=>r.l-t.l)[0];p.l>=.9&&(a.canvas=`anchor:${p.name}`);const S=[...n].sort((t,r)=>t.l-r.l)[0];S.l<=.3&&(w.canvas=`anchor:${S.name}`);const f=n.filter(t=>t.l>=.3&&t.l<=.65).sort((t,r)=>r.c-t.c)[0];f&&(a.action=`anchor:${f.name}`,w.action=`anchor:${f.name}`);const e={...c};return{light:{anchors:e,lightness:O(c,"light"),roles:a,seedColor:o},dark:{anchors:e,lightness:O(c,"dark"),roles:w,seedColor:o}}}export{O as deriveLightnessRamp,H as themeFromSwatches};
@@ -228,9 +228,16 @@ export type ThemeRoles = {
228
228
  * A context may also carry a partial lightness ramp. Color sources alone
229
229
  * cannot make a dark zone inside a light scheme because semantic derivation
230
230
  * deliberately takes lightness from the ramp rather than from an anchor.
231
+ *
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.
231
237
  */
232
238
  export type ThemeContext = ThemeRoles & {
233
239
  lightness?: Partial<LightnessMap>;
240
+ semanticMappings?: Partial<SemanticMappings>;
234
241
  };
235
242
  /** Named role-rebinding zones available to `context` attributes. */
236
243
  export type ThemeContexts = Record<string, ThemeContext>;
@@ -274,6 +281,8 @@ interface CompileRoleOptions {
274
281
  explicitMappings?: Partial<SemanticMappings>;
275
282
  /** Exact maximum paired-ink contrast of an action surface candidate. */
276
283
  actionSurfaceContrast?: (source: MappingSource, stop: LightnessKey) => number;
284
+ /** ΔE-OK between an action surface candidate and the theme's background. */
285
+ actionSurfaceSeparation?: (source: MappingSource, stop: LightnessKey) => number;
277
286
  }
278
287
  /**
279
288
  * Compile roles into semantic mappings.
@@ -635,6 +644,18 @@ export declare function parseTheme(base64: string): PartialTheme | null;
635
644
  * @returns A Base64-encoded JSON string.
636
645
  */
637
646
  export declare function encodeTheme(partial: PartialTheme): string;
647
+ /**
648
+ * Recover explicit mapping provenance from a resolved theme.
649
+ *
650
+ * Themes produced in this process carry exact provenance in the WeakMap.
651
+ * The comparison fallback preserves the historical behavior for a fully
652
+ * resolved theme reconstructed outside `mergeTheme`, where provenance is
653
+ * necessarily unavailable because it is not part of the serialized model.
654
+ *
655
+ * Exported for the fit report (ESP0175), which marks explicit tokens so
656
+ * a zone's surprising value traces to the mapping that pinned it.
657
+ */
658
+ export declare function explicitMappingTokens(theme: EspalierTheme): Set<SemanticColorName>;
638
659
  /**
639
660
  * Deep-merge a {@link PartialTheme} over a set of defaults.
640
661
  *
@@ -648,6 +669,21 @@ export declare function encodeTheme(partial: PartialTheme): string;
648
669
  * @returns A new fully-resolved {@link EspalierTheme}.
649
670
  */
650
671
  export declare function mergeTheme(defaults: EspalierTheme, overrides: PartialTheme): EspalierTheme;
672
+ /**
673
+ * Compile a named context over a resolved theme — the exact theme a
674
+ * `context="<name>"` zone renders (ESP0175).
675
+ *
676
+ * This is the same compilation `esp-element-base` performs for a zone
677
+ * 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.
682
+ *
683
+ * @param theme A resolved theme (as `mergeTheme` returns).
684
+ * @param name The context name a zone would set in its attribute.
685
+ */
686
+ export declare function resolveContextTheme(theme: EspalierTheme, name: string): EspalierTheme | null;
651
687
  /**
652
688
  * Validate a single encoded theme partial.
653
689
  *