tempest-react-sdk 0.72.0 → 0.74.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/README.md +6 -4
  2. package/dist/br/MapMarkers.cjs +1 -1
  3. package/dist/br/MapMarkers.cjs.map +1 -1
  4. package/dist/br/MapMarkers.js +19 -22
  5. package/dist/br/MapMarkers.js.map +1 -1
  6. package/dist/components/AIChat/AIChatTurn.cjs +1 -1
  7. package/dist/components/AIChat/AIChatTurn.cjs.map +1 -1
  8. package/dist/components/AIChat/AIChatTurn.js +1 -1
  9. package/dist/components/AIChat/AIChatTurn.js.map +1 -1
  10. package/dist/components/AppShell/AppShell.module.cjs.map +1 -1
  11. package/dist/components/AppShell/AppShell.module.js.map +1 -1
  12. package/dist/components/Button/Button.cjs +1 -1
  13. package/dist/components/Button/Button.cjs.map +1 -1
  14. package/dist/components/Button/Button.js +41 -36
  15. package/dist/components/Button/Button.js.map +1 -1
  16. package/dist/components/Button/Button.module.cjs.map +1 -1
  17. package/dist/components/Button/Button.module.js.map +1 -1
  18. package/dist/components/Button/ButtonContent.cjs +2 -0
  19. package/dist/components/Button/ButtonContent.cjs.map +1 -0
  20. package/dist/components/Button/ButtonContent.js +34 -0
  21. package/dist/components/Button/ButtonContent.js.map +1 -0
  22. package/dist/components/Button/ButtonSlot.cjs +2 -0
  23. package/dist/components/Button/ButtonSlot.cjs.map +1 -0
  24. package/dist/components/Button/ButtonSlot.js +36 -0
  25. package/dist/components/Button/ButtonSlot.js.map +1 -0
  26. package/dist/components/Button/button-class.cjs +2 -0
  27. package/dist/components/Button/button-class.cjs.map +1 -0
  28. package/dist/components/Button/button-class.js +13 -0
  29. package/dist/components/Button/button-class.js.map +1 -0
  30. package/dist/components/Button/link-rel.cjs +2 -0
  31. package/dist/components/Button/link-rel.cjs.map +1 -0
  32. package/dist/components/Button/link-rel.js +11 -0
  33. package/dist/components/Button/link-rel.js.map +1 -0
  34. package/dist/components/Button/slot.cjs +2 -0
  35. package/dist/components/Button/slot.cjs.map +1 -0
  36. package/dist/components/Button/slot.js +41 -0
  37. package/dist/components/Button/slot.js.map +1 -0
  38. package/dist/components/Collapsible/Collapsible.cjs +1 -1
  39. package/dist/components/Collapsible/Collapsible.cjs.map +1 -1
  40. package/dist/components/Collapsible/Collapsible.js +1 -1
  41. package/dist/components/Collapsible/Collapsible.js.map +1 -1
  42. package/dist/components/Form/Form.cjs.map +1 -1
  43. package/dist/components/Form/Form.js.map +1 -1
  44. package/dist/components/InstallButton/InstallButton.cjs.map +1 -1
  45. package/dist/components/InstallButton/InstallButton.js.map +1 -1
  46. package/dist/components/Modal/Modal.cjs +1 -1
  47. package/dist/components/Modal/Modal.cjs.map +1 -1
  48. package/dist/components/Modal/Modal.js +1 -1
  49. package/dist/components/Modal/Modal.js.map +1 -1
  50. package/dist/components/Modal/Modal.module.cjs +1 -1
  51. package/dist/components/Modal/Modal.module.cjs.map +1 -1
  52. package/dist/components/Modal/Modal.module.js +1 -1
  53. package/dist/components/Modal/Modal.module.js.map +1 -1
  54. package/dist/components/Page/Page.cjs +1 -1
  55. package/dist/components/Page/Page.cjs.map +1 -1
  56. package/dist/components/Page/Page.js +23 -39
  57. package/dist/components/Page/Page.js.map +1 -1
  58. package/dist/components/Page/Page.module.cjs +1 -1
  59. package/dist/components/Page/Page.module.cjs.map +1 -1
  60. package/dist/components/Page/Page.module.js +5 -11
  61. package/dist/components/Page/Page.module.js.map +1 -1
  62. package/dist/components/Reveal/Reveal.cjs +2 -0
  63. package/dist/components/Reveal/Reveal.cjs.map +1 -0
  64. package/dist/components/Reveal/Reveal.js +56 -0
  65. package/dist/components/Reveal/Reveal.js.map +1 -0
  66. package/dist/components/Reveal/Reveal.module.cjs +3 -0
  67. package/dist/components/Reveal/Reveal.module.cjs.map +1 -0
  68. package/dist/components/Reveal/Reveal.module.js +7 -0
  69. package/dist/components/Reveal/Reveal.module.js.map +1 -0
  70. package/dist/components/Reveal/reveal-threshold.cjs +2 -0
  71. package/dist/components/Reveal/reveal-threshold.cjs.map +1 -0
  72. package/dist/components/Reveal/reveal-threshold.js +13 -0
  73. package/dist/components/Reveal/reveal-threshold.js.map +1 -0
  74. package/dist/components/SectionHeader/SectionHeader.cjs +2 -0
  75. package/dist/components/SectionHeader/SectionHeader.cjs.map +1 -0
  76. package/dist/components/SectionHeader/SectionHeader.js +58 -0
  77. package/dist/components/SectionHeader/SectionHeader.js.map +1 -0
  78. package/dist/components/SectionHeader/SectionHeader.module.cjs +3 -0
  79. package/dist/components/SectionHeader/SectionHeader.module.cjs.map +1 -0
  80. package/dist/components/SectionHeader/SectionHeader.module.js +15 -0
  81. package/dist/components/SectionHeader/SectionHeader.module.js.map +1 -0
  82. package/dist/components/Timeline/Timeline.cjs +1 -1
  83. package/dist/components/Timeline/Timeline.cjs.map +1 -1
  84. package/dist/components/Timeline/Timeline.js +22 -15
  85. package/dist/components/Timeline/Timeline.js.map +1 -1
  86. package/dist/components/Timeline/Timeline.module.cjs +1 -1
  87. package/dist/components/Timeline/Timeline.module.cjs.map +1 -1
  88. package/dist/components/Timeline/Timeline.module.js +1 -1
  89. package/dist/components/Timeline/Timeline.module.js.map +1 -1
  90. package/dist/styles/Reveal.css +2 -0
  91. package/dist/styles/SectionHeader.css +2 -0
  92. package/dist/styles/actions.css +24 -22
  93. package/dist/styles/base.css +3 -1
  94. package/dist/styles/component/AppShell.css +1 -1
  95. package/dist/styles/component/Button.css +24 -22
  96. package/dist/styles/component/Modal.css +2 -2
  97. package/dist/styles/component/Page.css +0 -6
  98. package/dist/styles/component/Reveal.css +9 -0
  99. package/dist/styles/component/SectionHeader.css +9 -0
  100. package/dist/styles/component/Timeline.css +5 -5
  101. package/dist/styles/core.css +3 -1
  102. package/dist/styles/data.css +5 -5
  103. package/dist/styles/inverse.css +147 -0
  104. package/dist/styles/layout.css +9 -7
  105. package/dist/styles/manifest.json +14 -1
  106. package/dist/styles/overlay.css +2 -2
  107. package/dist/styles/utility.css +8 -0
  108. package/dist/styles.css +1 -1
  109. package/dist/tempest-react-sdk.cjs +1 -1
  110. package/dist/tempest-react-sdk.d.ts +353 -4
  111. package/dist/tempest-react-sdk.js +350 -346
  112. package/dist/theme/create-theme.cjs +3 -3
  113. package/dist/theme/create-theme.cjs.map +1 -1
  114. package/dist/theme/create-theme.js +67 -55
  115. package/dist/theme/create-theme.js.map +1 -1
  116. package/dist/theme/inverse-render.cjs +3 -0
  117. package/dist/theme/inverse-render.cjs.map +1 -0
  118. package/dist/theme/inverse-render.js +10 -0
  119. package/dist/theme/inverse-render.js.map +1 -0
  120. package/dist/theme/inverse-surface.cjs +2 -0
  121. package/dist/theme/inverse-surface.cjs.map +1 -0
  122. package/dist/theme/inverse-surface.js +258 -0
  123. package/dist/theme/inverse-surface.js.map +1 -0
  124. package/dist/theme/status-tokens.cjs +2 -0
  125. package/dist/theme/status-tokens.cjs.map +1 -0
  126. package/dist/theme/status-tokens.js +15 -0
  127. package/dist/theme/status-tokens.js.map +1 -0
  128. package/dist/vision/core/graph.cjs.map +1 -1
  129. package/dist/vision/core/graph.js.map +1 -1
  130. package/dist/vision/core/metadata.cjs +2 -2
  131. package/dist/vision/core/metadata.cjs.map +1 -1
  132. package/dist/vision/core/metadata.js +101 -29
  133. package/dist/vision/core/metadata.js.map +1 -1
  134. package/dist/vision/core/session.cjs +1 -1
  135. package/dist/vision/core/session.cjs.map +1 -1
  136. package/dist/vision/core/session.js +59 -48
  137. package/dist/vision/core/session.js.map +1 -1
  138. package/dist/vision/index.cjs +1 -1
  139. package/dist/vision/index.cjs.map +1 -1
  140. package/dist/vision/index.js +1 -1
  141. package/dist/vision/index.js.map +1 -1
  142. package/dist/vision.d.ts +25 -21
  143. package/dist/vite/create-vite-config.cjs +1 -1
  144. package/dist/vite/create-vite-config.cjs.map +1 -1
  145. package/dist/vite/create-vite-config.js +6 -4
  146. package/dist/vite/create-vite-config.js.map +1 -1
  147. package/package.json +2 -2
@@ -1,5 +1,5 @@
1
- const e=require("../utils/dev-mode.cjs"),t=require("./color.cjs"),n=require("./data-viz-ramps.cjs");var r={none:{xs:`0`,sm:`0`,md:`0`,lg:`0`,xl:`0`,"2xl":`0`},sm:{xs:`1px`,sm:`2px`,md:`4px`,lg:`6px`,xl:`8px`,"2xl":`12px`},md:{xs:`2px`,sm:`4px`,md:`8px`,lg:`12px`,xl:`16px`,"2xl":`24px`},lg:{xs:`4px`,sm:`6px`,md:`12px`,lg:`16px`,xl:`22px`,"2xl":`32px`},xl:{xs:`6px`,sm:`10px`,md:`16px`,lg:`24px`,xl:`32px`,"2xl":`44px`},full:{xs:`4px`,sm:`8px`,md:`9999px`,lg:`9999px`,xl:`9999px`,"2xl":`9999px`}},i=[50,100,200,300,400,500,600,700,800,900];function a(e,t,n){for(let r of i)e[`--tempest-${t}-${r}`]=n[r]}var o=4.5;function s(e,n,r){for(let i of r)if(t.contrastRatio(e[n],e[i])>=o)return i;return r[r.length-1]}var c=3,l={light:[`#ffffff`,`#f8f9fb`,`#f1f3f6`,`#e4e7ec`],dark:[`#0b0d12`,`#14171f`,`#1d2230`,`#262d3f`]},u=[500,600,700,800,900];function d(e,n){let r=r=>Math.min(...n.map(n=>t.contrastRatio(e[r],n))),i=u[0],a=-1;for(let e of u){let t=r(e);if(t>=c)return e;t>a&&(i=e,a=t)}return i}function f(e,t){if(!t)return l[e];let n=t[e];return e===`light`?[`#ffffff`,n[50],n[100],n[200]]:[n[50],n[100],n[200],n[300]]}function p(t){t===void 0||t>=1||e.isDevBuild()&&console.warn(`[tempest-react-sdk] createTheme({ focusRingAlpha: ${t} }) makes the focus ring translucent, so its contrast becomes whatever it composites over — measured below the 3:1 of WCAG 2.2 SC 1.4.11 on every surface the SDK paints. Drop the option to get the opaque ring, which is derived from your brand ramp and measured against those surfaces.`)}var m=`#1f0606`;function h(e,n,r,i,a){e[`--tempest-primary`]=`var(--tempest-primary-500)`,r===`light`?(e[`--tempest-primary-hover`]=`var(--tempest-primary-600)`,e[`--tempest-primary-active`]=`var(--tempest-primary-700)`,e[`--tempest-primary-soft`]=`var(--tempest-primary-50)`,e[`--tempest-primary-soft-hover`]=`var(--tempest-primary-100)`,e[`--tempest-primary-on-soft`]=`var(--tempest-primary-${s(n,50,[600,700,800,900])})`):(e[`--tempest-primary-hover`]=`var(--tempest-primary-400)`,e[`--tempest-primary-active`]=`var(--tempest-primary-300)`,e[`--tempest-primary-soft`]=`var(--tempest-primary-100)`,e[`--tempest-primary-soft-hover`]=`var(--tempest-primary-200)`,e[`--tempest-primary-on-soft`]=`var(--tempest-primary-${s(n,100,[700,800,900])})`);let o=t.readableForeground(n[500]);e[`--tempest-primary-foreground`]=o,e[`--tempest-text-on-primary`]=o;let c=n[d(n,i)];e[`--tempest-focus-ring-color`]=a===void 0||a>=1?c:t.hexToRgbaString(c,a),e[`--tempest-selected-indicator`]=c}function g(e,n,r){r===`light`?(e[`--tempest-bg`]=`#ffffff`,e[`--tempest-surface`]=`var(--tempest-gray-50)`,e[`--tempest-surface-2`]=`var(--tempest-gray-100)`,e[`--tempest-surface-3`]=`var(--tempest-gray-200)`,e[`--tempest-border`]=`var(--tempest-gray-200)`,e[`--tempest-border-strong`]=`var(--tempest-gray-300)`,e[`--tempest-text`]=`var(--tempest-gray-900)`,e[`--tempest-text-muted`]=`var(--tempest-gray-600)`,e[`--tempest-text-subtle`]=`var(--tempest-gray-500)`):(e[`--tempest-bg`]=n[50],e[`--tempest-surface`]=`var(--tempest-gray-100)`,e[`--tempest-surface-2`]=`var(--tempest-gray-200)`,e[`--tempest-surface-3`]=`var(--tempest-gray-300)`,e[`--tempest-border`]=`var(--tempest-gray-300)`,e[`--tempest-border-strong`]=`var(--tempest-gray-400)`,e[`--tempest-text`]=`var(--tempest-gray-900)`,e[`--tempest-text-muted`]=`var(--tempest-gray-700)`,e[`--tempest-text-subtle`]=`var(--tempest-gray-600)`),e[`--tempest-neutral-on-solid`]=t.readableForeground(n[700],`#ffffff`,m)}function _(e,n,r,i){i===`light`?(e[`--tempest-${n}`]=r[700],e[`--tempest-${n}-fg`]=r[800],e[`--tempest-${n}-bg`]=r[50],e[`--tempest-${n}-border`]=r[200],e[`--tempest-${n}-solid`]=r[600],e[`--tempest-${n}-on-solid`]=t.readableForeground(r[600],`#ffffff`,m)):(e[`--tempest-${n}`]=r[700],e[`--tempest-${n}-fg`]=r[700],e[`--tempest-${n}-bg`]=r[50],e[`--tempest-${n}-border`]=r[200],e[`--tempest-${n}-solid`]=r[500],e[`--tempest-${n}-on-solid`]=t.readableForeground(r[500],`#ffffff`,m))}function v(e,t){let n=Object.entries(t);return n.length===0?``:`${e} {\n${n.map(([e,t])=>` ${e}: ${t};`).join(`
2
- `)}\n}`}function y(e={}){let{primary:i,gray:o,chart:s,radius:c,focusRingAlpha:l,selector:u=`:root`,darkSelector:d=`[data-tempest-theme="dark"]`}=e;p(l);let m={},y={},b=o?{light:t.createColorScale(o,`light`,{anchor:!1,neutral:!0}),dark:t.createColorScale(o,`dark`,{anchor:!1,neutral:!0})}:void 0;if(i){let e=t.createColorScale(i,`light`),n=t.createColorScale(i,`dark`);a(m,`primary`,e),a(y,`primary`,n),h(m,e,`light`,f(`light`,b),l),h(y,n,`dark`,f(`dark`,b),l)}b&&(a(m,`gray`,b.light),a(y,`gray`,b.dark),g(m,b.light,`light`),g(y,b.dark,`dark`));for(let n of[`success`,`warning`,`danger`,`info`]){let r=e[n];r&&(_(m,n,t.createColorScale(r,`light`),`light`),_(y,n,t.createColorScale(r,`dark`),`dark`))}s?.length&&(s.forEach((e,t)=>{m[`--tempest-chart-${t+1}`]=e}),m[`--tempest-chart-count`]=String(s.length));let x=s?.[0]??i;if(x){let t=n.hueOf(x),r=e.danger?n.hueOf(e.danger):(t+180)%360;for(let[e,i]of[[m,`light`],[y,`dark`]]){n.buildRamp(t,i).forEach((t,n)=>{e[`--tempest-chart-sequential-${n+1}`]=t});let a=n.buildDivergingRamp({coolHue:t,warmHue:r,mid:i===`light`?`#e4e7ec`:`#262d3f`,mode:i});[...a.cool,a.mid,...a.warm].forEach((t,n)=>{e[`--tempest-chart-diverging-${n+1}`]=t})}}if(c){let e=typeof c==`string`?r[c]:c;for(let[t,n]of Object.entries(e))m[`--tempest-radius-${t}`]=n}return{light:m,dark:y,css:[v(u,m),v(d,y)].filter(Boolean).join(`
1
+ const e=require("../utils/dev-mode.cjs"),t=require("./color.cjs"),n=require("./data-viz-ramps.cjs"),r=require("./inverse-render.cjs"),i=require("./status-tokens.cjs");var a={none:{xs:`0`,sm:`0`,md:`0`,lg:`0`,xl:`0`,"2xl":`0`},sm:{xs:`1px`,sm:`2px`,md:`4px`,lg:`6px`,xl:`8px`,"2xl":`12px`},md:{xs:`2px`,sm:`4px`,md:`8px`,lg:`12px`,xl:`16px`,"2xl":`24px`},lg:{xs:`4px`,sm:`6px`,md:`12px`,lg:`16px`,xl:`22px`,"2xl":`32px`},xl:{xs:`6px`,sm:`10px`,md:`16px`,lg:`24px`,xl:`32px`,"2xl":`44px`},full:{xs:`4px`,sm:`8px`,md:`9999px`,lg:`9999px`,xl:`9999px`,"2xl":`9999px`}},o=[50,100,200,300,400,500,600,700,800,900];function s(e,t,n){for(let r of o)e[`--tempest-${t}-${r}`]=n[r]}var c=4.5;function l(e,n,r){for(let i of r)if(t.contrastRatio(e[n],e[i])>=c)return i;return r[r.length-1]}var u=3,d={light:[`#ffffff`,`#f8f9fb`,`#f1f3f6`,`#e4e7ec`],dark:[`#0b0d12`,`#14171f`,`#1d2230`,`#262d3f`]},f=[500,600,700,800,900];function p(e,n){let r=r=>Math.min(...n.map(n=>t.contrastRatio(e[r],n))),i=f[0],a=-1;for(let e of f){let t=r(e);if(t>=u)return e;t>a&&(i=e,a=t)}return i}function m(e,t){if(!t)return d[e];let n=t[e];return e===`light`?[`#ffffff`,n[50],n[100],n[200]]:[n[50],n[100],n[200],n[300]]}function h(t){t===void 0||t>=1||e.isDevBuild()&&console.warn(`[tempest-react-sdk] createTheme({ focusRingAlpha: ${t} }) makes the focus ring translucent, so its contrast becomes whatever it composites over — measured below the 3:1 of WCAG 2.2 SC 1.4.11 on every surface the SDK paints. Drop the option to get the opaque ring, which is derived from your brand ramp and measured against those surfaces.`)}function g(e,n,r,i,a){e[`--tempest-primary`]=`var(--tempest-primary-500)`,r===`light`?(e[`--tempest-primary-hover`]=`var(--tempest-primary-600)`,e[`--tempest-primary-active`]=`var(--tempest-primary-700)`,e[`--tempest-primary-soft`]=`var(--tempest-primary-50)`,e[`--tempest-primary-soft-hover`]=`var(--tempest-primary-100)`,e[`--tempest-primary-on-soft`]=`var(--tempest-primary-${l(n,50,[600,700,800,900])})`):(e[`--tempest-primary-hover`]=`var(--tempest-primary-400)`,e[`--tempest-primary-active`]=`var(--tempest-primary-300)`,e[`--tempest-primary-soft`]=`var(--tempest-primary-100)`,e[`--tempest-primary-soft-hover`]=`var(--tempest-primary-200)`,e[`--tempest-primary-on-soft`]=`var(--tempest-primary-${l(n,100,[700,800,900])})`);let o=t.readableForeground(n[500]);e[`--tempest-primary-foreground`]=o,e[`--tempest-text-on-primary`]=o;let s=n[p(n,i)];e[`--tempest-focus-ring-color`]=a===void 0||a>=1?s:t.hexToRgbaString(s,a),e[`--tempest-selected-indicator`]=s}function _(e,n,r){r===`light`?(e[`--tempest-bg`]=`#ffffff`,e[`--tempest-surface`]=`var(--tempest-gray-50)`,e[`--tempest-surface-2`]=`var(--tempest-gray-100)`,e[`--tempest-surface-3`]=`var(--tempest-gray-200)`,e[`--tempest-border`]=`var(--tempest-gray-200)`,e[`--tempest-border-strong`]=`var(--tempest-gray-300)`,e[`--tempest-text`]=`var(--tempest-gray-900)`,e[`--tempest-text-muted`]=`var(--tempest-gray-600)`,e[`--tempest-text-subtle`]=`var(--tempest-gray-500)`):(e[`--tempest-bg`]=n[50],e[`--tempest-surface`]=`var(--tempest-gray-100)`,e[`--tempest-surface-2`]=`var(--tempest-gray-200)`,e[`--tempest-surface-3`]=`var(--tempest-gray-300)`,e[`--tempest-border`]=`var(--tempest-gray-300)`,e[`--tempest-border-strong`]=`var(--tempest-gray-400)`,e[`--tempest-text`]=`var(--tempest-gray-900)`,e[`--tempest-text-muted`]=`var(--tempest-gray-700)`,e[`--tempest-text-subtle`]=`var(--tempest-gray-600)`),e[`--tempest-neutral-on-solid`]=t.readableForeground(n[700],`#ffffff`,i.ON_SOLID_INK)}function v(e,t){let n=Object.entries(t);return n.length===0?``:`${e} {\n${n.map(([e,t])=>` ${e}: ${t};`).join(`
2
+ `)}\n}`}function y(e={}){let{primary:o,gray:c,chart:l,radius:u,focusRingAlpha:d,selector:f=`:root`,darkSelector:p=`[data-tempest-theme="dark"]`,inverse:y,inverseSelector:b=r.INVERSE_SELECTOR}=e;h(d);let x={},S={},C=c?{light:t.createColorScale(c,`light`,{anchor:!1,neutral:!0}),dark:t.createColorScale(c,`dark`,{anchor:!1,neutral:!0})}:void 0;if(o){let e=t.createColorScale(o,`light`),n=t.createColorScale(o,`dark`);s(x,`primary`,e),s(S,`primary`,n),g(x,e,`light`,m(`light`,C),d),g(S,n,`dark`,m(`dark`,C),d)}C&&(s(x,`gray`,C.light),s(S,`gray`,C.dark),_(x,C.light,`light`),_(S,C.dark,`dark`));for(let n of i.THEME_STATUSES){let r=e[n];r&&(i.writeStatus(x,n,t.createColorScale(r,`light`),`light`),i.writeStatus(S,n,t.createColorScale(r,`dark`),`dark`))}l?.length&&(l.forEach((e,t)=>{x[`--tempest-chart-${t+1}`]=e}),x[`--tempest-chart-count`]=String(l.length));let w=l?.[0]??o;if(w){let t=n.hueOf(w),r=e.danger?n.hueOf(e.danger):(t+180)%360;for(let[e,i]of[[x,`light`],[S,`dark`]]){n.buildRamp(t,i).forEach((t,n)=>{e[`--tempest-chart-sequential-${n+1}`]=t});let a=n.buildDivergingRamp({coolHue:t,warmHue:r,mid:i===`light`?`#e4e7ec`:`#262d3f`,mode:i});[...a.cool,a.mid,...a.warm].forEach((t,n)=>{e[`--tempest-chart-diverging-${n+1}`]=t})}}if(u){let e=typeof u==`string`?a[u]:u;for(let[t,n]of Object.entries(e))x[`--tempest-radius-${t}`]=n}let T=o&&y?y({primary:o,statuses:{success:e.success,warning:e.warning,danger:e.danger,info:e.info},chart:l}):null,E=[v(f,x),v(p,S),T?r.renderInverseSurface(b,T):``].filter(Boolean).join(`
3
3
 
4
- `)}}function b(e){if(!e.primary)return null;let n=t.createColorScale(e.primary,`light`);return t.contrastRatio(n[500],t.readableForeground(n[500]))}exports.createTheme=y,exports.themeContrast=b;
4
+ `);return T?{light:x,dark:S,inverse:T.tokens,css:E}:{light:x,dark:S,css:E}}function b(e){if(!e.primary)return null;let n=t.createColorScale(e.primary,`light`);return t.contrastRatio(n[500],t.readableForeground(n[500]))}exports.createTheme=y,exports.themeContrast=b;
5
5
  //# sourceMappingURL=create-theme.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"create-theme.cjs","names":[],"sources":["../../src/theme/create-theme.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — one pass that emits the whole token set — surfaces,\n * text, borders, states, the chart ramps — for both modes, with every value checked\n * against the surface it will sit on. The checks are what make the length: each\n * token is derived and then verified against the same background table.\n */\n/**\n * Theme factory: turns a handful of brand colors into the full set of\n * `--tempest-*` token overrides, for both light and dark.\n *\n * Rebranding used to mean hand-writing ~167 custom properties (and getting the\n * dark ramp inversion right by hand). `createTheme({ primary: \"#7c3aed\" })`\n * emits the same thing, derived in OKLCH so the ramp stays perceptually even.\n */\nimport {\n contrastRatio,\n createColorScale,\n hexToRgbaString,\n readableForeground,\n type ColorScale,\n type ScaleStep,\n} from \"./color\";\nimport { buildDivergingRamp, buildRamp, hueOf } from \"./data-viz-ramps\";\nimport { isDevBuild } from \"../utils/dev-mode\";\n\n/** Radius presets, applied to the whole `--tempest-radius-*` family at once. */\nexport type ThemeRadius = \"none\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"full\";\n\n/** Status token families that {@link createTheme} can regenerate. */\nexport type ThemeStatus = \"success\" | \"warning\" | \"danger\" | \"info\";\n\n/** Input for {@link createTheme}. Every field is optional — omitted families keep the built-in tokens. */\nexport interface CreateThemeOptions {\n /** Brand color, used as the `500` step of the primary scale. */\n primary?: string;\n /** Neutral color, used as the `500` step of the gray scale (surfaces, borders, text). */\n gray?: string;\n /** Success color (`--tempest-success*`). */\n success?: string;\n /** Warning color (`--tempest-warning*`). */\n warning?: string;\n /** Danger color (`--tempest-danger*`). */\n danger?: string;\n /** Info color (`--tempest-info*`). */\n info?: string;\n /**\n * Categorical series colors for `tempest-react-sdk/charts`, in cycle order.\n * Written to `--tempest-chart-1` … `--tempest-chart-N`, so charts follow the\n * theme instead of a hardcoded palette.\n */\n chart?: string[];\n /** Corner radius scale. A preset name, or explicit per-step values. */\n radius?: ThemeRadius | Partial<Record<\"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\", string>>;\n /**\n * Opacity of `--tempest-focus-ring-color`. Omit it — the default ring is opaque.\n *\n * A translucent ring has no contrast of its own; it has the contrast of\n * whatever it composites over. Passing a value below `1` reintroduces the\n * failure 0.61.0 fixed in the built-in tokens, and logs a warning in a\n * development build. Kept because a translucent ring over a background you\n * control is a legitimate choice.\n */\n focusRingAlpha?: number;\n /** Selector the light tokens are written under. Default `\":root\"`. */\n selector?: string;\n /** Selector the dark tokens are written under. Default `'[data-tempest-theme=\"dark\"]'`. */\n darkSelector?: string;\n}\n\n/** A generated theme: token maps per color scheme, plus the CSS text that carries them. */\nexport interface GeneratedTheme {\n /** Light-scheme custom properties, without the leading `--`-less names (keys include `--`). */\n light: Record<string, string>;\n /** Dark-scheme custom properties. */\n dark: Record<string, string>;\n /** Both blocks rendered as CSS, ready for {@link applyTheme} or a stylesheet. */\n css: string;\n}\n\nconst RADIUS_PRESETS: Record<ThemeRadius, Record<string, string>> = {\n none: { xs: \"0\", sm: \"0\", md: \"0\", lg: \"0\", xl: \"0\", \"2xl\": \"0\" },\n sm: { xs: \"1px\", sm: \"2px\", md: \"4px\", lg: \"6px\", xl: \"8px\", \"2xl\": \"12px\" },\n md: { xs: \"2px\", sm: \"4px\", md: \"8px\", lg: \"12px\", xl: \"16px\", \"2xl\": \"24px\" },\n lg: { xs: \"4px\", sm: \"6px\", md: \"12px\", lg: \"16px\", xl: \"22px\", \"2xl\": \"32px\" },\n xl: { xs: \"6px\", sm: \"10px\", md: \"16px\", lg: \"24px\", xl: \"32px\", \"2xl\": \"44px\" },\n full: { xs: \"4px\", sm: \"8px\", md: \"9999px\", lg: \"9999px\", xl: \"9999px\", \"2xl\": \"9999px\" },\n};\n\nconst SCALE_STEPS: ScaleStep[] = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900];\n\nfunction writeScale(tokens: Record<string, string>, name: string, scale: ColorScale): void {\n for (const step of SCALE_STEPS) {\n tokens[`--tempest-${name}-${step}`] = scale[step];\n }\n}\n\n/** Minimum contrast for body text, WCAG 2.x AA. */\nconst AA_TEXT_CONTRAST = 4.5;\n\n/**\n * Choose the ramp step for text sitting on the soft tint.\n *\n * A fixed step does not survive an arbitrary brand color: the built-in blue only\n * reaches 4.37:1 at `500` over its own `50` tint, and a generated emerald lands\n * at 4.41:1 at `600` — both fail AA for body text by a hair. So the step is\n * picked by measuring, walking away from the tint until it clears AA, and\n * falling back to the most extreme candidate when even that cannot (a very light\n * or very desaturated brand).\n *\n * @param scale - The generated ramp.\n * @param softStep - Step used as `--tempest-primary-soft`.\n * @param candidates - Steps to try, in order of preference.\n * @returns The first candidate clearing {@link AA_TEXT_CONTRAST}, else the last.\n */\nfunction pickOnSoftStep(\n scale: ColorScale,\n softStep: ScaleStep,\n candidates: ScaleStep[],\n): ScaleStep {\n for (const step of candidates) {\n if (contrastRatio(scale[softStep], scale[step]) >= AA_TEXT_CONTRAST) return step;\n }\n return candidates[candidates.length - 1];\n}\n\n/** Minimum contrast for a non-text indicator, WCAG 2.2 SC 1.4.11. */\nconst INDICATOR_CONTRAST = 3;\n\n/**\n * The four surfaces a focus ring can land on, as the SDK's own `colors.css`\n * paints them.\n *\n * Ported from `src/styles/colors.css` (`--tempest-bg`, `--tempest-surface`,\n * `--tempest-surface-2`, `--tempest-surface-3`, resolved through the gray ramp),\n * and pinned by a test that reads that file — a theme that only names `primary`\n * keeps these, so the ring has to be measured against them and not against a\n * guess. The light values resolve through `--tempest-gray-50/100/200`; the dark\n * ones are literals in the dark block.\n */\nconst SDK_SURFACES: Record<\"light\" | \"dark\", readonly string[]> = {\n light: [\"#ffffff\", \"#f8f9fb\", \"#f1f3f6\", \"#e4e7ec\"],\n dark: [\"#0b0d12\", \"#14171f\", \"#1d2230\", \"#262d3f\"],\n};\n\n/**\n * Ramp steps to try for the focus ring, in order.\n *\n * `500` first, because the ring should read as the brand when it can, then up\n * the ramp — which moves away from the surfaces in **both** schemes, because\n * `createColorScale` inverts the dark ramp: there `50` is the darkest step (it\n * is what `--tempest-bg` is made of) and `900` the lightest. Walking down in\n * dark mode looks like the right inversion and is the wrong direction; measured,\n * it left a near-black brand's ring at 1.04:1 over its own background.\n */\nconst FOCUS_RING_CANDIDATES: readonly ScaleStep[] = [500, 600, 700, 800, 900];\n\n/**\n * Choose the ramp step for the focus ring, by measuring it against the surfaces\n * it will actually be drawn on.\n *\n * A fixed step does not survive an arbitrary brand, and the measurement says so\n * loudly: of twelve brands checked against the four surfaces in both schemes —\n * 96 pairings — `500` opaque clears 3:1 in 58 of them, and `500` at the old\n * default alpha of 0.35 in 3. Picking the step by measurement clears all 96. The\n * brand from the report that opened this\n * (`#8100D7`) passes in light at 7.20:1 and fails in **dark** at 2.70:1 down to\n * 1.91:1 on `surface-3`; a yellow fails in light at 1.43:1. So the step is\n * picked the way {@link pickOnSoftStep} picks text on a tint: walk away from the\n * surfaces until the ring clears the floor against *all* of them.\n *\n * The fallback is the candidate with the best worst case rather than the last\n * one, because \"the most extreme step\" is not always the most contrasting one\n * once a brand is near-neutral.\n *\n * @param scale - The generated ramp.\n * @param surfaces - The four surfaces this theme will paint, opaque hex.\n * @returns The first candidate clearing {@link INDICATOR_CONTRAST} on every\n * surface, else the candidate whose weakest pairing is strongest.\n */\nfunction pickFocusRingStep(scale: ColorScale, surfaces: readonly string[]): ScaleStep {\n const worstCase = (step: ScaleStep): number =>\n Math.min(...surfaces.map((surface) => contrastRatio(scale[step], surface)));\n let best: ScaleStep = FOCUS_RING_CANDIDATES[0] as ScaleStep;\n let bestRatio = -1;\n for (const step of FOCUS_RING_CANDIDATES) {\n const measured = worstCase(step);\n if (measured >= INDICATOR_CONTRAST) return step;\n if (measured > bestRatio) {\n best = step;\n bestRatio = measured;\n }\n }\n return best;\n}\n\n/**\n * The surfaces this theme will actually paint, per scheme.\n *\n * A theme that names `gray` repaints all four, so measuring the ring against the\n * SDK's would be measuring the wrong backdrop — and a theme that does not keeps\n * the SDK's, which is the common case. The order and the ramp steps mirror\n * {@link writeNeutralAliases}, which is what emits them.\n *\n * @param scheme - Which scheme is being emitted.\n * @param grayScales - The generated neutral ramps, when the theme names one.\n * @returns Four opaque hex colors: bg, surface, surface-2, surface-3.\n */\nfunction surfacesOf(\n scheme: \"light\" | \"dark\",\n grayScales: { light: ColorScale; dark: ColorScale } | undefined,\n): readonly string[] {\n if (!grayScales) return SDK_SURFACES[scheme];\n const scale = grayScales[scheme];\n return scheme === \"light\"\n ? [\"#ffffff\", scale[50], scale[100], scale[200]]\n : [scale[50], scale[100], scale[200], scale[300]];\n}\n\n/**\n * Warn, in a development build, that an alpha was asked for the focus ring.\n *\n * A translucent ring has no contrast of its own — it has the contrast of\n * whatever it composites over — and that is the failure this option used to\n * ship by default: measured at alpha `0.35`, twelve brands across four surfaces\n * and both schemes produced 96 pairings, of which 3 cleared the 3:1 of WCAG 2.2\n * SC 1.4.11. It stays available, because a translucent ring over a\n * known background is a legitimate choice, but it is a choice a theme now has to\n * make on purpose.\n *\n * The warning is the difficult part to notice otherwise: a failing focus ring is\n * visible, it just does not separate.\n *\n * @param focusRingAlpha - The opacity the caller asked for, if any.\n */\nfunction warnOnTranslucentFocusRing(focusRingAlpha: number | undefined): void {\n if (focusRingAlpha === undefined || focusRingAlpha >= 1) return;\n if (!isDevBuild()) return;\n console.warn(\n `[tempest-react-sdk] createTheme({ focusRingAlpha: ${focusRingAlpha} }) makes the focus ring ` +\n \"translucent, so its contrast becomes whatever it composites over — measured below the 3:1 \" +\n \"of WCAG 2.2 SC 1.4.11 on every surface the SDK paints. Drop the option to get the opaque \" +\n \"ring, which is derived from your brand ramp and measured against those surfaces.\",\n );\n}\n\n/**\n * The dark ink for content sitting on a saturated fill.\n *\n * Near-black tinted toward warm rather than pure black, matching the value\n * `colors.css` uses for the same job: it reads as part of the swatch instead of\n * a hole punched in it.\n */\nconst ON_SOLID_INK = \"#1f0606\";\n\n/**\n * Emit the primary aliases for one scheme.\n *\n * The dark scheme walks the ramp the other way (hover is *lighter*, the soft\n * tint is a dark shade, and readable text on that tint is a light shade) — the\n * same inversion the built-in `colors.css` dark block does by hand.\n *\n * The selected-state indicator is derived from the same measurement as the focus\n * ring and emitted as its own token. It never takes `focusRingAlpha`: a\n * translucent ring over a known background is a legitimate choice a theme can\n * make, while a translucent *state* indicator has the contrast of whatever it\n * composites over, which is the defect this token exists to end.\n *\n * @param tokens - The token map for this scheme, written in place.\n * @param scale - The generated primary ramp.\n * @param scheme - Which scheme is being emitted.\n * @param surfaces - The surfaces this theme paints, for the focus-ring measurement.\n * @param focusRingAlpha - Opacity asked for the ring, or `undefined` for opaque.\n */\nfunction writePrimaryAliases(\n tokens: Record<string, string>,\n scale: ColorScale,\n scheme: \"light\" | \"dark\",\n surfaces: readonly string[],\n focusRingAlpha: number | undefined,\n): void {\n tokens[\"--tempest-primary\"] = \"var(--tempest-primary-500)\";\n if (scheme === \"light\") {\n tokens[\"--tempest-primary-hover\"] = \"var(--tempest-primary-600)\";\n tokens[\"--tempest-primary-active\"] = \"var(--tempest-primary-700)\";\n tokens[\"--tempest-primary-soft\"] = \"var(--tempest-primary-50)\";\n tokens[\"--tempest-primary-soft-hover\"] = \"var(--tempest-primary-100)\";\n tokens[\"--tempest-primary-on-soft\"] =\n `var(--tempest-primary-${pickOnSoftStep(scale, 50, [600, 700, 800, 900])})`;\n } else {\n tokens[\"--tempest-primary-hover\"] = \"var(--tempest-primary-400)\";\n tokens[\"--tempest-primary-active\"] = \"var(--tempest-primary-300)\";\n tokens[\"--tempest-primary-soft\"] = \"var(--tempest-primary-100)\";\n tokens[\"--tempest-primary-soft-hover\"] = \"var(--tempest-primary-200)\";\n tokens[\"--tempest-primary-on-soft\"] =\n `var(--tempest-primary-${pickOnSoftStep(scale, 100, [700, 800, 900])})`;\n }\n\n const foreground = readableForeground(scale[500]);\n tokens[\"--tempest-primary-foreground\"] = foreground;\n tokens[\"--tempest-text-on-primary\"] = foreground;\n const ring = scale[pickFocusRingStep(scale, surfaces)];\n tokens[\"--tempest-focus-ring-color\"] =\n focusRingAlpha === undefined || focusRingAlpha >= 1\n ? ring\n : hexToRgbaString(ring, focusRingAlpha);\n tokens[\"--tempest-selected-indicator\"] = ring;\n}\n\n/**\n * Emit the neutral surface/border/text aliases from a generated gray scale.\n *\n * `--tempest-bg` is pushed past the ramp on purpose: pure white in light mode and\n * a shade darker than `gray-50` in dark mode, so a raised surface still reads as\n * raised against the page.\n */\nfunction writeNeutralAliases(\n tokens: Record<string, string>,\n scale: ColorScale,\n scheme: \"light\" | \"dark\",\n): void {\n if (scheme === \"light\") {\n tokens[\"--tempest-bg\"] = \"#ffffff\";\n tokens[\"--tempest-surface\"] = \"var(--tempest-gray-50)\";\n tokens[\"--tempest-surface-2\"] = \"var(--tempest-gray-100)\";\n tokens[\"--tempest-surface-3\"] = \"var(--tempest-gray-200)\";\n tokens[\"--tempest-border\"] = \"var(--tempest-gray-200)\";\n tokens[\"--tempest-border-strong\"] = \"var(--tempest-gray-300)\";\n tokens[\"--tempest-text\"] = \"var(--tempest-gray-900)\";\n tokens[\"--tempest-text-muted\"] = \"var(--tempest-gray-600)\";\n tokens[\"--tempest-text-subtle\"] = \"var(--tempest-gray-500)\";\n } else {\n tokens[\"--tempest-bg\"] = scale[50];\n tokens[\"--tempest-surface\"] = \"var(--tempest-gray-100)\";\n tokens[\"--tempest-surface-2\"] = \"var(--tempest-gray-200)\";\n tokens[\"--tempest-surface-3\"] = \"var(--tempest-gray-300)\";\n tokens[\"--tempest-border\"] = \"var(--tempest-gray-300)\";\n tokens[\"--tempest-border-strong\"] = \"var(--tempest-gray-400)\";\n tokens[\"--tempest-text\"] = \"var(--tempest-gray-900)\";\n tokens[\"--tempest-text-muted\"] = \"var(--tempest-gray-700)\";\n tokens[\"--tempest-text-subtle\"] = \"var(--tempest-gray-600)\";\n }\n tokens[\"--tempest-neutral-on-solid\"] = readableForeground(scale[700], \"#ffffff\", ON_SOLID_INK);\n}\n\n/**\n * Emit one status family (`--tempest-danger`, `-fg`, `-bg`, `-border`, `-solid`).\n *\n * `-fg` is the text shade over `-bg`, so it has to cross the ramp in opposite\n * directions per scheme; `-solid` stays the saturated fill used by badges.\n *\n * `-on-solid` is derived from the `-solid` this call just emitted, rather than\n * left to fall through. Falling through was the bug: the SDK's own\n * `-on-solid` values are measured against the SDK's own fills, so a brand whose\n * `danger-600` lands light kept white ink over it and nothing said so. The\n * generated neutral hit exactly that — white over a generated `gray-700` of\n * `#a8b2c6` measures 2.13:1.\n */\nfunction writeStatus(\n tokens: Record<string, string>,\n name: ThemeStatus,\n scale: ColorScale,\n scheme: \"light\" | \"dark\",\n): void {\n if (scheme === \"light\") {\n tokens[`--tempest-${name}`] = scale[700];\n tokens[`--tempest-${name}-fg`] = scale[800];\n tokens[`--tempest-${name}-bg`] = scale[50];\n tokens[`--tempest-${name}-border`] = scale[200];\n tokens[`--tempest-${name}-solid`] = scale[600];\n tokens[`--tempest-${name}-on-solid`] = readableForeground(\n scale[600],\n \"#ffffff\",\n ON_SOLID_INK,\n );\n } else {\n tokens[`--tempest-${name}`] = scale[700];\n tokens[`--tempest-${name}-fg`] = scale[700];\n tokens[`--tempest-${name}-bg`] = scale[50];\n tokens[`--tempest-${name}-border`] = scale[200];\n tokens[`--tempest-${name}-solid`] = scale[500];\n tokens[`--tempest-${name}-on-solid`] = readableForeground(\n scale[500],\n \"#ffffff\",\n ON_SOLID_INK,\n );\n }\n}\n\nfunction renderBlock(selector: string, tokens: Record<string, string>): string {\n const entries = Object.entries(tokens);\n if (entries.length === 0) return \"\";\n const body = entries.map(([name, value]) => ` ${name}: ${value};`).join(\"\\n\");\n return `${selector} {\\n${body}\\n}`;\n}\n\n/**\n * Generate `--tempest-*` overrides from a small brand description.\n *\n * Only the families you pass are generated; everything else falls through to the\n * SDK's own tokens, so a theme stays a patch and not a fork of `colors.css`.\n *\n * @example\n * ```ts\n * import { applyTheme, createTheme } from \"tempest-react-sdk\";\n *\n * const theme = createTheme({\n * primary: \"#7c3aed\",\n * radius: \"lg\",\n * chart: [\"#7c3aed\", \"#0ea5e9\", \"#22c55e\", \"#f59e0b\"],\n * });\n *\n * applyTheme(theme);\n * ```\n *\n * @param options - Brand colors plus optional radius / chart / focus-ring tuning.\n * @returns The light and dark token maps and the CSS text that carries them.\n */\nexport function createTheme(options: CreateThemeOptions = {}): GeneratedTheme {\n const {\n primary,\n gray,\n chart,\n radius,\n focusRingAlpha,\n selector = \":root\",\n darkSelector = '[data-tempest-theme=\"dark\"]',\n } = options;\n\n warnOnTranslucentFocusRing(focusRingAlpha);\n\n const light: Record<string, string> = {};\n const dark: Record<string, string> = {};\n\n // `anchor: false`: a neutral ramp has to keep its tuned lightness curve.\n // Anchoring it at the input compressed both halves and dropped\n // text-muted-on-surface-3 to ~4.2:1 — below AA — which the browser axe\n // sweep caught on the generated themes.\n const grayScales = gray\n ? {\n light: createColorScale(gray, \"light\", { anchor: false, neutral: true }),\n dark: createColorScale(gray, \"dark\", { anchor: false, neutral: true }),\n }\n : undefined;\n\n if (primary) {\n const lightScale = createColorScale(primary, \"light\");\n const darkScale = createColorScale(primary, \"dark\");\n writeScale(light, \"primary\", lightScale);\n writeScale(dark, \"primary\", darkScale);\n writePrimaryAliases(\n light,\n lightScale,\n \"light\",\n surfacesOf(\"light\", grayScales),\n focusRingAlpha,\n );\n writePrimaryAliases(\n dark,\n darkScale,\n \"dark\",\n surfacesOf(\"dark\", grayScales),\n focusRingAlpha,\n );\n }\n\n if (grayScales) {\n writeScale(light, \"gray\", grayScales.light);\n writeScale(dark, \"gray\", grayScales.dark);\n writeNeutralAliases(light, grayScales.light, \"light\");\n writeNeutralAliases(dark, grayScales.dark, \"dark\");\n }\n\n for (const status of [\"success\", \"warning\", \"danger\", \"info\"] as const) {\n const value = options[status];\n if (!value) continue;\n writeStatus(light, status, createColorScale(value, \"light\"), \"light\");\n writeStatus(dark, status, createColorScale(value, \"dark\"), \"dark\");\n }\n\n if (chart?.length) {\n chart.forEach((color, index) => {\n light[`--tempest-chart-${index + 1}`] = color;\n });\n // Declares how many series colors this theme owns. Without it the reader\n // keeps walking into the SDK's built-in `--tempest-chart-7`/`-8`, so a\n // 6-color brand palette silently mixes with two leftover defaults — which\n // is exactly what a 7-series chart would show.\n light[\"--tempest-chart-count\"] = String(chart.length);\n }\n\n /*\n * Continuous scales follow the brand too.\n *\n * Without this a rebranded app gets its categorical slots recoloured and then a\n * heatmap still painted in the SDK's blue — the one chart on the page that\n * silently ignores the brand. Both scales derive from the first series hue (or\n * the primary), stepped by perceptual lightness for each mode separately.\n *\n * Skipped entirely when the theme names no colour to derive from, so the\n * built-in tokens survive instead of being overwritten with a guess.\n */\n const scaleSource = chart?.[0] ?? primary;\n if (scaleSource) {\n const coolHue = hueOf(scaleSource);\n // The opposite pole: the brand's own danger colour when it has one, since a\n // diverging scale's warm arm and \"bad\" should not disagree on screen.\n const warmHue = options.danger ? hueOf(options.danger) : (coolHue + 180) % 360;\n for (const [target, mode] of [\n [light, \"light\"],\n [dark, \"dark\"],\n ] as const) {\n buildRamp(coolHue, mode).forEach((stepColor, index) => {\n target[`--tempest-chart-sequential-${index + 1}`] = stepColor;\n });\n const diverging = buildDivergingRamp({\n coolHue,\n warmHue,\n mid: mode === \"light\" ? \"#e4e7ec\" : \"#262d3f\",\n mode,\n });\n [...diverging.cool, diverging.mid, ...diverging.warm].forEach((stepColor, index) => {\n target[`--tempest-chart-diverging-${index + 1}`] = stepColor;\n });\n }\n }\n\n if (radius) {\n const steps = typeof radius === \"string\" ? RADIUS_PRESETS[radius] : radius;\n for (const [step, value] of Object.entries(steps)) {\n light[`--tempest-radius-${step}`] = value;\n }\n }\n\n const css = [renderBlock(selector, light), renderBlock(darkSelector, dark)]\n .filter(Boolean)\n .join(\"\\n\\n\");\n\n return { light, dark, css };\n}\n\n/**\n * Contrast ratio of `--tempest-primary-foreground` over the brand color.\n *\n * Exposed so an app (or a test) can assert its own brand clears WCAG AA (4.5) for\n * body text or AA-large (3.0) for button labels, instead of trusting the pick.\n *\n * @param options - The same input given to {@link createTheme}.\n * @returns The ratio, or `null` when no `primary` was provided.\n */\nexport function themeContrast(options: CreateThemeOptions): number | null {\n if (!options.primary) return null;\n const scale = createColorScale(options.primary, \"light\");\n return contrastRatio(scale[500], readableForeground(scale[500]));\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"create-theme.cjs","names":[],"sources":["../../src/theme/create-theme.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — one pass that emits the whole token set — surfaces,\n * text, borders, states, the chart ramps — for both modes, with every value checked\n * against the surface it will sit on. The checks are what make the length: each\n * token is derived and then verified against the same background table.\n */\n/**\n * Theme factory: turns a handful of brand colors into the full set of\n * `--tempest-*` token overrides, for both light and dark.\n *\n * Rebranding used to mean hand-writing ~167 custom properties (and getting the\n * dark ramp inversion right by hand). `createTheme({ primary: \"#7c3aed\" })`\n * emits the same thing, derived in OKLCH so the ramp stays perceptually even.\n */\nimport {\n contrastRatio,\n createColorScale,\n hexToRgbaString,\n readableForeground,\n type ColorScale,\n type ScaleStep,\n} from \"./color\";\nimport { buildDivergingRamp, buildRamp, hueOf } from \"./data-viz-ramps\";\nimport { isDevBuild } from \"../utils/dev-mode\";\nimport {\n INVERSE_SELECTOR,\n renderInverseSurface,\n type InverseSurfaceGenerator,\n} from \"./inverse-render\";\nimport { ON_SOLID_INK, THEME_STATUSES, writeStatus } from \"./status-tokens\";\n\n/** Radius presets, applied to the whole `--tempest-radius-*` family at once. */\nexport type ThemeRadius = \"none\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"full\";\n\nexport type { ThemeStatus } from \"./status-tokens\";\n\n/** Input for {@link createTheme}. Every field is optional — omitted families keep the built-in tokens. */\nexport interface CreateThemeOptions {\n /** Brand color, used as the `500` step of the primary scale. */\n primary?: string;\n /** Neutral color, used as the `500` step of the gray scale (surfaces, borders, text). */\n gray?: string;\n /** Success color (`--tempest-success*`). */\n success?: string;\n /** Warning color (`--tempest-warning*`). */\n warning?: string;\n /** Danger color (`--tempest-danger*`). */\n danger?: string;\n /** Info color (`--tempest-info*`). */\n info?: string;\n /**\n * Categorical series colors for `tempest-react-sdk/charts`, in cycle order.\n * Written to `--tempest-chart-1` … `--tempest-chart-N`, so charts follow the\n * theme instead of a hardcoded palette.\n */\n chart?: string[];\n /** Corner radius scale. A preset name, or explicit per-step values. */\n radius?: ThemeRadius | Partial<Record<\"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\", string>>;\n /**\n * Opacity of `--tempest-focus-ring-color`. Omit it — the default ring is opaque.\n *\n * A translucent ring has no contrast of its own; it has the contrast of\n * whatever it composites over. Passing a value below `1` reintroduces the\n * failure 0.61.0 fixed in the built-in tokens, and logs a warning in a\n * development build. Kept because a translucent ring over a background you\n * control is a legitimate choice.\n */\n focusRingAlpha?: number;\n /** Selector the light tokens are written under. Default `\":root\"`. */\n selector?: string;\n /** Selector the dark tokens are written under. Default `'[data-tempest-theme=\"dark\"]'`. */\n darkSelector?: string;\n /**\n * Also emit the inverse surface — the token set for a section painted in the\n * brand color — by passing its generator, `createInverseSurface`. Off by\n * default; ignored without `primary`.\n *\n * The generator is passed in rather than switched on with `true` so that\n * `createTheme` never imports it: an app that does not paint a section in\n * its brand does not ship the 1.68 kB brotli it costs.\n *\n * @example\n * ```ts\n * import { createInverseSurface, createTheme } from \"tempest-react-sdk\";\n *\n * createTheme({ primary: \"#03184b\", inverse: createInverseSurface });\n * ```\n */\n inverse?: InverseSurfaceGenerator;\n /** Selector the inverse tokens are written under. Default `'[data-tempest-tone=\"inverse\"]'`. */\n inverseSelector?: string;\n}\n\n/** A generated theme: token maps per color scheme, plus the CSS text that carries them. */\nexport interface GeneratedTheme {\n /** Light-scheme custom properties, without the leading `--`-less names (keys include `--`). */\n light: Record<string, string>;\n /** Dark-scheme custom properties. */\n dark: Record<string, string>;\n /**\n * Inverse-surface custom properties — literal colors, identical under the\n * light and the dark page scheme. Present only when the theme names a\n * `primary` and passes `inverse`.\n */\n inverse?: Record<string, string>;\n /** Both blocks rendered as CSS, ready for {@link applyTheme} or a stylesheet. */\n css: string;\n}\n\nconst RADIUS_PRESETS: Record<ThemeRadius, Record<string, string>> = {\n none: { xs: \"0\", sm: \"0\", md: \"0\", lg: \"0\", xl: \"0\", \"2xl\": \"0\" },\n sm: { xs: \"1px\", sm: \"2px\", md: \"4px\", lg: \"6px\", xl: \"8px\", \"2xl\": \"12px\" },\n md: { xs: \"2px\", sm: \"4px\", md: \"8px\", lg: \"12px\", xl: \"16px\", \"2xl\": \"24px\" },\n lg: { xs: \"4px\", sm: \"6px\", md: \"12px\", lg: \"16px\", xl: \"22px\", \"2xl\": \"32px\" },\n xl: { xs: \"6px\", sm: \"10px\", md: \"16px\", lg: \"24px\", xl: \"32px\", \"2xl\": \"44px\" },\n full: { xs: \"4px\", sm: \"8px\", md: \"9999px\", lg: \"9999px\", xl: \"9999px\", \"2xl\": \"9999px\" },\n};\n\nconst SCALE_STEPS: ScaleStep[] = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900];\n\nfunction writeScale(tokens: Record<string, string>, name: string, scale: ColorScale): void {\n for (const step of SCALE_STEPS) {\n tokens[`--tempest-${name}-${step}`] = scale[step];\n }\n}\n\n/** Minimum contrast for body text, WCAG 2.x AA. */\nconst AA_TEXT_CONTRAST = 4.5;\n\n/**\n * Choose the ramp step for text sitting on the soft tint.\n *\n * A fixed step does not survive an arbitrary brand color: the built-in blue only\n * reaches 4.37:1 at `500` over its own `50` tint, and a generated emerald lands\n * at 4.41:1 at `600` — both fail AA for body text by a hair. So the step is\n * picked by measuring, walking away from the tint until it clears AA, and\n * falling back to the most extreme candidate when even that cannot (a very light\n * or very desaturated brand).\n *\n * @param scale - The generated ramp.\n * @param softStep - Step used as `--tempest-primary-soft`.\n * @param candidates - Steps to try, in order of preference.\n * @returns The first candidate clearing {@link AA_TEXT_CONTRAST}, else the last.\n */\nfunction pickOnSoftStep(\n scale: ColorScale,\n softStep: ScaleStep,\n candidates: ScaleStep[],\n): ScaleStep {\n for (const step of candidates) {\n if (contrastRatio(scale[softStep], scale[step]) >= AA_TEXT_CONTRAST) return step;\n }\n return candidates[candidates.length - 1];\n}\n\n/** Minimum contrast for a non-text indicator, WCAG 2.2 SC 1.4.11. */\nconst INDICATOR_CONTRAST = 3;\n\n/**\n * The four surfaces a focus ring can land on, as the SDK's own `colors.css`\n * paints them.\n *\n * Ported from `src/styles/colors.css` (`--tempest-bg`, `--tempest-surface`,\n * `--tempest-surface-2`, `--tempest-surface-3`, resolved through the gray ramp),\n * and pinned by a test that reads that file — a theme that only names `primary`\n * keeps these, so the ring has to be measured against them and not against a\n * guess. The light values resolve through `--tempest-gray-50/100/200`; the dark\n * ones are literals in the dark block.\n */\nconst SDK_SURFACES: Record<\"light\" | \"dark\", readonly string[]> = {\n light: [\"#ffffff\", \"#f8f9fb\", \"#f1f3f6\", \"#e4e7ec\"],\n dark: [\"#0b0d12\", \"#14171f\", \"#1d2230\", \"#262d3f\"],\n};\n\n/**\n * Ramp steps to try for the focus ring, in order.\n *\n * `500` first, because the ring should read as the brand when it can, then up\n * the ramp — which moves away from the surfaces in **both** schemes, because\n * `createColorScale` inverts the dark ramp: there `50` is the darkest step (it\n * is what `--tempest-bg` is made of) and `900` the lightest. Walking down in\n * dark mode looks like the right inversion and is the wrong direction; measured,\n * it left a near-black brand's ring at 1.04:1 over its own background.\n */\nconst FOCUS_RING_CANDIDATES: readonly ScaleStep[] = [500, 600, 700, 800, 900];\n\n/**\n * Choose the ramp step for the focus ring, by measuring it against the surfaces\n * it will actually be drawn on.\n *\n * A fixed step does not survive an arbitrary brand, and the measurement says so\n * loudly: of twelve brands checked against the four surfaces in both schemes —\n * 96 pairings — `500` opaque clears 3:1 in 58 of them, and `500` at the old\n * default alpha of 0.35 in 3. Picking the step by measurement clears all 96. The\n * brand from the report that opened this\n * (`#8100D7`) passes in light at 7.20:1 and fails in **dark** at 2.70:1 down to\n * 1.91:1 on `surface-3`; a yellow fails in light at 1.43:1. So the step is\n * picked the way {@link pickOnSoftStep} picks text on a tint: walk away from the\n * surfaces until the ring clears the floor against *all* of them.\n *\n * The fallback is the candidate with the best worst case rather than the last\n * one, because \"the most extreme step\" is not always the most contrasting one\n * once a brand is near-neutral.\n *\n * @param scale - The generated ramp.\n * @param surfaces - The four surfaces this theme will paint, opaque hex.\n * @returns The first candidate clearing {@link INDICATOR_CONTRAST} on every\n * surface, else the candidate whose weakest pairing is strongest.\n */\nfunction pickFocusRingStep(scale: ColorScale, surfaces: readonly string[]): ScaleStep {\n const worstCase = (step: ScaleStep): number =>\n Math.min(...surfaces.map((surface) => contrastRatio(scale[step], surface)));\n let best: ScaleStep = FOCUS_RING_CANDIDATES[0] as ScaleStep;\n let bestRatio = -1;\n for (const step of FOCUS_RING_CANDIDATES) {\n const measured = worstCase(step);\n if (measured >= INDICATOR_CONTRAST) return step;\n if (measured > bestRatio) {\n best = step;\n bestRatio = measured;\n }\n }\n return best;\n}\n\n/**\n * The surfaces this theme will actually paint, per scheme.\n *\n * A theme that names `gray` repaints all four, so measuring the ring against the\n * SDK's would be measuring the wrong backdrop — and a theme that does not keeps\n * the SDK's, which is the common case. The order and the ramp steps mirror\n * {@link writeNeutralAliases}, which is what emits them.\n *\n * @param scheme - Which scheme is being emitted.\n * @param grayScales - The generated neutral ramps, when the theme names one.\n * @returns Four opaque hex colors: bg, surface, surface-2, surface-3.\n */\nfunction surfacesOf(\n scheme: \"light\" | \"dark\",\n grayScales: { light: ColorScale; dark: ColorScale } | undefined,\n): readonly string[] {\n if (!grayScales) return SDK_SURFACES[scheme];\n const scale = grayScales[scheme];\n return scheme === \"light\"\n ? [\"#ffffff\", scale[50], scale[100], scale[200]]\n : [scale[50], scale[100], scale[200], scale[300]];\n}\n\n/**\n * Warn, in a development build, that an alpha was asked for the focus ring.\n *\n * A translucent ring has no contrast of its own — it has the contrast of\n * whatever it composites over — and that is the failure this option used to\n * ship by default: measured at alpha `0.35`, twelve brands across four surfaces\n * and both schemes produced 96 pairings, of which 3 cleared the 3:1 of WCAG 2.2\n * SC 1.4.11. It stays available, because a translucent ring over a\n * known background is a legitimate choice, but it is a choice a theme now has to\n * make on purpose.\n *\n * The warning is the difficult part to notice otherwise: a failing focus ring is\n * visible, it just does not separate.\n *\n * @param focusRingAlpha - The opacity the caller asked for, if any.\n */\nfunction warnOnTranslucentFocusRing(focusRingAlpha: number | undefined): void {\n if (focusRingAlpha === undefined || focusRingAlpha >= 1) return;\n if (!isDevBuild()) return;\n console.warn(\n `[tempest-react-sdk] createTheme({ focusRingAlpha: ${focusRingAlpha} }) makes the focus ring ` +\n \"translucent, so its contrast becomes whatever it composites over — measured below the 3:1 \" +\n \"of WCAG 2.2 SC 1.4.11 on every surface the SDK paints. Drop the option to get the opaque \" +\n \"ring, which is derived from your brand ramp and measured against those surfaces.\",\n );\n}\n\n/**\n * Emit the primary aliases for one scheme.\n *\n * The dark scheme walks the ramp the other way (hover is *lighter*, the soft\n * tint is a dark shade, and readable text on that tint is a light shade) — the\n * same inversion the built-in `colors.css` dark block does by hand.\n *\n * The selected-state indicator is derived from the same measurement as the focus\n * ring and emitted as its own token. It never takes `focusRingAlpha`: a\n * translucent ring over a known background is a legitimate choice a theme can\n * make, while a translucent *state* indicator has the contrast of whatever it\n * composites over, which is the defect this token exists to end.\n *\n * @param tokens - The token map for this scheme, written in place.\n * @param scale - The generated primary ramp.\n * @param scheme - Which scheme is being emitted.\n * @param surfaces - The surfaces this theme paints, for the focus-ring measurement.\n * @param focusRingAlpha - Opacity asked for the ring, or `undefined` for opaque.\n */\nfunction writePrimaryAliases(\n tokens: Record<string, string>,\n scale: ColorScale,\n scheme: \"light\" | \"dark\",\n surfaces: readonly string[],\n focusRingAlpha: number | undefined,\n): void {\n tokens[\"--tempest-primary\"] = \"var(--tempest-primary-500)\";\n if (scheme === \"light\") {\n tokens[\"--tempest-primary-hover\"] = \"var(--tempest-primary-600)\";\n tokens[\"--tempest-primary-active\"] = \"var(--tempest-primary-700)\";\n tokens[\"--tempest-primary-soft\"] = \"var(--tempest-primary-50)\";\n tokens[\"--tempest-primary-soft-hover\"] = \"var(--tempest-primary-100)\";\n tokens[\"--tempest-primary-on-soft\"] =\n `var(--tempest-primary-${pickOnSoftStep(scale, 50, [600, 700, 800, 900])})`;\n } else {\n tokens[\"--tempest-primary-hover\"] = \"var(--tempest-primary-400)\";\n tokens[\"--tempest-primary-active\"] = \"var(--tempest-primary-300)\";\n tokens[\"--tempest-primary-soft\"] = \"var(--tempest-primary-100)\";\n tokens[\"--tempest-primary-soft-hover\"] = \"var(--tempest-primary-200)\";\n tokens[\"--tempest-primary-on-soft\"] =\n `var(--tempest-primary-${pickOnSoftStep(scale, 100, [700, 800, 900])})`;\n }\n\n const foreground = readableForeground(scale[500]);\n tokens[\"--tempest-primary-foreground\"] = foreground;\n tokens[\"--tempest-text-on-primary\"] = foreground;\n const ring = scale[pickFocusRingStep(scale, surfaces)];\n tokens[\"--tempest-focus-ring-color\"] =\n focusRingAlpha === undefined || focusRingAlpha >= 1\n ? ring\n : hexToRgbaString(ring, focusRingAlpha);\n tokens[\"--tempest-selected-indicator\"] = ring;\n}\n\n/**\n * Emit the neutral surface/border/text aliases from a generated gray scale.\n *\n * `--tempest-bg` is pushed past the ramp on purpose: pure white in light mode and\n * a shade darker than `gray-50` in dark mode, so a raised surface still reads as\n * raised against the page.\n */\nfunction writeNeutralAliases(\n tokens: Record<string, string>,\n scale: ColorScale,\n scheme: \"light\" | \"dark\",\n): void {\n if (scheme === \"light\") {\n tokens[\"--tempest-bg\"] = \"#ffffff\";\n tokens[\"--tempest-surface\"] = \"var(--tempest-gray-50)\";\n tokens[\"--tempest-surface-2\"] = \"var(--tempest-gray-100)\";\n tokens[\"--tempest-surface-3\"] = \"var(--tempest-gray-200)\";\n tokens[\"--tempest-border\"] = \"var(--tempest-gray-200)\";\n tokens[\"--tempest-border-strong\"] = \"var(--tempest-gray-300)\";\n tokens[\"--tempest-text\"] = \"var(--tempest-gray-900)\";\n tokens[\"--tempest-text-muted\"] = \"var(--tempest-gray-600)\";\n tokens[\"--tempest-text-subtle\"] = \"var(--tempest-gray-500)\";\n } else {\n tokens[\"--tempest-bg\"] = scale[50];\n tokens[\"--tempest-surface\"] = \"var(--tempest-gray-100)\";\n tokens[\"--tempest-surface-2\"] = \"var(--tempest-gray-200)\";\n tokens[\"--tempest-surface-3\"] = \"var(--tempest-gray-300)\";\n tokens[\"--tempest-border\"] = \"var(--tempest-gray-300)\";\n tokens[\"--tempest-border-strong\"] = \"var(--tempest-gray-400)\";\n tokens[\"--tempest-text\"] = \"var(--tempest-gray-900)\";\n tokens[\"--tempest-text-muted\"] = \"var(--tempest-gray-700)\";\n tokens[\"--tempest-text-subtle\"] = \"var(--tempest-gray-600)\";\n }\n tokens[\"--tempest-neutral-on-solid\"] = readableForeground(scale[700], \"#ffffff\", ON_SOLID_INK);\n}\n\nfunction renderBlock(selector: string, tokens: Record<string, string>): string {\n const entries = Object.entries(tokens);\n if (entries.length === 0) return \"\";\n const body = entries.map(([name, value]) => ` ${name}: ${value};`).join(\"\\n\");\n return `${selector} {\\n${body}\\n}`;\n}\n\n/**\n * Generate `--tempest-*` overrides from a small brand description.\n *\n * Only the families you pass are generated; everything else falls through to the\n * SDK's own tokens, so a theme stays a patch and not a fork of `colors.css`.\n *\n * @example\n * ```ts\n * import { applyTheme, createTheme } from \"tempest-react-sdk\";\n *\n * const theme = createTheme({\n * primary: \"#7c3aed\",\n * radius: \"lg\",\n * chart: [\"#7c3aed\", \"#0ea5e9\", \"#22c55e\", \"#f59e0b\"],\n * });\n *\n * applyTheme(theme);\n * ```\n *\n * @param options - Brand colors plus optional radius / chart / focus-ring tuning.\n * @returns The light and dark token maps and the CSS text that carries them.\n */\nexport function createTheme(options: CreateThemeOptions = {}): GeneratedTheme {\n const {\n primary,\n gray,\n chart,\n radius,\n focusRingAlpha,\n selector = \":root\",\n darkSelector = '[data-tempest-theme=\"dark\"]',\n inverse,\n inverseSelector = INVERSE_SELECTOR,\n } = options;\n\n warnOnTranslucentFocusRing(focusRingAlpha);\n\n const light: Record<string, string> = {};\n const dark: Record<string, string> = {};\n\n // `anchor: false`: a neutral ramp has to keep its tuned lightness curve.\n // Anchoring it at the input compressed both halves and dropped\n // text-muted-on-surface-3 to ~4.2:1 — below AA — which the browser axe\n // sweep caught on the generated themes.\n const grayScales = gray\n ? {\n light: createColorScale(gray, \"light\", { anchor: false, neutral: true }),\n dark: createColorScale(gray, \"dark\", { anchor: false, neutral: true }),\n }\n : undefined;\n\n if (primary) {\n const lightScale = createColorScale(primary, \"light\");\n const darkScale = createColorScale(primary, \"dark\");\n writeScale(light, \"primary\", lightScale);\n writeScale(dark, \"primary\", darkScale);\n writePrimaryAliases(\n light,\n lightScale,\n \"light\",\n surfacesOf(\"light\", grayScales),\n focusRingAlpha,\n );\n writePrimaryAliases(\n dark,\n darkScale,\n \"dark\",\n surfacesOf(\"dark\", grayScales),\n focusRingAlpha,\n );\n }\n\n if (grayScales) {\n writeScale(light, \"gray\", grayScales.light);\n writeScale(dark, \"gray\", grayScales.dark);\n writeNeutralAliases(light, grayScales.light, \"light\");\n writeNeutralAliases(dark, grayScales.dark, \"dark\");\n }\n\n for (const status of THEME_STATUSES) {\n const value = options[status];\n if (!value) continue;\n writeStatus(light, status, createColorScale(value, \"light\"), \"light\");\n writeStatus(dark, status, createColorScale(value, \"dark\"), \"dark\");\n }\n\n if (chart?.length) {\n chart.forEach((color, index) => {\n light[`--tempest-chart-${index + 1}`] = color;\n });\n // Declares how many series colors this theme owns. Without it the reader\n // keeps walking into the SDK's built-in `--tempest-chart-7`/`-8`, so a\n // 6-color brand palette silently mixes with two leftover defaults — which\n // is exactly what a 7-series chart would show.\n light[\"--tempest-chart-count\"] = String(chart.length);\n }\n\n /*\n * Continuous scales follow the brand too.\n *\n * Without this a rebranded app gets its categorical slots recoloured and then a\n * heatmap still painted in the SDK's blue — the one chart on the page that\n * silently ignores the brand. Both scales derive from the first series hue (or\n * the primary), stepped by perceptual lightness for each mode separately.\n *\n * Skipped entirely when the theme names no colour to derive from, so the\n * built-in tokens survive instead of being overwritten with a guess.\n */\n const scaleSource = chart?.[0] ?? primary;\n if (scaleSource) {\n const coolHue = hueOf(scaleSource);\n // The opposite pole: the brand's own danger colour when it has one, since a\n // diverging scale's warm arm and \"bad\" should not disagree on screen.\n const warmHue = options.danger ? hueOf(options.danger) : (coolHue + 180) % 360;\n for (const [target, mode] of [\n [light, \"light\"],\n [dark, \"dark\"],\n ] as const) {\n buildRamp(coolHue, mode).forEach((stepColor, index) => {\n target[`--tempest-chart-sequential-${index + 1}`] = stepColor;\n });\n const diverging = buildDivergingRamp({\n coolHue,\n warmHue,\n mid: mode === \"light\" ? \"#e4e7ec\" : \"#262d3f\",\n mode,\n });\n [...diverging.cool, diverging.mid, ...diverging.warm].forEach((stepColor, index) => {\n target[`--tempest-chart-diverging-${index + 1}`] = stepColor;\n });\n }\n }\n\n if (radius) {\n const steps = typeof radius === \"string\" ? RADIUS_PRESETS[radius] : radius;\n for (const [step, value] of Object.entries(steps)) {\n light[`--tempest-radius-${step}`] = value;\n }\n }\n\n const inverseSurface =\n primary && inverse\n ? inverse({\n primary,\n statuses: {\n success: options.success,\n warning: options.warning,\n danger: options.danger,\n info: options.info,\n },\n chart,\n })\n : null;\n\n const css = [\n renderBlock(selector, light),\n renderBlock(darkSelector, dark),\n inverseSurface ? renderInverseSurface(inverseSelector, inverseSurface) : \"\",\n ]\n .filter(Boolean)\n .join(\"\\n\\n\");\n\n return inverseSurface\n ? { light, dark, inverse: inverseSurface.tokens, css }\n : { light, dark, css };\n}\n\n/**\n * Contrast ratio of `--tempest-primary-foreground` over the brand color.\n *\n * Exposed so an app (or a test) can assert its own brand clears WCAG AA (4.5) for\n * body text or AA-large (3.0) for button labels, instead of trusting the pick.\n *\n * @param options - The same input given to {@link createTheme}.\n * @returns The ratio, or `null` when no `primary` was provided.\n */\nexport function themeContrast(options: CreateThemeOptions): number | null {\n if (!options.primary) return null;\n const scale = createColorScale(options.primary, \"light\");\n return contrastRatio(scale[500], readableForeground(scale[500]));\n}\n"],"mappings":"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"}
@@ -1,8 +1,10 @@
1
1
  import { isDevBuild as e } from "../utils/dev-mode.js";
2
2
  import { contrastRatio as t, createColorScale as n, hexToRgbaString as r, readableForeground as i } from "./color.js";
3
3
  import { buildDivergingRamp as a, buildRamp as o, hueOf as s } from "./data-viz-ramps.js";
4
+ import { INVERSE_SELECTOR as c, renderInverseSurface as l } from "./inverse-render.js";
5
+ import { ON_SOLID_INK as u, THEME_STATUSES as d, writeStatus as f } from "./status-tokens.js";
4
6
  //#region src/theme/create-theme.ts
5
- var c = {
7
+ var p = {
6
8
  none: {
7
9
  xs: "0",
8
10
  sm: "0",
@@ -51,7 +53,7 @@ var c = {
51
53
  xl: "9999px",
52
54
  "2xl": "9999px"
53
55
  }
54
- }, l = [
56
+ }, m = [
55
57
  50,
56
58
  100,
57
59
  200,
@@ -63,15 +65,15 @@ var c = {
63
65
  800,
64
66
  900
65
67
  ];
66
- function u(e, t, n) {
67
- for (let r of l) e[`--tempest-${t}-${r}`] = n[r];
68
+ function h(e, t, n) {
69
+ for (let r of m) e[`--tempest-${t}-${r}`] = n[r];
68
70
  }
69
- var d = 4.5;
70
- function f(e, n, r) {
71
- for (let i of r) if (t(e[n], e[i]) >= d) return i;
71
+ var g = 4.5;
72
+ function _(e, n, r) {
73
+ for (let i of r) if (t(e[n], e[i]) >= g) return i;
72
74
  return r[r.length - 1];
73
75
  }
74
- var p = 3, m = {
76
+ var v = 3, y = {
75
77
  light: [
76
78
  "#ffffff",
77
79
  "#f8f9fb",
@@ -84,24 +86,24 @@ var p = 3, m = {
84
86
  "#1d2230",
85
87
  "#262d3f"
86
88
  ]
87
- }, h = [
89
+ }, b = [
88
90
  500,
89
91
  600,
90
92
  700,
91
93
  800,
92
94
  900
93
95
  ];
94
- function g(e, n) {
95
- let r = (r) => Math.min(...n.map((n) => t(e[r], n))), i = h[0], a = -1;
96
- for (let e of h) {
96
+ function x(e, n) {
97
+ let r = (r) => Math.min(...n.map((n) => t(e[r], n))), i = b[0], a = -1;
98
+ for (let e of b) {
97
99
  let t = r(e);
98
- if (t >= p) return e;
100
+ if (t >= v) return e;
99
101
  t > a && (i = e, a = t);
100
102
  }
101
103
  return i;
102
104
  }
103
- function _(e, t) {
104
- if (!t) return m[e];
105
+ function S(e, t) {
106
+ if (!t) return y[e];
105
107
  let n = t[e];
106
108
  return e === "light" ? [
107
109
  "#ffffff",
@@ -115,40 +117,36 @@ function _(e, t) {
115
117
  n[300]
116
118
  ];
117
119
  }
118
- function v(t) {
120
+ function C(t) {
119
121
  t === void 0 || t >= 1 || e() && console.warn(`[tempest-react-sdk] createTheme({ focusRingAlpha: ${t} }) makes the focus ring translucent, so its contrast becomes whatever it composites over — measured below the 3:1 of WCAG 2.2 SC 1.4.11 on every surface the SDK paints. Drop the option to get the opaque ring, which is derived from your brand ramp and measured against those surfaces.`);
120
122
  }
121
- var y = "#1f0606";
122
- function b(e, t, n, a, o) {
123
- e["--tempest-primary"] = "var(--tempest-primary-500)", n === "light" ? (e["--tempest-primary-hover"] = "var(--tempest-primary-600)", e["--tempest-primary-active"] = "var(--tempest-primary-700)", e["--tempest-primary-soft"] = "var(--tempest-primary-50)", e["--tempest-primary-soft-hover"] = "var(--tempest-primary-100)", e["--tempest-primary-on-soft"] = `var(--tempest-primary-${f(t, 50, [
123
+ function w(e, t, n, a, o) {
124
+ e["--tempest-primary"] = "var(--tempest-primary-500)", n === "light" ? (e["--tempest-primary-hover"] = "var(--tempest-primary-600)", e["--tempest-primary-active"] = "var(--tempest-primary-700)", e["--tempest-primary-soft"] = "var(--tempest-primary-50)", e["--tempest-primary-soft-hover"] = "var(--tempest-primary-100)", e["--tempest-primary-on-soft"] = `var(--tempest-primary-${_(t, 50, [
124
125
  600,
125
126
  700,
126
127
  800,
127
128
  900
128
- ])})`) : (e["--tempest-primary-hover"] = "var(--tempest-primary-400)", e["--tempest-primary-active"] = "var(--tempest-primary-300)", e["--tempest-primary-soft"] = "var(--tempest-primary-100)", e["--tempest-primary-soft-hover"] = "var(--tempest-primary-200)", e["--tempest-primary-on-soft"] = `var(--tempest-primary-${f(t, 100, [
129
+ ])})`) : (e["--tempest-primary-hover"] = "var(--tempest-primary-400)", e["--tempest-primary-active"] = "var(--tempest-primary-300)", e["--tempest-primary-soft"] = "var(--tempest-primary-100)", e["--tempest-primary-soft-hover"] = "var(--tempest-primary-200)", e["--tempest-primary-on-soft"] = `var(--tempest-primary-${_(t, 100, [
129
130
  700,
130
131
  800,
131
132
  900
132
133
  ])})`);
133
134
  let s = i(t[500]);
134
135
  e["--tempest-primary-foreground"] = s, e["--tempest-text-on-primary"] = s;
135
- let c = t[g(t, a)];
136
+ let c = t[x(t, a)];
136
137
  e["--tempest-focus-ring-color"] = o === void 0 || o >= 1 ? c : r(c, o), e["--tempest-selected-indicator"] = c;
137
138
  }
138
- function x(e, t, n) {
139
- n === "light" ? (e["--tempest-bg"] = "#ffffff", e["--tempest-surface"] = "var(--tempest-gray-50)", e["--tempest-surface-2"] = "var(--tempest-gray-100)", e["--tempest-surface-3"] = "var(--tempest-gray-200)", e["--tempest-border"] = "var(--tempest-gray-200)", e["--tempest-border-strong"] = "var(--tempest-gray-300)", e["--tempest-text"] = "var(--tempest-gray-900)", e["--tempest-text-muted"] = "var(--tempest-gray-600)", e["--tempest-text-subtle"] = "var(--tempest-gray-500)") : (e["--tempest-bg"] = t[50], e["--tempest-surface"] = "var(--tempest-gray-100)", e["--tempest-surface-2"] = "var(--tempest-gray-200)", e["--tempest-surface-3"] = "var(--tempest-gray-300)", e["--tempest-border"] = "var(--tempest-gray-300)", e["--tempest-border-strong"] = "var(--tempest-gray-400)", e["--tempest-text"] = "var(--tempest-gray-900)", e["--tempest-text-muted"] = "var(--tempest-gray-700)", e["--tempest-text-subtle"] = "var(--tempest-gray-600)"), e["--tempest-neutral-on-solid"] = i(t[700], "#ffffff", y);
139
+ function T(e, t, n) {
140
+ n === "light" ? (e["--tempest-bg"] = "#ffffff", e["--tempest-surface"] = "var(--tempest-gray-50)", e["--tempest-surface-2"] = "var(--tempest-gray-100)", e["--tempest-surface-3"] = "var(--tempest-gray-200)", e["--tempest-border"] = "var(--tempest-gray-200)", e["--tempest-border-strong"] = "var(--tempest-gray-300)", e["--tempest-text"] = "var(--tempest-gray-900)", e["--tempest-text-muted"] = "var(--tempest-gray-600)", e["--tempest-text-subtle"] = "var(--tempest-gray-500)") : (e["--tempest-bg"] = t[50], e["--tempest-surface"] = "var(--tempest-gray-100)", e["--tempest-surface-2"] = "var(--tempest-gray-200)", e["--tempest-surface-3"] = "var(--tempest-gray-300)", e["--tempest-border"] = "var(--tempest-gray-300)", e["--tempest-border-strong"] = "var(--tempest-gray-400)", e["--tempest-text"] = "var(--tempest-gray-900)", e["--tempest-text-muted"] = "var(--tempest-gray-700)", e["--tempest-text-subtle"] = "var(--tempest-gray-600)"), e["--tempest-neutral-on-solid"] = i(t[700], "#ffffff", u);
140
141
  }
141
- function S(e, t, n, r) {
142
- r === "light" ? (e[`--tempest-${t}`] = n[700], e[`--tempest-${t}-fg`] = n[800], e[`--tempest-${t}-bg`] = n[50], e[`--tempest-${t}-border`] = n[200], e[`--tempest-${t}-solid`] = n[600], e[`--tempest-${t}-on-solid`] = i(n[600], "#ffffff", y)) : (e[`--tempest-${t}`] = n[700], e[`--tempest-${t}-fg`] = n[700], e[`--tempest-${t}-bg`] = n[50], e[`--tempest-${t}-border`] = n[200], e[`--tempest-${t}-solid`] = n[500], e[`--tempest-${t}-on-solid`] = i(n[500], "#ffffff", y));
143
- }
144
- function C(e, t) {
142
+ function E(e, t) {
145
143
  let n = Object.entries(t);
146
144
  return n.length === 0 ? "" : `${e} {\n${n.map(([e, t]) => ` ${e}: ${t};`).join("\n")}\n}`;
147
145
  }
148
- function w(e = {}) {
149
- let { primary: t, gray: r, chart: i, radius: l, focusRingAlpha: d, selector: f = ":root", darkSelector: p = "[data-tempest-theme=\"dark\"]" } = e;
150
- v(d);
151
- let m = {}, h = {}, g = r ? {
146
+ function D(e = {}) {
147
+ let { primary: t, gray: r, chart: i, radius: u, focusRingAlpha: m, selector: g = ":root", darkSelector: _ = "[data-tempest-theme=\"dark\"]", inverse: v, inverseSelector: y = c } = e;
148
+ C(m);
149
+ let b = {}, x = {}, D = r ? {
152
150
  light: n(r, "light", {
153
151
  anchor: !1,
154
152
  neutral: !0
@@ -160,25 +158,20 @@ function w(e = {}) {
160
158
  } : void 0;
161
159
  if (t) {
162
160
  let e = n(t, "light"), r = n(t, "dark");
163
- u(m, "primary", e), u(h, "primary", r), b(m, e, "light", _("light", g), d), b(h, r, "dark", _("dark", g), d);
161
+ h(b, "primary", e), h(x, "primary", r), w(b, e, "light", S("light", D), m), w(x, r, "dark", S("dark", D), m);
164
162
  }
165
- g && (u(m, "gray", g.light), u(h, "gray", g.dark), x(m, g.light, "light"), x(h, g.dark, "dark"));
166
- for (let t of [
167
- "success",
168
- "warning",
169
- "danger",
170
- "info"
171
- ]) {
163
+ D && (h(b, "gray", D.light), h(x, "gray", D.dark), T(b, D.light, "light"), T(x, D.dark, "dark"));
164
+ for (let t of d) {
172
165
  let r = e[t];
173
- r && (S(m, t, n(r, "light"), "light"), S(h, t, n(r, "dark"), "dark"));
166
+ r && (f(b, t, n(r, "light"), "light"), f(x, t, n(r, "dark"), "dark"));
174
167
  }
175
168
  i?.length && (i.forEach((e, t) => {
176
- m[`--tempest-chart-${t + 1}`] = e;
177
- }), m["--tempest-chart-count"] = String(i.length));
178
- let y = i?.[0] ?? t;
179
- if (y) {
180
- let t = s(y), n = e.danger ? s(e.danger) : (t + 180) % 360;
181
- for (let [e, r] of [[m, "light"], [h, "dark"]]) {
169
+ b[`--tempest-chart-${t + 1}`] = e;
170
+ }), b["--tempest-chart-count"] = String(i.length));
171
+ let O = i?.[0] ?? t;
172
+ if (O) {
173
+ let t = s(O), n = e.danger ? s(e.danger) : (t + 180) % 360;
174
+ for (let [e, r] of [[b, "light"], [x, "dark"]]) {
182
175
  o(t, r).forEach((t, n) => {
183
176
  e[`--tempest-chart-sequential-${n + 1}`] = t;
184
177
  });
@@ -197,22 +190,41 @@ function w(e = {}) {
197
190
  });
198
191
  }
199
192
  }
200
- if (l) {
201
- let e = typeof l == "string" ? c[l] : l;
202
- for (let [t, n] of Object.entries(e)) m[`--tempest-radius-${t}`] = n;
193
+ if (u) {
194
+ let e = typeof u == "string" ? p[u] : u;
195
+ for (let [t, n] of Object.entries(e)) b[`--tempest-radius-${t}`] = n;
203
196
  }
204
- return {
205
- light: m,
206
- dark: h,
207
- css: [C(f, m), C(p, h)].filter(Boolean).join("\n\n")
197
+ let k = t && v ? v({
198
+ primary: t,
199
+ statuses: {
200
+ success: e.success,
201
+ warning: e.warning,
202
+ danger: e.danger,
203
+ info: e.info
204
+ },
205
+ chart: i
206
+ }) : null, A = [
207
+ E(g, b),
208
+ E(_, x),
209
+ k ? l(y, k) : ""
210
+ ].filter(Boolean).join("\n\n");
211
+ return k ? {
212
+ light: b,
213
+ dark: x,
214
+ inverse: k.tokens,
215
+ css: A
216
+ } : {
217
+ light: b,
218
+ dark: x,
219
+ css: A
208
220
  };
209
221
  }
210
- function T(e) {
222
+ function O(e) {
211
223
  if (!e.primary) return null;
212
224
  let r = n(e.primary, "light");
213
225
  return t(r[500], i(r[500]));
214
226
  }
215
227
  //#endregion
216
- export { w as createTheme, T as themeContrast };
228
+ export { D as createTheme, O as themeContrast };
217
229
 
218
230
  //# sourceMappingURL=create-theme.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"create-theme.js","names":[],"sources":["../../src/theme/create-theme.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — one pass that emits the whole token set — surfaces,\n * text, borders, states, the chart ramps — for both modes, with every value checked\n * against the surface it will sit on. The checks are what make the length: each\n * token is derived and then verified against the same background table.\n */\n/**\n * Theme factory: turns a handful of brand colors into the full set of\n * `--tempest-*` token overrides, for both light and dark.\n *\n * Rebranding used to mean hand-writing ~167 custom properties (and getting the\n * dark ramp inversion right by hand). `createTheme({ primary: \"#7c3aed\" })`\n * emits the same thing, derived in OKLCH so the ramp stays perceptually even.\n */\nimport {\n contrastRatio,\n createColorScale,\n hexToRgbaString,\n readableForeground,\n type ColorScale,\n type ScaleStep,\n} from \"./color\";\nimport { buildDivergingRamp, buildRamp, hueOf } from \"./data-viz-ramps\";\nimport { isDevBuild } from \"../utils/dev-mode\";\n\n/** Radius presets, applied to the whole `--tempest-radius-*` family at once. */\nexport type ThemeRadius = \"none\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"full\";\n\n/** Status token families that {@link createTheme} can regenerate. */\nexport type ThemeStatus = \"success\" | \"warning\" | \"danger\" | \"info\";\n\n/** Input for {@link createTheme}. Every field is optional — omitted families keep the built-in tokens. */\nexport interface CreateThemeOptions {\n /** Brand color, used as the `500` step of the primary scale. */\n primary?: string;\n /** Neutral color, used as the `500` step of the gray scale (surfaces, borders, text). */\n gray?: string;\n /** Success color (`--tempest-success*`). */\n success?: string;\n /** Warning color (`--tempest-warning*`). */\n warning?: string;\n /** Danger color (`--tempest-danger*`). */\n danger?: string;\n /** Info color (`--tempest-info*`). */\n info?: string;\n /**\n * Categorical series colors for `tempest-react-sdk/charts`, in cycle order.\n * Written to `--tempest-chart-1` … `--tempest-chart-N`, so charts follow the\n * theme instead of a hardcoded palette.\n */\n chart?: string[];\n /** Corner radius scale. A preset name, or explicit per-step values. */\n radius?: ThemeRadius | Partial<Record<\"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\", string>>;\n /**\n * Opacity of `--tempest-focus-ring-color`. Omit it — the default ring is opaque.\n *\n * A translucent ring has no contrast of its own; it has the contrast of\n * whatever it composites over. Passing a value below `1` reintroduces the\n * failure 0.61.0 fixed in the built-in tokens, and logs a warning in a\n * development build. Kept because a translucent ring over a background you\n * control is a legitimate choice.\n */\n focusRingAlpha?: number;\n /** Selector the light tokens are written under. Default `\":root\"`. */\n selector?: string;\n /** Selector the dark tokens are written under. Default `'[data-tempest-theme=\"dark\"]'`. */\n darkSelector?: string;\n}\n\n/** A generated theme: token maps per color scheme, plus the CSS text that carries them. */\nexport interface GeneratedTheme {\n /** Light-scheme custom properties, without the leading `--`-less names (keys include `--`). */\n light: Record<string, string>;\n /** Dark-scheme custom properties. */\n dark: Record<string, string>;\n /** Both blocks rendered as CSS, ready for {@link applyTheme} or a stylesheet. */\n css: string;\n}\n\nconst RADIUS_PRESETS: Record<ThemeRadius, Record<string, string>> = {\n none: { xs: \"0\", sm: \"0\", md: \"0\", lg: \"0\", xl: \"0\", \"2xl\": \"0\" },\n sm: { xs: \"1px\", sm: \"2px\", md: \"4px\", lg: \"6px\", xl: \"8px\", \"2xl\": \"12px\" },\n md: { xs: \"2px\", sm: \"4px\", md: \"8px\", lg: \"12px\", xl: \"16px\", \"2xl\": \"24px\" },\n lg: { xs: \"4px\", sm: \"6px\", md: \"12px\", lg: \"16px\", xl: \"22px\", \"2xl\": \"32px\" },\n xl: { xs: \"6px\", sm: \"10px\", md: \"16px\", lg: \"24px\", xl: \"32px\", \"2xl\": \"44px\" },\n full: { xs: \"4px\", sm: \"8px\", md: \"9999px\", lg: \"9999px\", xl: \"9999px\", \"2xl\": \"9999px\" },\n};\n\nconst SCALE_STEPS: ScaleStep[] = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900];\n\nfunction writeScale(tokens: Record<string, string>, name: string, scale: ColorScale): void {\n for (const step of SCALE_STEPS) {\n tokens[`--tempest-${name}-${step}`] = scale[step];\n }\n}\n\n/** Minimum contrast for body text, WCAG 2.x AA. */\nconst AA_TEXT_CONTRAST = 4.5;\n\n/**\n * Choose the ramp step for text sitting on the soft tint.\n *\n * A fixed step does not survive an arbitrary brand color: the built-in blue only\n * reaches 4.37:1 at `500` over its own `50` tint, and a generated emerald lands\n * at 4.41:1 at `600` — both fail AA for body text by a hair. So the step is\n * picked by measuring, walking away from the tint until it clears AA, and\n * falling back to the most extreme candidate when even that cannot (a very light\n * or very desaturated brand).\n *\n * @param scale - The generated ramp.\n * @param softStep - Step used as `--tempest-primary-soft`.\n * @param candidates - Steps to try, in order of preference.\n * @returns The first candidate clearing {@link AA_TEXT_CONTRAST}, else the last.\n */\nfunction pickOnSoftStep(\n scale: ColorScale,\n softStep: ScaleStep,\n candidates: ScaleStep[],\n): ScaleStep {\n for (const step of candidates) {\n if (contrastRatio(scale[softStep], scale[step]) >= AA_TEXT_CONTRAST) return step;\n }\n return candidates[candidates.length - 1];\n}\n\n/** Minimum contrast for a non-text indicator, WCAG 2.2 SC 1.4.11. */\nconst INDICATOR_CONTRAST = 3;\n\n/**\n * The four surfaces a focus ring can land on, as the SDK's own `colors.css`\n * paints them.\n *\n * Ported from `src/styles/colors.css` (`--tempest-bg`, `--tempest-surface`,\n * `--tempest-surface-2`, `--tempest-surface-3`, resolved through the gray ramp),\n * and pinned by a test that reads that file — a theme that only names `primary`\n * keeps these, so the ring has to be measured against them and not against a\n * guess. The light values resolve through `--tempest-gray-50/100/200`; the dark\n * ones are literals in the dark block.\n */\nconst SDK_SURFACES: Record<\"light\" | \"dark\", readonly string[]> = {\n light: [\"#ffffff\", \"#f8f9fb\", \"#f1f3f6\", \"#e4e7ec\"],\n dark: [\"#0b0d12\", \"#14171f\", \"#1d2230\", \"#262d3f\"],\n};\n\n/**\n * Ramp steps to try for the focus ring, in order.\n *\n * `500` first, because the ring should read as the brand when it can, then up\n * the ramp — which moves away from the surfaces in **both** schemes, because\n * `createColorScale` inverts the dark ramp: there `50` is the darkest step (it\n * is what `--tempest-bg` is made of) and `900` the lightest. Walking down in\n * dark mode looks like the right inversion and is the wrong direction; measured,\n * it left a near-black brand's ring at 1.04:1 over its own background.\n */\nconst FOCUS_RING_CANDIDATES: readonly ScaleStep[] = [500, 600, 700, 800, 900];\n\n/**\n * Choose the ramp step for the focus ring, by measuring it against the surfaces\n * it will actually be drawn on.\n *\n * A fixed step does not survive an arbitrary brand, and the measurement says so\n * loudly: of twelve brands checked against the four surfaces in both schemes —\n * 96 pairings — `500` opaque clears 3:1 in 58 of them, and `500` at the old\n * default alpha of 0.35 in 3. Picking the step by measurement clears all 96. The\n * brand from the report that opened this\n * (`#8100D7`) passes in light at 7.20:1 and fails in **dark** at 2.70:1 down to\n * 1.91:1 on `surface-3`; a yellow fails in light at 1.43:1. So the step is\n * picked the way {@link pickOnSoftStep} picks text on a tint: walk away from the\n * surfaces until the ring clears the floor against *all* of them.\n *\n * The fallback is the candidate with the best worst case rather than the last\n * one, because \"the most extreme step\" is not always the most contrasting one\n * once a brand is near-neutral.\n *\n * @param scale - The generated ramp.\n * @param surfaces - The four surfaces this theme will paint, opaque hex.\n * @returns The first candidate clearing {@link INDICATOR_CONTRAST} on every\n * surface, else the candidate whose weakest pairing is strongest.\n */\nfunction pickFocusRingStep(scale: ColorScale, surfaces: readonly string[]): ScaleStep {\n const worstCase = (step: ScaleStep): number =>\n Math.min(...surfaces.map((surface) => contrastRatio(scale[step], surface)));\n let best: ScaleStep = FOCUS_RING_CANDIDATES[0] as ScaleStep;\n let bestRatio = -1;\n for (const step of FOCUS_RING_CANDIDATES) {\n const measured = worstCase(step);\n if (measured >= INDICATOR_CONTRAST) return step;\n if (measured > bestRatio) {\n best = step;\n bestRatio = measured;\n }\n }\n return best;\n}\n\n/**\n * The surfaces this theme will actually paint, per scheme.\n *\n * A theme that names `gray` repaints all four, so measuring the ring against the\n * SDK's would be measuring the wrong backdrop — and a theme that does not keeps\n * the SDK's, which is the common case. The order and the ramp steps mirror\n * {@link writeNeutralAliases}, which is what emits them.\n *\n * @param scheme - Which scheme is being emitted.\n * @param grayScales - The generated neutral ramps, when the theme names one.\n * @returns Four opaque hex colors: bg, surface, surface-2, surface-3.\n */\nfunction surfacesOf(\n scheme: \"light\" | \"dark\",\n grayScales: { light: ColorScale; dark: ColorScale } | undefined,\n): readonly string[] {\n if (!grayScales) return SDK_SURFACES[scheme];\n const scale = grayScales[scheme];\n return scheme === \"light\"\n ? [\"#ffffff\", scale[50], scale[100], scale[200]]\n : [scale[50], scale[100], scale[200], scale[300]];\n}\n\n/**\n * Warn, in a development build, that an alpha was asked for the focus ring.\n *\n * A translucent ring has no contrast of its own — it has the contrast of\n * whatever it composites over — and that is the failure this option used to\n * ship by default: measured at alpha `0.35`, twelve brands across four surfaces\n * and both schemes produced 96 pairings, of which 3 cleared the 3:1 of WCAG 2.2\n * SC 1.4.11. It stays available, because a translucent ring over a\n * known background is a legitimate choice, but it is a choice a theme now has to\n * make on purpose.\n *\n * The warning is the difficult part to notice otherwise: a failing focus ring is\n * visible, it just does not separate.\n *\n * @param focusRingAlpha - The opacity the caller asked for, if any.\n */\nfunction warnOnTranslucentFocusRing(focusRingAlpha: number | undefined): void {\n if (focusRingAlpha === undefined || focusRingAlpha >= 1) return;\n if (!isDevBuild()) return;\n console.warn(\n `[tempest-react-sdk] createTheme({ focusRingAlpha: ${focusRingAlpha} }) makes the focus ring ` +\n \"translucent, so its contrast becomes whatever it composites over — measured below the 3:1 \" +\n \"of WCAG 2.2 SC 1.4.11 on every surface the SDK paints. Drop the option to get the opaque \" +\n \"ring, which is derived from your brand ramp and measured against those surfaces.\",\n );\n}\n\n/**\n * The dark ink for content sitting on a saturated fill.\n *\n * Near-black tinted toward warm rather than pure black, matching the value\n * `colors.css` uses for the same job: it reads as part of the swatch instead of\n * a hole punched in it.\n */\nconst ON_SOLID_INK = \"#1f0606\";\n\n/**\n * Emit the primary aliases for one scheme.\n *\n * The dark scheme walks the ramp the other way (hover is *lighter*, the soft\n * tint is a dark shade, and readable text on that tint is a light shade) — the\n * same inversion the built-in `colors.css` dark block does by hand.\n *\n * The selected-state indicator is derived from the same measurement as the focus\n * ring and emitted as its own token. It never takes `focusRingAlpha`: a\n * translucent ring over a known background is a legitimate choice a theme can\n * make, while a translucent *state* indicator has the contrast of whatever it\n * composites over, which is the defect this token exists to end.\n *\n * @param tokens - The token map for this scheme, written in place.\n * @param scale - The generated primary ramp.\n * @param scheme - Which scheme is being emitted.\n * @param surfaces - The surfaces this theme paints, for the focus-ring measurement.\n * @param focusRingAlpha - Opacity asked for the ring, or `undefined` for opaque.\n */\nfunction writePrimaryAliases(\n tokens: Record<string, string>,\n scale: ColorScale,\n scheme: \"light\" | \"dark\",\n surfaces: readonly string[],\n focusRingAlpha: number | undefined,\n): void {\n tokens[\"--tempest-primary\"] = \"var(--tempest-primary-500)\";\n if (scheme === \"light\") {\n tokens[\"--tempest-primary-hover\"] = \"var(--tempest-primary-600)\";\n tokens[\"--tempest-primary-active\"] = \"var(--tempest-primary-700)\";\n tokens[\"--tempest-primary-soft\"] = \"var(--tempest-primary-50)\";\n tokens[\"--tempest-primary-soft-hover\"] = \"var(--tempest-primary-100)\";\n tokens[\"--tempest-primary-on-soft\"] =\n `var(--tempest-primary-${pickOnSoftStep(scale, 50, [600, 700, 800, 900])})`;\n } else {\n tokens[\"--tempest-primary-hover\"] = \"var(--tempest-primary-400)\";\n tokens[\"--tempest-primary-active\"] = \"var(--tempest-primary-300)\";\n tokens[\"--tempest-primary-soft\"] = \"var(--tempest-primary-100)\";\n tokens[\"--tempest-primary-soft-hover\"] = \"var(--tempest-primary-200)\";\n tokens[\"--tempest-primary-on-soft\"] =\n `var(--tempest-primary-${pickOnSoftStep(scale, 100, [700, 800, 900])})`;\n }\n\n const foreground = readableForeground(scale[500]);\n tokens[\"--tempest-primary-foreground\"] = foreground;\n tokens[\"--tempest-text-on-primary\"] = foreground;\n const ring = scale[pickFocusRingStep(scale, surfaces)];\n tokens[\"--tempest-focus-ring-color\"] =\n focusRingAlpha === undefined || focusRingAlpha >= 1\n ? ring\n : hexToRgbaString(ring, focusRingAlpha);\n tokens[\"--tempest-selected-indicator\"] = ring;\n}\n\n/**\n * Emit the neutral surface/border/text aliases from a generated gray scale.\n *\n * `--tempest-bg` is pushed past the ramp on purpose: pure white in light mode and\n * a shade darker than `gray-50` in dark mode, so a raised surface still reads as\n * raised against the page.\n */\nfunction writeNeutralAliases(\n tokens: Record<string, string>,\n scale: ColorScale,\n scheme: \"light\" | \"dark\",\n): void {\n if (scheme === \"light\") {\n tokens[\"--tempest-bg\"] = \"#ffffff\";\n tokens[\"--tempest-surface\"] = \"var(--tempest-gray-50)\";\n tokens[\"--tempest-surface-2\"] = \"var(--tempest-gray-100)\";\n tokens[\"--tempest-surface-3\"] = \"var(--tempest-gray-200)\";\n tokens[\"--tempest-border\"] = \"var(--tempest-gray-200)\";\n tokens[\"--tempest-border-strong\"] = \"var(--tempest-gray-300)\";\n tokens[\"--tempest-text\"] = \"var(--tempest-gray-900)\";\n tokens[\"--tempest-text-muted\"] = \"var(--tempest-gray-600)\";\n tokens[\"--tempest-text-subtle\"] = \"var(--tempest-gray-500)\";\n } else {\n tokens[\"--tempest-bg\"] = scale[50];\n tokens[\"--tempest-surface\"] = \"var(--tempest-gray-100)\";\n tokens[\"--tempest-surface-2\"] = \"var(--tempest-gray-200)\";\n tokens[\"--tempest-surface-3\"] = \"var(--tempest-gray-300)\";\n tokens[\"--tempest-border\"] = \"var(--tempest-gray-300)\";\n tokens[\"--tempest-border-strong\"] = \"var(--tempest-gray-400)\";\n tokens[\"--tempest-text\"] = \"var(--tempest-gray-900)\";\n tokens[\"--tempest-text-muted\"] = \"var(--tempest-gray-700)\";\n tokens[\"--tempest-text-subtle\"] = \"var(--tempest-gray-600)\";\n }\n tokens[\"--tempest-neutral-on-solid\"] = readableForeground(scale[700], \"#ffffff\", ON_SOLID_INK);\n}\n\n/**\n * Emit one status family (`--tempest-danger`, `-fg`, `-bg`, `-border`, `-solid`).\n *\n * `-fg` is the text shade over `-bg`, so it has to cross the ramp in opposite\n * directions per scheme; `-solid` stays the saturated fill used by badges.\n *\n * `-on-solid` is derived from the `-solid` this call just emitted, rather than\n * left to fall through. Falling through was the bug: the SDK's own\n * `-on-solid` values are measured against the SDK's own fills, so a brand whose\n * `danger-600` lands light kept white ink over it and nothing said so. The\n * generated neutral hit exactly that — white over a generated `gray-700` of\n * `#a8b2c6` measures 2.13:1.\n */\nfunction writeStatus(\n tokens: Record<string, string>,\n name: ThemeStatus,\n scale: ColorScale,\n scheme: \"light\" | \"dark\",\n): void {\n if (scheme === \"light\") {\n tokens[`--tempest-${name}`] = scale[700];\n tokens[`--tempest-${name}-fg`] = scale[800];\n tokens[`--tempest-${name}-bg`] = scale[50];\n tokens[`--tempest-${name}-border`] = scale[200];\n tokens[`--tempest-${name}-solid`] = scale[600];\n tokens[`--tempest-${name}-on-solid`] = readableForeground(\n scale[600],\n \"#ffffff\",\n ON_SOLID_INK,\n );\n } else {\n tokens[`--tempest-${name}`] = scale[700];\n tokens[`--tempest-${name}-fg`] = scale[700];\n tokens[`--tempest-${name}-bg`] = scale[50];\n tokens[`--tempest-${name}-border`] = scale[200];\n tokens[`--tempest-${name}-solid`] = scale[500];\n tokens[`--tempest-${name}-on-solid`] = readableForeground(\n scale[500],\n \"#ffffff\",\n ON_SOLID_INK,\n );\n }\n}\n\nfunction renderBlock(selector: string, tokens: Record<string, string>): string {\n const entries = Object.entries(tokens);\n if (entries.length === 0) return \"\";\n const body = entries.map(([name, value]) => ` ${name}: ${value};`).join(\"\\n\");\n return `${selector} {\\n${body}\\n}`;\n}\n\n/**\n * Generate `--tempest-*` overrides from a small brand description.\n *\n * Only the families you pass are generated; everything else falls through to the\n * SDK's own tokens, so a theme stays a patch and not a fork of `colors.css`.\n *\n * @example\n * ```ts\n * import { applyTheme, createTheme } from \"tempest-react-sdk\";\n *\n * const theme = createTheme({\n * primary: \"#7c3aed\",\n * radius: \"lg\",\n * chart: [\"#7c3aed\", \"#0ea5e9\", \"#22c55e\", \"#f59e0b\"],\n * });\n *\n * applyTheme(theme);\n * ```\n *\n * @param options - Brand colors plus optional radius / chart / focus-ring tuning.\n * @returns The light and dark token maps and the CSS text that carries them.\n */\nexport function createTheme(options: CreateThemeOptions = {}): GeneratedTheme {\n const {\n primary,\n gray,\n chart,\n radius,\n focusRingAlpha,\n selector = \":root\",\n darkSelector = '[data-tempest-theme=\"dark\"]',\n } = options;\n\n warnOnTranslucentFocusRing(focusRingAlpha);\n\n const light: Record<string, string> = {};\n const dark: Record<string, string> = {};\n\n // `anchor: false`: a neutral ramp has to keep its tuned lightness curve.\n // Anchoring it at the input compressed both halves and dropped\n // text-muted-on-surface-3 to ~4.2:1 — below AA — which the browser axe\n // sweep caught on the generated themes.\n const grayScales = gray\n ? {\n light: createColorScale(gray, \"light\", { anchor: false, neutral: true }),\n dark: createColorScale(gray, \"dark\", { anchor: false, neutral: true }),\n }\n : undefined;\n\n if (primary) {\n const lightScale = createColorScale(primary, \"light\");\n const darkScale = createColorScale(primary, \"dark\");\n writeScale(light, \"primary\", lightScale);\n writeScale(dark, \"primary\", darkScale);\n writePrimaryAliases(\n light,\n lightScale,\n \"light\",\n surfacesOf(\"light\", grayScales),\n focusRingAlpha,\n );\n writePrimaryAliases(\n dark,\n darkScale,\n \"dark\",\n surfacesOf(\"dark\", grayScales),\n focusRingAlpha,\n );\n }\n\n if (grayScales) {\n writeScale(light, \"gray\", grayScales.light);\n writeScale(dark, \"gray\", grayScales.dark);\n writeNeutralAliases(light, grayScales.light, \"light\");\n writeNeutralAliases(dark, grayScales.dark, \"dark\");\n }\n\n for (const status of [\"success\", \"warning\", \"danger\", \"info\"] as const) {\n const value = options[status];\n if (!value) continue;\n writeStatus(light, status, createColorScale(value, \"light\"), \"light\");\n writeStatus(dark, status, createColorScale(value, \"dark\"), \"dark\");\n }\n\n if (chart?.length) {\n chart.forEach((color, index) => {\n light[`--tempest-chart-${index + 1}`] = color;\n });\n // Declares how many series colors this theme owns. Without it the reader\n // keeps walking into the SDK's built-in `--tempest-chart-7`/`-8`, so a\n // 6-color brand palette silently mixes with two leftover defaults — which\n // is exactly what a 7-series chart would show.\n light[\"--tempest-chart-count\"] = String(chart.length);\n }\n\n /*\n * Continuous scales follow the brand too.\n *\n * Without this a rebranded app gets its categorical slots recoloured and then a\n * heatmap still painted in the SDK's blue — the one chart on the page that\n * silently ignores the brand. Both scales derive from the first series hue (or\n * the primary), stepped by perceptual lightness for each mode separately.\n *\n * Skipped entirely when the theme names no colour to derive from, so the\n * built-in tokens survive instead of being overwritten with a guess.\n */\n const scaleSource = chart?.[0] ?? primary;\n if (scaleSource) {\n const coolHue = hueOf(scaleSource);\n // The opposite pole: the brand's own danger colour when it has one, since a\n // diverging scale's warm arm and \"bad\" should not disagree on screen.\n const warmHue = options.danger ? hueOf(options.danger) : (coolHue + 180) % 360;\n for (const [target, mode] of [\n [light, \"light\"],\n [dark, \"dark\"],\n ] as const) {\n buildRamp(coolHue, mode).forEach((stepColor, index) => {\n target[`--tempest-chart-sequential-${index + 1}`] = stepColor;\n });\n const diverging = buildDivergingRamp({\n coolHue,\n warmHue,\n mid: mode === \"light\" ? \"#e4e7ec\" : \"#262d3f\",\n mode,\n });\n [...diverging.cool, diverging.mid, ...diverging.warm].forEach((stepColor, index) => {\n target[`--tempest-chart-diverging-${index + 1}`] = stepColor;\n });\n }\n }\n\n if (radius) {\n const steps = typeof radius === \"string\" ? RADIUS_PRESETS[radius] : radius;\n for (const [step, value] of Object.entries(steps)) {\n light[`--tempest-radius-${step}`] = value;\n }\n }\n\n const css = [renderBlock(selector, light), renderBlock(darkSelector, dark)]\n .filter(Boolean)\n .join(\"\\n\\n\");\n\n return { light, dark, css };\n}\n\n/**\n * Contrast ratio of `--tempest-primary-foreground` over the brand color.\n *\n * Exposed so an app (or a test) can assert its own brand clears WCAG AA (4.5) for\n * body text or AA-large (3.0) for button labels, instead of trusting the pick.\n *\n * @param options - The same input given to {@link createTheme}.\n * @returns The ratio, or `null` when no `primary` was provided.\n */\nexport function themeContrast(options: CreateThemeOptions): number | null {\n if (!options.primary) return null;\n const scale = createColorScale(options.primary, \"light\");\n return contrastRatio(scale[500], readableForeground(scale[500]));\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"create-theme.js","names":[],"sources":["../../src/theme/create-theme.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — one pass that emits the whole token set — surfaces,\n * text, borders, states, the chart ramps — for both modes, with every value checked\n * against the surface it will sit on. The checks are what make the length: each\n * token is derived and then verified against the same background table.\n */\n/**\n * Theme factory: turns a handful of brand colors into the full set of\n * `--tempest-*` token overrides, for both light and dark.\n *\n * Rebranding used to mean hand-writing ~167 custom properties (and getting the\n * dark ramp inversion right by hand). `createTheme({ primary: \"#7c3aed\" })`\n * emits the same thing, derived in OKLCH so the ramp stays perceptually even.\n */\nimport {\n contrastRatio,\n createColorScale,\n hexToRgbaString,\n readableForeground,\n type ColorScale,\n type ScaleStep,\n} from \"./color\";\nimport { buildDivergingRamp, buildRamp, hueOf } from \"./data-viz-ramps\";\nimport { isDevBuild } from \"../utils/dev-mode\";\nimport {\n INVERSE_SELECTOR,\n renderInverseSurface,\n type InverseSurfaceGenerator,\n} from \"./inverse-render\";\nimport { ON_SOLID_INK, THEME_STATUSES, writeStatus } from \"./status-tokens\";\n\n/** Radius presets, applied to the whole `--tempest-radius-*` family at once. */\nexport type ThemeRadius = \"none\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"full\";\n\nexport type { ThemeStatus } from \"./status-tokens\";\n\n/** Input for {@link createTheme}. Every field is optional — omitted families keep the built-in tokens. */\nexport interface CreateThemeOptions {\n /** Brand color, used as the `500` step of the primary scale. */\n primary?: string;\n /** Neutral color, used as the `500` step of the gray scale (surfaces, borders, text). */\n gray?: string;\n /** Success color (`--tempest-success*`). */\n success?: string;\n /** Warning color (`--tempest-warning*`). */\n warning?: string;\n /** Danger color (`--tempest-danger*`). */\n danger?: string;\n /** Info color (`--tempest-info*`). */\n info?: string;\n /**\n * Categorical series colors for `tempest-react-sdk/charts`, in cycle order.\n * Written to `--tempest-chart-1` … `--tempest-chart-N`, so charts follow the\n * theme instead of a hardcoded palette.\n */\n chart?: string[];\n /** Corner radius scale. A preset name, or explicit per-step values. */\n radius?: ThemeRadius | Partial<Record<\"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\", string>>;\n /**\n * Opacity of `--tempest-focus-ring-color`. Omit it — the default ring is opaque.\n *\n * A translucent ring has no contrast of its own; it has the contrast of\n * whatever it composites over. Passing a value below `1` reintroduces the\n * failure 0.61.0 fixed in the built-in tokens, and logs a warning in a\n * development build. Kept because a translucent ring over a background you\n * control is a legitimate choice.\n */\n focusRingAlpha?: number;\n /** Selector the light tokens are written under. Default `\":root\"`. */\n selector?: string;\n /** Selector the dark tokens are written under. Default `'[data-tempest-theme=\"dark\"]'`. */\n darkSelector?: string;\n /**\n * Also emit the inverse surface — the token set for a section painted in the\n * brand color — by passing its generator, `createInverseSurface`. Off by\n * default; ignored without `primary`.\n *\n * The generator is passed in rather than switched on with `true` so that\n * `createTheme` never imports it: an app that does not paint a section in\n * its brand does not ship the 1.68 kB brotli it costs.\n *\n * @example\n * ```ts\n * import { createInverseSurface, createTheme } from \"tempest-react-sdk\";\n *\n * createTheme({ primary: \"#03184b\", inverse: createInverseSurface });\n * ```\n */\n inverse?: InverseSurfaceGenerator;\n /** Selector the inverse tokens are written under. Default `'[data-tempest-tone=\"inverse\"]'`. */\n inverseSelector?: string;\n}\n\n/** A generated theme: token maps per color scheme, plus the CSS text that carries them. */\nexport interface GeneratedTheme {\n /** Light-scheme custom properties, without the leading `--`-less names (keys include `--`). */\n light: Record<string, string>;\n /** Dark-scheme custom properties. */\n dark: Record<string, string>;\n /**\n * Inverse-surface custom properties — literal colors, identical under the\n * light and the dark page scheme. Present only when the theme names a\n * `primary` and passes `inverse`.\n */\n inverse?: Record<string, string>;\n /** Both blocks rendered as CSS, ready for {@link applyTheme} or a stylesheet. */\n css: string;\n}\n\nconst RADIUS_PRESETS: Record<ThemeRadius, Record<string, string>> = {\n none: { xs: \"0\", sm: \"0\", md: \"0\", lg: \"0\", xl: \"0\", \"2xl\": \"0\" },\n sm: { xs: \"1px\", sm: \"2px\", md: \"4px\", lg: \"6px\", xl: \"8px\", \"2xl\": \"12px\" },\n md: { xs: \"2px\", sm: \"4px\", md: \"8px\", lg: \"12px\", xl: \"16px\", \"2xl\": \"24px\" },\n lg: { xs: \"4px\", sm: \"6px\", md: \"12px\", lg: \"16px\", xl: \"22px\", \"2xl\": \"32px\" },\n xl: { xs: \"6px\", sm: \"10px\", md: \"16px\", lg: \"24px\", xl: \"32px\", \"2xl\": \"44px\" },\n full: { xs: \"4px\", sm: \"8px\", md: \"9999px\", lg: \"9999px\", xl: \"9999px\", \"2xl\": \"9999px\" },\n};\n\nconst SCALE_STEPS: ScaleStep[] = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900];\n\nfunction writeScale(tokens: Record<string, string>, name: string, scale: ColorScale): void {\n for (const step of SCALE_STEPS) {\n tokens[`--tempest-${name}-${step}`] = scale[step];\n }\n}\n\n/** Minimum contrast for body text, WCAG 2.x AA. */\nconst AA_TEXT_CONTRAST = 4.5;\n\n/**\n * Choose the ramp step for text sitting on the soft tint.\n *\n * A fixed step does not survive an arbitrary brand color: the built-in blue only\n * reaches 4.37:1 at `500` over its own `50` tint, and a generated emerald lands\n * at 4.41:1 at `600` — both fail AA for body text by a hair. So the step is\n * picked by measuring, walking away from the tint until it clears AA, and\n * falling back to the most extreme candidate when even that cannot (a very light\n * or very desaturated brand).\n *\n * @param scale - The generated ramp.\n * @param softStep - Step used as `--tempest-primary-soft`.\n * @param candidates - Steps to try, in order of preference.\n * @returns The first candidate clearing {@link AA_TEXT_CONTRAST}, else the last.\n */\nfunction pickOnSoftStep(\n scale: ColorScale,\n softStep: ScaleStep,\n candidates: ScaleStep[],\n): ScaleStep {\n for (const step of candidates) {\n if (contrastRatio(scale[softStep], scale[step]) >= AA_TEXT_CONTRAST) return step;\n }\n return candidates[candidates.length - 1];\n}\n\n/** Minimum contrast for a non-text indicator, WCAG 2.2 SC 1.4.11. */\nconst INDICATOR_CONTRAST = 3;\n\n/**\n * The four surfaces a focus ring can land on, as the SDK's own `colors.css`\n * paints them.\n *\n * Ported from `src/styles/colors.css` (`--tempest-bg`, `--tempest-surface`,\n * `--tempest-surface-2`, `--tempest-surface-3`, resolved through the gray ramp),\n * and pinned by a test that reads that file — a theme that only names `primary`\n * keeps these, so the ring has to be measured against them and not against a\n * guess. The light values resolve through `--tempest-gray-50/100/200`; the dark\n * ones are literals in the dark block.\n */\nconst SDK_SURFACES: Record<\"light\" | \"dark\", readonly string[]> = {\n light: [\"#ffffff\", \"#f8f9fb\", \"#f1f3f6\", \"#e4e7ec\"],\n dark: [\"#0b0d12\", \"#14171f\", \"#1d2230\", \"#262d3f\"],\n};\n\n/**\n * Ramp steps to try for the focus ring, in order.\n *\n * `500` first, because the ring should read as the brand when it can, then up\n * the ramp — which moves away from the surfaces in **both** schemes, because\n * `createColorScale` inverts the dark ramp: there `50` is the darkest step (it\n * is what `--tempest-bg` is made of) and `900` the lightest. Walking down in\n * dark mode looks like the right inversion and is the wrong direction; measured,\n * it left a near-black brand's ring at 1.04:1 over its own background.\n */\nconst FOCUS_RING_CANDIDATES: readonly ScaleStep[] = [500, 600, 700, 800, 900];\n\n/**\n * Choose the ramp step for the focus ring, by measuring it against the surfaces\n * it will actually be drawn on.\n *\n * A fixed step does not survive an arbitrary brand, and the measurement says so\n * loudly: of twelve brands checked against the four surfaces in both schemes —\n * 96 pairings — `500` opaque clears 3:1 in 58 of them, and `500` at the old\n * default alpha of 0.35 in 3. Picking the step by measurement clears all 96. The\n * brand from the report that opened this\n * (`#8100D7`) passes in light at 7.20:1 and fails in **dark** at 2.70:1 down to\n * 1.91:1 on `surface-3`; a yellow fails in light at 1.43:1. So the step is\n * picked the way {@link pickOnSoftStep} picks text on a tint: walk away from the\n * surfaces until the ring clears the floor against *all* of them.\n *\n * The fallback is the candidate with the best worst case rather than the last\n * one, because \"the most extreme step\" is not always the most contrasting one\n * once a brand is near-neutral.\n *\n * @param scale - The generated ramp.\n * @param surfaces - The four surfaces this theme will paint, opaque hex.\n * @returns The first candidate clearing {@link INDICATOR_CONTRAST} on every\n * surface, else the candidate whose weakest pairing is strongest.\n */\nfunction pickFocusRingStep(scale: ColorScale, surfaces: readonly string[]): ScaleStep {\n const worstCase = (step: ScaleStep): number =>\n Math.min(...surfaces.map((surface) => contrastRatio(scale[step], surface)));\n let best: ScaleStep = FOCUS_RING_CANDIDATES[0] as ScaleStep;\n let bestRatio = -1;\n for (const step of FOCUS_RING_CANDIDATES) {\n const measured = worstCase(step);\n if (measured >= INDICATOR_CONTRAST) return step;\n if (measured > bestRatio) {\n best = step;\n bestRatio = measured;\n }\n }\n return best;\n}\n\n/**\n * The surfaces this theme will actually paint, per scheme.\n *\n * A theme that names `gray` repaints all four, so measuring the ring against the\n * SDK's would be measuring the wrong backdrop — and a theme that does not keeps\n * the SDK's, which is the common case. The order and the ramp steps mirror\n * {@link writeNeutralAliases}, which is what emits them.\n *\n * @param scheme - Which scheme is being emitted.\n * @param grayScales - The generated neutral ramps, when the theme names one.\n * @returns Four opaque hex colors: bg, surface, surface-2, surface-3.\n */\nfunction surfacesOf(\n scheme: \"light\" | \"dark\",\n grayScales: { light: ColorScale; dark: ColorScale } | undefined,\n): readonly string[] {\n if (!grayScales) return SDK_SURFACES[scheme];\n const scale = grayScales[scheme];\n return scheme === \"light\"\n ? [\"#ffffff\", scale[50], scale[100], scale[200]]\n : [scale[50], scale[100], scale[200], scale[300]];\n}\n\n/**\n * Warn, in a development build, that an alpha was asked for the focus ring.\n *\n * A translucent ring has no contrast of its own — it has the contrast of\n * whatever it composites over — and that is the failure this option used to\n * ship by default: measured at alpha `0.35`, twelve brands across four surfaces\n * and both schemes produced 96 pairings, of which 3 cleared the 3:1 of WCAG 2.2\n * SC 1.4.11. It stays available, because a translucent ring over a\n * known background is a legitimate choice, but it is a choice a theme now has to\n * make on purpose.\n *\n * The warning is the difficult part to notice otherwise: a failing focus ring is\n * visible, it just does not separate.\n *\n * @param focusRingAlpha - The opacity the caller asked for, if any.\n */\nfunction warnOnTranslucentFocusRing(focusRingAlpha: number | undefined): void {\n if (focusRingAlpha === undefined || focusRingAlpha >= 1) return;\n if (!isDevBuild()) return;\n console.warn(\n `[tempest-react-sdk] createTheme({ focusRingAlpha: ${focusRingAlpha} }) makes the focus ring ` +\n \"translucent, so its contrast becomes whatever it composites over — measured below the 3:1 \" +\n \"of WCAG 2.2 SC 1.4.11 on every surface the SDK paints. Drop the option to get the opaque \" +\n \"ring, which is derived from your brand ramp and measured against those surfaces.\",\n );\n}\n\n/**\n * Emit the primary aliases for one scheme.\n *\n * The dark scheme walks the ramp the other way (hover is *lighter*, the soft\n * tint is a dark shade, and readable text on that tint is a light shade) — the\n * same inversion the built-in `colors.css` dark block does by hand.\n *\n * The selected-state indicator is derived from the same measurement as the focus\n * ring and emitted as its own token. It never takes `focusRingAlpha`: a\n * translucent ring over a known background is a legitimate choice a theme can\n * make, while a translucent *state* indicator has the contrast of whatever it\n * composites over, which is the defect this token exists to end.\n *\n * @param tokens - The token map for this scheme, written in place.\n * @param scale - The generated primary ramp.\n * @param scheme - Which scheme is being emitted.\n * @param surfaces - The surfaces this theme paints, for the focus-ring measurement.\n * @param focusRingAlpha - Opacity asked for the ring, or `undefined` for opaque.\n */\nfunction writePrimaryAliases(\n tokens: Record<string, string>,\n scale: ColorScale,\n scheme: \"light\" | \"dark\",\n surfaces: readonly string[],\n focusRingAlpha: number | undefined,\n): void {\n tokens[\"--tempest-primary\"] = \"var(--tempest-primary-500)\";\n if (scheme === \"light\") {\n tokens[\"--tempest-primary-hover\"] = \"var(--tempest-primary-600)\";\n tokens[\"--tempest-primary-active\"] = \"var(--tempest-primary-700)\";\n tokens[\"--tempest-primary-soft\"] = \"var(--tempest-primary-50)\";\n tokens[\"--tempest-primary-soft-hover\"] = \"var(--tempest-primary-100)\";\n tokens[\"--tempest-primary-on-soft\"] =\n `var(--tempest-primary-${pickOnSoftStep(scale, 50, [600, 700, 800, 900])})`;\n } else {\n tokens[\"--tempest-primary-hover\"] = \"var(--tempest-primary-400)\";\n tokens[\"--tempest-primary-active\"] = \"var(--tempest-primary-300)\";\n tokens[\"--tempest-primary-soft\"] = \"var(--tempest-primary-100)\";\n tokens[\"--tempest-primary-soft-hover\"] = \"var(--tempest-primary-200)\";\n tokens[\"--tempest-primary-on-soft\"] =\n `var(--tempest-primary-${pickOnSoftStep(scale, 100, [700, 800, 900])})`;\n }\n\n const foreground = readableForeground(scale[500]);\n tokens[\"--tempest-primary-foreground\"] = foreground;\n tokens[\"--tempest-text-on-primary\"] = foreground;\n const ring = scale[pickFocusRingStep(scale, surfaces)];\n tokens[\"--tempest-focus-ring-color\"] =\n focusRingAlpha === undefined || focusRingAlpha >= 1\n ? ring\n : hexToRgbaString(ring, focusRingAlpha);\n tokens[\"--tempest-selected-indicator\"] = ring;\n}\n\n/**\n * Emit the neutral surface/border/text aliases from a generated gray scale.\n *\n * `--tempest-bg` is pushed past the ramp on purpose: pure white in light mode and\n * a shade darker than `gray-50` in dark mode, so a raised surface still reads as\n * raised against the page.\n */\nfunction writeNeutralAliases(\n tokens: Record<string, string>,\n scale: ColorScale,\n scheme: \"light\" | \"dark\",\n): void {\n if (scheme === \"light\") {\n tokens[\"--tempest-bg\"] = \"#ffffff\";\n tokens[\"--tempest-surface\"] = \"var(--tempest-gray-50)\";\n tokens[\"--tempest-surface-2\"] = \"var(--tempest-gray-100)\";\n tokens[\"--tempest-surface-3\"] = \"var(--tempest-gray-200)\";\n tokens[\"--tempest-border\"] = \"var(--tempest-gray-200)\";\n tokens[\"--tempest-border-strong\"] = \"var(--tempest-gray-300)\";\n tokens[\"--tempest-text\"] = \"var(--tempest-gray-900)\";\n tokens[\"--tempest-text-muted\"] = \"var(--tempest-gray-600)\";\n tokens[\"--tempest-text-subtle\"] = \"var(--tempest-gray-500)\";\n } else {\n tokens[\"--tempest-bg\"] = scale[50];\n tokens[\"--tempest-surface\"] = \"var(--tempest-gray-100)\";\n tokens[\"--tempest-surface-2\"] = \"var(--tempest-gray-200)\";\n tokens[\"--tempest-surface-3\"] = \"var(--tempest-gray-300)\";\n tokens[\"--tempest-border\"] = \"var(--tempest-gray-300)\";\n tokens[\"--tempest-border-strong\"] = \"var(--tempest-gray-400)\";\n tokens[\"--tempest-text\"] = \"var(--tempest-gray-900)\";\n tokens[\"--tempest-text-muted\"] = \"var(--tempest-gray-700)\";\n tokens[\"--tempest-text-subtle\"] = \"var(--tempest-gray-600)\";\n }\n tokens[\"--tempest-neutral-on-solid\"] = readableForeground(scale[700], \"#ffffff\", ON_SOLID_INK);\n}\n\nfunction renderBlock(selector: string, tokens: Record<string, string>): string {\n const entries = Object.entries(tokens);\n if (entries.length === 0) return \"\";\n const body = entries.map(([name, value]) => ` ${name}: ${value};`).join(\"\\n\");\n return `${selector} {\\n${body}\\n}`;\n}\n\n/**\n * Generate `--tempest-*` overrides from a small brand description.\n *\n * Only the families you pass are generated; everything else falls through to the\n * SDK's own tokens, so a theme stays a patch and not a fork of `colors.css`.\n *\n * @example\n * ```ts\n * import { applyTheme, createTheme } from \"tempest-react-sdk\";\n *\n * const theme = createTheme({\n * primary: \"#7c3aed\",\n * radius: \"lg\",\n * chart: [\"#7c3aed\", \"#0ea5e9\", \"#22c55e\", \"#f59e0b\"],\n * });\n *\n * applyTheme(theme);\n * ```\n *\n * @param options - Brand colors plus optional radius / chart / focus-ring tuning.\n * @returns The light and dark token maps and the CSS text that carries them.\n */\nexport function createTheme(options: CreateThemeOptions = {}): GeneratedTheme {\n const {\n primary,\n gray,\n chart,\n radius,\n focusRingAlpha,\n selector = \":root\",\n darkSelector = '[data-tempest-theme=\"dark\"]',\n inverse,\n inverseSelector = INVERSE_SELECTOR,\n } = options;\n\n warnOnTranslucentFocusRing(focusRingAlpha);\n\n const light: Record<string, string> = {};\n const dark: Record<string, string> = {};\n\n // `anchor: false`: a neutral ramp has to keep its tuned lightness curve.\n // Anchoring it at the input compressed both halves and dropped\n // text-muted-on-surface-3 to ~4.2:1 — below AA — which the browser axe\n // sweep caught on the generated themes.\n const grayScales = gray\n ? {\n light: createColorScale(gray, \"light\", { anchor: false, neutral: true }),\n dark: createColorScale(gray, \"dark\", { anchor: false, neutral: true }),\n }\n : undefined;\n\n if (primary) {\n const lightScale = createColorScale(primary, \"light\");\n const darkScale = createColorScale(primary, \"dark\");\n writeScale(light, \"primary\", lightScale);\n writeScale(dark, \"primary\", darkScale);\n writePrimaryAliases(\n light,\n lightScale,\n \"light\",\n surfacesOf(\"light\", grayScales),\n focusRingAlpha,\n );\n writePrimaryAliases(\n dark,\n darkScale,\n \"dark\",\n surfacesOf(\"dark\", grayScales),\n focusRingAlpha,\n );\n }\n\n if (grayScales) {\n writeScale(light, \"gray\", grayScales.light);\n writeScale(dark, \"gray\", grayScales.dark);\n writeNeutralAliases(light, grayScales.light, \"light\");\n writeNeutralAliases(dark, grayScales.dark, \"dark\");\n }\n\n for (const status of THEME_STATUSES) {\n const value = options[status];\n if (!value) continue;\n writeStatus(light, status, createColorScale(value, \"light\"), \"light\");\n writeStatus(dark, status, createColorScale(value, \"dark\"), \"dark\");\n }\n\n if (chart?.length) {\n chart.forEach((color, index) => {\n light[`--tempest-chart-${index + 1}`] = color;\n });\n // Declares how many series colors this theme owns. Without it the reader\n // keeps walking into the SDK's built-in `--tempest-chart-7`/`-8`, so a\n // 6-color brand palette silently mixes with two leftover defaults — which\n // is exactly what a 7-series chart would show.\n light[\"--tempest-chart-count\"] = String(chart.length);\n }\n\n /*\n * Continuous scales follow the brand too.\n *\n * Without this a rebranded app gets its categorical slots recoloured and then a\n * heatmap still painted in the SDK's blue — the one chart on the page that\n * silently ignores the brand. Both scales derive from the first series hue (or\n * the primary), stepped by perceptual lightness for each mode separately.\n *\n * Skipped entirely when the theme names no colour to derive from, so the\n * built-in tokens survive instead of being overwritten with a guess.\n */\n const scaleSource = chart?.[0] ?? primary;\n if (scaleSource) {\n const coolHue = hueOf(scaleSource);\n // The opposite pole: the brand's own danger colour when it has one, since a\n // diverging scale's warm arm and \"bad\" should not disagree on screen.\n const warmHue = options.danger ? hueOf(options.danger) : (coolHue + 180) % 360;\n for (const [target, mode] of [\n [light, \"light\"],\n [dark, \"dark\"],\n ] as const) {\n buildRamp(coolHue, mode).forEach((stepColor, index) => {\n target[`--tempest-chart-sequential-${index + 1}`] = stepColor;\n });\n const diverging = buildDivergingRamp({\n coolHue,\n warmHue,\n mid: mode === \"light\" ? \"#e4e7ec\" : \"#262d3f\",\n mode,\n });\n [...diverging.cool, diverging.mid, ...diverging.warm].forEach((stepColor, index) => {\n target[`--tempest-chart-diverging-${index + 1}`] = stepColor;\n });\n }\n }\n\n if (radius) {\n const steps = typeof radius === \"string\" ? RADIUS_PRESETS[radius] : radius;\n for (const [step, value] of Object.entries(steps)) {\n light[`--tempest-radius-${step}`] = value;\n }\n }\n\n const inverseSurface =\n primary && inverse\n ? inverse({\n primary,\n statuses: {\n success: options.success,\n warning: options.warning,\n danger: options.danger,\n info: options.info,\n },\n chart,\n })\n : null;\n\n const css = [\n renderBlock(selector, light),\n renderBlock(darkSelector, dark),\n inverseSurface ? renderInverseSurface(inverseSelector, inverseSurface) : \"\",\n ]\n .filter(Boolean)\n .join(\"\\n\\n\");\n\n return inverseSurface\n ? { light, dark, inverse: inverseSurface.tokens, css }\n : { light, dark, css };\n}\n\n/**\n * Contrast ratio of `--tempest-primary-foreground` over the brand color.\n *\n * Exposed so an app (or a test) can assert its own brand clears WCAG AA (4.5) for\n * body text or AA-large (3.0) for button labels, instead of trusting the pick.\n *\n * @param options - The same input given to {@link createTheme}.\n * @returns The ratio, or `null` when no `primary` was provided.\n */\nexport function themeContrast(options: CreateThemeOptions): number | null {\n if (!options.primary) return null;\n const scale = createColorScale(options.primary, \"light\");\n return contrastRatio(scale[500], readableForeground(scale[500]));\n}\n"],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ var e=`[data-tempest-tone="inverse"]`;function t(e,t){let n=Object.entries(t.tokens).map(([e,t])=>` ${e}: ${t};`).join(`
2
+ `);return`${e} {\n color-scheme: ${t.scheme};\n${n}\n}\n\n:where(${e}) {\n background-color: var(--tempest-bg);\n color: var(--tempest-text);\n}`}exports.INVERSE_SELECTOR=e,exports.renderInverseSurface=t;
3
+ //# sourceMappingURL=inverse-render.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inverse-render.cjs","names":[],"sources":["../../src/theme/inverse-render.ts"],"sourcesContent":["/**\n * The small, always-loaded half of the inverse surface: its types, its default\n * selector and the CSS renderer. The generator lives in `inverse-surface.ts`,\n * which only an app that asks for it imports.\n */\nimport type { ScaleStep } from \"./color\";\nimport type { ThemeStatus } from \"./status-tokens\";\n\n/** Default selector the inverse tokens are written under. */\nexport const INVERSE_SELECTOR = '[data-tempest-tone=\"inverse\"]';\n\n/** Which way the inverse surface reads: light ink on a dark fill, or the reverse. */\nexport type InverseScheme = \"light\" | \"dark\";\n\n/** The inverse surface for one brand. */\nexport interface InverseSurface {\n /** Every color custom property the surface redefines, as literal colors. */\n tokens: Record<string, string>;\n /**\n * `\"dark\"` when the fill carries light ink (a navy brand), `\"light\"` when it\n * carries dark ink (a yellow brand). Written as `color-scheme`, so native\n * controls inside the section follow the fill rather than the page.\n */\n scheme: InverseScheme;\n /** Step of the brand's light ramp used as the fill. */\n step: ScaleStep;\n}\n\n/** Input for an {@link InverseSurfaceGenerator} — `createInverseSurface`. */\nexport interface InverseSurfaceOptions {\n /** Brand color — the fill is picked from its ramp. */\n primary: string;\n /**\n * Status colors the theme names; the rest use the SDK's defaults. A named\n * `danger` is also the warm pole of the diverging chart scale, as on the page.\n */\n statuses?: Partial<Record<ThemeStatus, string>>;\n /** Categorical chart colors the theme names; else the SDK's defaults. */\n chart?: readonly string[];\n}\n\n/**\n * The inverse-surface generator `createTheme` calls when it is handed one —\n * in practice `createInverseSurface`.\n *\n * A function rather than a flag on purpose: `createTheme` never imports the\n * generator, so an app that does not paint a section in its brand does not ship\n * it. Measured with `npx size-limit`, `{ createTheme }` is 3.18 kB brotli on its\n * own and 4.86 kB with the generator inside it — what a boolean option would\n * have charged every theme.\n */\nexport type InverseSurfaceGenerator = (options: InverseSurfaceOptions) => InverseSurface;\n\n/**\n * Render the inverse surface as CSS: the token block, plus a zero-specificity\n * rule that paints the fill and the ink on the element itself.\n *\n * The paint rule is what makes the attribute a complete shortcut — without it a\n * `<section data-tempest-tone=\"inverse\">` would get inverted tokens and keep the\n * page's background, which is white text on white. It goes through `:where()`\n * so any background the app sets on the same element wins without a fight.\n *\n * @param selector - Selector the surface is scoped to.\n * @param surface - The surface an {@link InverseSurfaceGenerator} returned.\n * @returns The CSS text.\n */\nexport function renderInverseSurface(selector: string, surface: InverseSurface): string {\n const body = Object.entries(surface.tokens)\n .map(([name, value]) => ` ${name}: ${value};`)\n .join(\"\\n\");\n return (\n `${selector} {\\n color-scheme: ${surface.scheme};\\n${body}\\n}\\n\\n` +\n `:where(${selector}) {\\n background-color: var(--tempest-bg);\\n color: var(--tempest-text);\\n}`\n );\n}\n"],"mappings":"AASA,IAAa,EAAmB,gCAyDhC,SAAgB,EAAqB,EAAkB,EAAiC,CACpF,IAAM,EAAO,OAAO,QAAQ,EAAQ,MAAM,CAAC,CACtC,KAAK,CAAC,EAAM,KAAW,OAAO,EAAK,IAAI,EAAM,EAAE,CAAC,CAChD,KAAK;CAAI,EACd,MACI,GAAG,EAAS,wBAAwB,EAAQ,OAAO,KAAK,EAAK,gBACnD,EAAS,kFAE3B"}
@@ -0,0 +1,10 @@
1
+ //#region src/theme/inverse-render.ts
2
+ var e = "[data-tempest-tone=\"inverse\"]";
3
+ function t(e, t) {
4
+ let n = Object.entries(t.tokens).map(([e, t]) => ` ${e}: ${t};`).join("\n");
5
+ return `${e} {\n color-scheme: ${t.scheme};\n${n}\n}\n\n:where(${e}) {\n background-color: var(--tempest-bg);\n color: var(--tempest-text);\n}`;
6
+ }
7
+ //#endregion
8
+ export { e as INVERSE_SELECTOR, t as renderInverseSurface };
9
+
10
+ //# sourceMappingURL=inverse-render.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inverse-render.js","names":[],"sources":["../../src/theme/inverse-render.ts"],"sourcesContent":["/**\n * The small, always-loaded half of the inverse surface: its types, its default\n * selector and the CSS renderer. The generator lives in `inverse-surface.ts`,\n * which only an app that asks for it imports.\n */\nimport type { ScaleStep } from \"./color\";\nimport type { ThemeStatus } from \"./status-tokens\";\n\n/** Default selector the inverse tokens are written under. */\nexport const INVERSE_SELECTOR = '[data-tempest-tone=\"inverse\"]';\n\n/** Which way the inverse surface reads: light ink on a dark fill, or the reverse. */\nexport type InverseScheme = \"light\" | \"dark\";\n\n/** The inverse surface for one brand. */\nexport interface InverseSurface {\n /** Every color custom property the surface redefines, as literal colors. */\n tokens: Record<string, string>;\n /**\n * `\"dark\"` when the fill carries light ink (a navy brand), `\"light\"` when it\n * carries dark ink (a yellow brand). Written as `color-scheme`, so native\n * controls inside the section follow the fill rather than the page.\n */\n scheme: InverseScheme;\n /** Step of the brand's light ramp used as the fill. */\n step: ScaleStep;\n}\n\n/** Input for an {@link InverseSurfaceGenerator} — `createInverseSurface`. */\nexport interface InverseSurfaceOptions {\n /** Brand color — the fill is picked from its ramp. */\n primary: string;\n /**\n * Status colors the theme names; the rest use the SDK's defaults. A named\n * `danger` is also the warm pole of the diverging chart scale, as on the page.\n */\n statuses?: Partial<Record<ThemeStatus, string>>;\n /** Categorical chart colors the theme names; else the SDK's defaults. */\n chart?: readonly string[];\n}\n\n/**\n * The inverse-surface generator `createTheme` calls when it is handed one —\n * in practice `createInverseSurface`.\n *\n * A function rather than a flag on purpose: `createTheme` never imports the\n * generator, so an app that does not paint a section in its brand does not ship\n * it. Measured with `npx size-limit`, `{ createTheme }` is 3.18 kB brotli on its\n * own and 4.86 kB with the generator inside it — what a boolean option would\n * have charged every theme.\n */\nexport type InverseSurfaceGenerator = (options: InverseSurfaceOptions) => InverseSurface;\n\n/**\n * Render the inverse surface as CSS: the token block, plus a zero-specificity\n * rule that paints the fill and the ink on the element itself.\n *\n * The paint rule is what makes the attribute a complete shortcut — without it a\n * `<section data-tempest-tone=\"inverse\">` would get inverted tokens and keep the\n * page's background, which is white text on white. It goes through `:where()`\n * so any background the app sets on the same element wins without a fight.\n *\n * @param selector - Selector the surface is scoped to.\n * @param surface - The surface an {@link InverseSurfaceGenerator} returned.\n * @returns The CSS text.\n */\nexport function renderInverseSurface(selector: string, surface: InverseSurface): string {\n const body = Object.entries(surface.tokens)\n .map(([name, value]) => ` ${name}: ${value};`)\n .join(\"\\n\");\n return (\n `${selector} {\\n color-scheme: ${surface.scheme};\\n${body}\\n}\\n\\n` +\n `:where(${selector}) {\\n background-color: var(--tempest-bg);\\n color: var(--tempest-text);\\n}`\n );\n}\n"],"mappings":";AASA,IAAa,IAAmB;AAyDhC,SAAgB,EAAqB,GAAkB,GAAiC;CACpF,IAAM,IAAO,OAAO,QAAQ,EAAQ,MAAM,CAAC,CACtC,KAAK,CAAC,GAAM,OAAW,OAAO,EAAK,IAAI,EAAM,EAAE,CAAC,CAChD,KAAK,IAAI;CACd,OACI,GAAG,EAAS,wBAAwB,EAAQ,OAAO,KAAK,EAAK,gBACnD,EAAS;AAE3B"}