tempest-react-sdk 0.64.0 → 0.65.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 (97) hide show
  1. package/README.md +4 -4
  2. package/dist/components/Command/Command.module.cjs.map +1 -1
  3. package/dist/components/Command/Command.module.js.map +1 -1
  4. package/dist/components/ContextMenu/ContextMenu.module.cjs.map +1 -1
  5. package/dist/components/ContextMenu/ContextMenu.module.js.map +1 -1
  6. package/dist/components/DataTable/DataTable.cjs +1 -1
  7. package/dist/components/DataTable/DataTable.cjs.map +1 -1
  8. package/dist/components/DataTable/DataTable.js +1 -0
  9. package/dist/components/DataTable/DataTable.js.map +1 -1
  10. package/dist/components/DataTable/use-dev-warnings.cjs +1 -1
  11. package/dist/components/DataTable/use-dev-warnings.cjs.map +1 -1
  12. package/dist/components/DataTable/use-dev-warnings.js +4 -3
  13. package/dist/components/DataTable/use-dev-warnings.js.map +1 -1
  14. package/dist/components/DropdownMenu/DropdownMenu.module.cjs.map +1 -1
  15. package/dist/components/DropdownMenu/DropdownMenu.module.js.map +1 -1
  16. package/dist/components/InstallBanner/InstallBanner.cjs +1 -1
  17. package/dist/components/InstallBanner/InstallBanner.cjs.map +1 -1
  18. package/dist/components/InstallBanner/InstallBanner.js +62 -37
  19. package/dist/components/InstallBanner/InstallBanner.js.map +1 -1
  20. package/dist/components/InstallBanner/InstallBanner.module.cjs +1 -1
  21. package/dist/components/InstallBanner/InstallBanner.module.cjs.map +1 -1
  22. package/dist/components/InstallBanner/InstallBanner.module.js +4 -3
  23. package/dist/components/InstallBanner/InstallBanner.module.js.map +1 -1
  24. package/dist/components/InstallButton/InstallButton.cjs +1 -1
  25. package/dist/components/InstallButton/InstallButton.cjs.map +1 -1
  26. package/dist/components/InstallButton/InstallButton.js +34 -10
  27. package/dist/components/InstallButton/InstallButton.js.map +1 -1
  28. package/dist/components/InstallButton/InstallButton.module.cjs +3 -0
  29. package/dist/components/InstallButton/InstallButton.module.cjs.map +1 -0
  30. package/dist/components/InstallButton/InstallButton.module.js +10 -0
  31. package/dist/components/InstallButton/InstallButton.module.js.map +1 -0
  32. package/dist/components/InstallButton/install-hints.cjs +2 -0
  33. package/dist/components/InstallButton/install-hints.cjs.map +1 -0
  34. package/dist/components/InstallButton/install-hints.js +32 -0
  35. package/dist/components/InstallButton/install-hints.js.map +1 -0
  36. package/dist/components/SegmentedControl/SegmentedControl.module.cjs.map +1 -1
  37. package/dist/components/SegmentedControl/SegmentedControl.module.js.map +1 -1
  38. package/dist/components/Select/Select.cjs +1 -1
  39. package/dist/components/Select/Select.cjs.map +1 -1
  40. package/dist/components/Select/Select.js +43 -40
  41. package/dist/components/Select/Select.js.map +1 -1
  42. package/dist/components/Select/Select.module.cjs +1 -1
  43. package/dist/components/Select/Select.module.cjs.map +1 -1
  44. package/dist/components/Select/Select.module.js +5 -3
  45. package/dist/components/Select/Select.module.js.map +1 -1
  46. package/dist/hooks/use-install-prompt.cjs +1 -1
  47. package/dist/hooks/use-install-prompt.cjs.map +1 -1
  48. package/dist/hooks/use-install-prompt.js +9 -2
  49. package/dist/hooks/use-install-prompt.js.map +1 -1
  50. package/dist/http/api-client.cjs +1 -1
  51. package/dist/http/api-client.cjs.map +1 -1
  52. package/dist/http/api-client.js +80 -74
  53. package/dist/http/api-client.js.map +1 -1
  54. package/dist/http/decode-response.cjs +2 -0
  55. package/dist/http/decode-response.cjs.map +1 -0
  56. package/dist/http/decode-response.js +28 -0
  57. package/dist/http/decode-response.js.map +1 -0
  58. package/dist/styles/InstallButton.css +2 -0
  59. package/dist/styles/actions.css +6 -1
  60. package/dist/styles/advanced.css +2 -2
  61. package/dist/styles/component/Command.css +1 -1
  62. package/dist/styles/component/ContextMenu.css +1 -1
  63. package/dist/styles/component/DropdownMenu.css +1 -1
  64. package/dist/styles/component/InstallBanner.css +2 -0
  65. package/dist/styles/component/InstallButton.css +4 -0
  66. package/dist/styles/component/SegmentedControl.css +1 -1
  67. package/dist/styles/component/Select.css +4 -0
  68. package/dist/styles/core.css +2 -2
  69. package/dist/styles/forms.css +4 -0
  70. package/dist/styles/manifest.json +2 -1
  71. package/dist/styles/navigation.css +1 -1
  72. package/dist/styles/tokens.css +2 -2
  73. package/dist/styles.css +1 -1
  74. package/dist/tempest-react-sdk.cjs +1 -1
  75. package/dist/tempest-react-sdk.d.ts +258 -26
  76. package/dist/tempest-react-sdk.js +319 -318
  77. package/dist/theme/ThemeProvider.cjs +1 -1
  78. package/dist/theme/ThemeProvider.cjs.map +1 -1
  79. package/dist/theme/ThemeProvider.js +40 -45
  80. package/dist/theme/ThemeProvider.js.map +1 -1
  81. package/dist/theme/create-theme.cjs +1 -1
  82. package/dist/theme/create-theme.cjs.map +1 -1
  83. package/dist/theme/create-theme.js +1 -1
  84. package/dist/theme/create-theme.js.map +1 -1
  85. package/dist/theme/initial-theme.cjs +2 -2
  86. package/dist/theme/initial-theme.cjs.map +1 -1
  87. package/dist/theme/initial-theme.js +7 -12
  88. package/dist/theme/initial-theme.js.map +1 -1
  89. package/dist/utils/format.cjs +1 -1
  90. package/dist/utils/format.cjs.map +1 -1
  91. package/dist/utils/format.js +5 -3
  92. package/dist/utils/format.js.map +1 -1
  93. package/dist/utils/storage.cjs +1 -1
  94. package/dist/utils/storage.cjs.map +1 -1
  95. package/dist/utils/storage.js +13 -0
  96. package/dist/utils/storage.js.map +1 -1
  97. package/package.json +1 -1
@@ -1,2 +1,2 @@
1
- const e=require("../hooks/use-latest-ref.cjs");let t=require("react"),n=require("react/jsx-runtime");var r=(0,t.createContext)(null);function i(e){return e===`dark`||e===`light`?e:typeof window>`u`?`light`:window.matchMedia(`(prefers-color-scheme: dark)`).matches?`dark`:`light`}function a(e,t,n,r){let i=Array.isArray(n)?n:[n];for(let n of i)e.setAttribute(n,t);if(r&&typeof document<`u`){let e=document.querySelector(`meta[name="theme-color"]`);e&&(e.content=r[t])}}function o(e){if(!e||typeof window>`u`)return null;try{let t=window.localStorage.getItem(e);return t===`light`||t===`dark`||t===`system`?t:null}catch{return null}}function s({children:s,defaultTheme:c=`system`,storageKey:l=`tempest-theme`,target:u,attribute:d=`data-tempest-theme`,themeColor:f}){let[p,m]=(0,t.useState)(()=>o(l)??c),[h,g]=(0,t.useState)(()=>i(p)),_=e.useLatestRef(u),v=Array.isArray(d)?d.join(`,`):d,y=e.useLatestRef(f);(0,t.useEffect)(()=>{let e=_.current?.()??document.documentElement;if(!e)return;let t=i(p);a(e,t,d,y.current),g(t)},[p,v]),(0,t.useEffect)(()=>{if(p!==`system`||typeof window>`u`)return;let e=window.matchMedia(`(prefers-color-scheme: dark)`),t=()=>{let t=_.current?.()??document.documentElement;if(!t)return;let n=e.matches?`dark`:`light`;a(t,n,d,y.current),g(n)};return e.addEventListener(`change`,t),()=>e.removeEventListener(`change`,t)},[p,v]);let b=(0,t.useCallback)(e=>{if(m(e),l&&typeof window<`u`)try{window.localStorage.setItem(l,e)}catch{}},[l]),x=(0,t.useCallback)(()=>{b(h===`dark`?`light`:`dark`)},[h,b]),S=(0,t.useMemo)(()=>({theme:p,resolvedTheme:h,setTheme:b,toggle:x}),[p,h,b,x]);return(0,n.jsx)(r.Provider,{value:S,children:s})}function c(){let e=(0,t.useContext)(r);if(!e)throw Error(`useTheme must be used inside a <ThemeProvider>`);return e}exports.ThemeProvider=s,exports.useTheme=c;
1
+ const e=require("../hooks/use-latest-ref.cjs"),t=require("../utils/storage.cjs");let n=require("react"),r=require("react/jsx-runtime");var i=(0,n.createContext)(null);function a(e){return e===`dark`||e===`light`?e:typeof window>`u`?`light`:window.matchMedia(`(prefers-color-scheme: dark)`).matches?`dark`:`light`}function o(e,t,n,r){let i=Array.isArray(n)?n:[n];for(let n of i)e.setAttribute(n,t);if(r&&typeof document<`u`){let e=document.querySelector(`meta[name="theme-color"]`);e&&(e.content=r[t])}}function s(e){if(!e)return null;let n=t.storage.getRaw(e);return n===`light`||n===`dark`||n===`system`?n:null}function c({children:c,defaultTheme:l=`system`,storageKey:u=`tempest-theme`,target:d,attribute:f=`data-tempest-theme`,themeColor:p}){let[m,h]=(0,n.useState)(()=>s(u)??l),[g,_]=(0,n.useState)(()=>a(m)),v=e.useLatestRef(d),y=Array.isArray(f)?f.join(`,`):f,b=e.useLatestRef(p);(0,n.useEffect)(()=>{let e=v.current?.()??document.documentElement;if(!e)return;let t=a(m);o(e,t,f,b.current),_(t)},[m,y]),(0,n.useEffect)(()=>{if(m!==`system`||typeof window>`u`)return;let e=window.matchMedia(`(prefers-color-scheme: dark)`),t=()=>{let t=v.current?.()??document.documentElement;if(!t)return;let n=e.matches?`dark`:`light`;o(t,n,f,b.current),_(n)};return e.addEventListener(`change`,t),()=>e.removeEventListener(`change`,t)},[m,y]);let x=(0,n.useCallback)(e=>{h(e),u&&t.storage.setRaw(u,e)},[u]),S=(0,n.useCallback)(()=>{x(g===`dark`?`light`:`dark`)},[g,x]),C=(0,n.useMemo)(()=>({theme:m,resolvedTheme:g,setTheme:x,toggle:S}),[m,g,x,S]);return(0,r.jsx)(i.Provider,{value:C,children:c})}function l(){let e=(0,n.useContext)(i);if(!e)throw Error(`useTheme must be used inside a <ThemeProvider>`);return e}exports.ThemeProvider=c,exports.useTheme=l;
2
2
  //# sourceMappingURL=ThemeProvider.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"ThemeProvider.cjs","names":[],"sources":["../../src/theme/ThemeProvider.tsx"],"sourcesContent":["import { createContext, useCallback, useContext, useEffect, useMemo, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { useLatestRef } from \"@/hooks/use-latest-ref\";\nimport type { ResolvedTheme, ThemeMode } from \"./types\";\n\nexport interface ThemeContextValue {\n /** Raw user preference (light / dark / system). */\n theme: ThemeMode;\n /** Effective theme actually applied to the DOM (light or dark). */\n resolvedTheme: ResolvedTheme;\n /** Update the preference. Persisted to localStorage when `storageKey` is set. */\n setTheme: (next: ThemeMode) => void;\n /** Convenience: flip light ↔ dark. When in `system` mode, switches to the opposite of the current resolved theme. */\n toggle: () => void;\n}\n\nconst ThemeContext = createContext<ThemeContextValue | null>(null);\n\nexport interface ThemeProviderProps {\n children: ReactNode;\n /** Initial preference when nothing is stored. Default: `\"system\"`. */\n defaultTheme?: ThemeMode;\n /** localStorage key used to persist the preference. Pass `null` to disable persistence. Default: `\"tempest-theme\"`. */\n storageKey?: string | null;\n /**\n * Element that receives the theme attribute(s). Defaults to\n * `document.documentElement`. Override when scoping the theme to a subtree.\n */\n target?: () => HTMLElement | null;\n /**\n * Attribute name(s) written on the target with the resolved theme\n * (`\"light\"` / `\"dark\"`). Default: `\"data-tempest-theme\"`.\n *\n * Pass an array to mirror the theme onto more than one attribute — handy\n * when the SDK components read `data-tempest-theme` but the host app's own\n * CSS keys off a different attribute (e.g. `[\"data-tempest-theme\",\n * \"data-theme\"]`). Avoids a separate sync effect in the consumer.\n */\n attribute?: string | string[];\n /**\n * When set, keeps `<meta name=\"theme-color\">` in sync with the resolved\n * theme — `content` becomes `themeColor.dark` in dark mode and\n * `themeColor.light` in light mode. The meta tag must already exist in the\n * document `<head>`. No-op when omitted.\n */\n themeColor?: { light: string; dark: string };\n}\n\nfunction resolve(mode: ThemeMode): ResolvedTheme {\n if (mode === \"dark\" || mode === \"light\") return mode;\n if (typeof window === \"undefined\") return \"light\";\n return window.matchMedia(\"(prefers-color-scheme: dark)\").matches ? \"dark\" : \"light\";\n}\n\n/**\n * Write the resolved theme onto every configured attribute and, when a\n * `themeColor` map is provided, sync the `<meta name=\"theme-color\">` tag.\n */\nfunction applyResolved(\n element: HTMLElement,\n resolved: ResolvedTheme,\n attribute: string | string[],\n themeColor?: { light: string; dark: string },\n): void {\n const attrs = Array.isArray(attribute) ? attribute : [attribute];\n for (const attr of attrs) element.setAttribute(attr, resolved);\n if (themeColor && typeof document !== \"undefined\") {\n const meta = document.querySelector<HTMLMetaElement>('meta[name=\"theme-color\"]');\n if (meta) meta.content = themeColor[resolved];\n }\n}\n\nfunction readStored(storageKey: string | null): ThemeMode | null {\n if (!storageKey || typeof window === \"undefined\") return null;\n try {\n const value = window.localStorage.getItem(storageKey);\n if (value === \"light\" || value === \"dark\" || value === \"system\") return value;\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * Wire dark/light theming. Writes a data attribute on a target element (the\n * `<html>` element by default) and exposes the current preference via\n * {@link useTheme}.\n *\n * Pair with `themeInitScript()` in the HTML head to prevent the flash of\n * incorrect theme on first paint.\n *\n * @tempest-limits empty-catch — the theme is applied to the DOM before it is\n * persisted, so a `localStorage` write refused by quota or private mode leaves the\n * user with the theme they just picked and only forfeits it on the next load.\n * Throwing from the setter would break the switch that already worked.\n */\nexport function ThemeProvider({\n children,\n defaultTheme = \"system\",\n storageKey = \"tempest-theme\",\n target,\n attribute = \"data-tempest-theme\",\n themeColor,\n}: ThemeProviderProps) {\n const [theme, setThemeState] = useState<ThemeMode>(\n () => readStored(storageKey) ?? defaultTheme,\n );\n const [resolvedTheme, setResolvedTheme] = useState<ResolvedTheme>(() => resolve(theme));\n\n const targetRef = useLatestRef(target);\n\n const attributeKey = Array.isArray(attribute) ? attribute.join(\",\") : attribute;\n const themeColorRef = useLatestRef(themeColor);\n\n useEffect(() => {\n const element = targetRef.current?.() ?? document.documentElement;\n if (!element) return;\n const next = resolve(theme);\n applyResolved(element, next, attribute, themeColorRef.current);\n setResolvedTheme(next);\n // attributeKey is the stable string form of `attribute`.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [theme, attributeKey]);\n\n useEffect(() => {\n if (theme !== \"system\" || typeof window === \"undefined\") return;\n const list = window.matchMedia(\"(prefers-color-scheme: dark)\");\n const handler = (): void => {\n const element = targetRef.current?.() ?? document.documentElement;\n if (!element) return;\n const next: ResolvedTheme = list.matches ? \"dark\" : \"light\";\n applyResolved(element, next, attribute, themeColorRef.current);\n setResolvedTheme(next);\n };\n list.addEventListener(\"change\", handler);\n return () => list.removeEventListener(\"change\", handler);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [theme, attributeKey]);\n\n const setTheme = useCallback(\n (next: ThemeMode) => {\n setThemeState(next);\n if (storageKey && typeof window !== \"undefined\") {\n try {\n window.localStorage.setItem(storageKey, next);\n } catch {\n /* empty */\n }\n }\n },\n [storageKey],\n );\n\n const toggle = useCallback(() => {\n setTheme(resolvedTheme === \"dark\" ? \"light\" : \"dark\");\n }, [resolvedTheme, setTheme]);\n\n const value = useMemo<ThemeContextValue>(\n () => ({ theme, resolvedTheme, setTheme, toggle }),\n [theme, resolvedTheme, setTheme, toggle],\n );\n\n return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;\n}\n\n/**\n * Read and mutate the current theme. Must be used inside a {@link ThemeProvider}.\n */\nexport function useTheme(): ThemeContextValue {\n const ctx = useContext(ThemeContext);\n if (!ctx) throw new Error(\"useTheme must be used inside a <ThemeProvider>\");\n return ctx;\n}\n"],"mappings":"qGAgBA,IAAM,GAAA,EAAe,EAAA,cAAA,CAAwC,IAAI,EAgCjE,SAAS,EAAQ,EAAgC,CAG7C,OAFI,IAAS,QAAU,IAAS,QAAgB,EAC5C,OAAO,OAAW,IAAoB,QACnC,OAAO,WAAW,8BAA8B,CAAC,CAAC,QAAU,OAAS,OAChF,CAMA,SAAS,EACL,EACA,EACA,EACA,EACI,CACJ,IAAM,EAAQ,MAAM,QAAQ,CAAS,EAAI,EAAY,CAAC,CAAS,EAC/D,IAAK,IAAM,KAAQ,EAAO,EAAQ,aAAa,EAAM,CAAQ,EAC7D,GAAI,GAAc,OAAO,SAAa,IAAa,CAC/C,IAAM,EAAO,SAAS,cAA+B,0BAA0B,EAC3E,IAAM,EAAK,QAAU,EAAW,GACxC,CACJ,CAEA,SAAS,EAAW,EAA6C,CAC7D,GAAI,CAAC,GAAc,OAAO,OAAW,IAAa,OAAO,KACzD,GAAI,CACA,IAAM,EAAQ,OAAO,aAAa,QAAQ,CAAU,EAEpD,OADI,IAAU,SAAW,IAAU,QAAU,IAAU,SAAiB,EACjE,IACX,MAAQ,CACJ,OAAO,IACX,CACJ,CAeA,SAAgB,EAAc,CAC1B,WACA,eAAe,SACf,aAAa,gBACb,SACA,YAAY,qBACZ,cACmB,CACnB,GAAM,CAAC,EAAO,IAAA,EAAiB,EAAA,SAAA,KACrB,EAAW,CAAU,GAAK,CACpC,EACM,CAAC,EAAe,IAAA,EAAoB,EAAA,SAAA,KAA8B,EAAQ,CAAK,CAAC,EAEhF,EAAY,EAAA,aAAa,CAAM,EAE/B,EAAe,MAAM,QAAQ,CAAS,EAAI,EAAU,KAAK,GAAG,EAAI,EAChE,EAAgB,EAAA,aAAa,CAAU,GAE7C,EAAA,EAAA,UAAA,KAAgB,CACZ,IAAM,EAAU,EAAU,UAAU,GAAK,SAAS,gBAClD,GAAI,CAAC,EAAS,OACd,IAAM,EAAO,EAAQ,CAAK,EAC1B,EAAc,EAAS,EAAM,EAAW,EAAc,OAAO,EAC7D,EAAiB,CAAI,CAGzB,EAAG,CAAC,EAAO,CAAY,CAAC,GAExB,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,IAAU,UAAY,OAAO,OAAW,IAAa,OACzD,IAAM,EAAO,OAAO,WAAW,8BAA8B,EACvD,MAAsB,CACxB,IAAM,EAAU,EAAU,UAAU,GAAK,SAAS,gBAClD,GAAI,CAAC,EAAS,OACd,IAAM,EAAsB,EAAK,QAAU,OAAS,QACpD,EAAc,EAAS,EAAM,EAAW,EAAc,OAAO,EAC7D,EAAiB,CAAI,CACzB,EAEA,OADA,EAAK,iBAAiB,SAAU,CAAO,MAC1B,EAAK,oBAAoB,SAAU,CAAO,CAE3D,EAAG,CAAC,EAAO,CAAY,CAAC,EAExB,IAAM,GAAA,EAAW,EAAA,YAAA,CACZ,GAAoB,CAEjB,GADA,EAAc,CAAI,EACd,GAAc,OAAO,OAAW,IAChC,GAAI,CACA,OAAO,aAAa,QAAQ,EAAY,CAAI,CAChD,MAAQ,CAER,CAER,EACA,CAAC,CAAU,CACf,EAEM,GAAA,EAAS,EAAA,YAAA,KAAkB,CAC7B,EAAS,IAAkB,OAAS,QAAU,MAAM,CACxD,EAAG,CAAC,EAAe,CAAQ,CAAC,EAEtB,GAAA,EAAQ,EAAA,QAAA,MACH,CAAE,QAAO,gBAAe,WAAU,QAAO,GAChD,CAAC,EAAO,EAAe,EAAU,CAAM,CAC3C,EAEA,OAAO,EAAA,EAAA,IAAA,CAAC,EAAa,SAAd,CAA8B,QAAQ,UAAgC,CAAA,CACjF,CAKA,SAAgB,GAA8B,CAC1C,IAAM,GAAA,EAAM,EAAA,WAAA,CAAW,CAAY,EACnC,GAAI,CAAC,EAAK,MAAU,MAAM,gDAAgD,EAC1E,OAAO,CACX"}
1
+ {"version":3,"file":"ThemeProvider.cjs","names":[],"sources":["../../src/theme/ThemeProvider.tsx"],"sourcesContent":["import { createContext, useCallback, useContext, useEffect, useMemo, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { useLatestRef } from \"@/hooks/use-latest-ref\";\nimport { storage } from \"@/utils/storage\";\nimport type { ResolvedTheme, ThemeMode } from \"./types\";\n\nexport interface ThemeContextValue {\n /** Raw user preference (light / dark / system). */\n theme: ThemeMode;\n /** Effective theme actually applied to the DOM (light or dark). */\n resolvedTheme: ResolvedTheme;\n /** Update the preference. Persisted to localStorage when `storageKey` is set. */\n setTheme: (next: ThemeMode) => void;\n /** Convenience: flip light ↔ dark. When in `system` mode, switches to the opposite of the current resolved theme. */\n toggle: () => void;\n}\n\nconst ThemeContext = createContext<ThemeContextValue | null>(null);\n\nexport interface ThemeProviderProps {\n children: ReactNode;\n /** Initial preference when nothing is stored. Default: `\"system\"`. */\n defaultTheme?: ThemeMode;\n /** localStorage key used to persist the preference. Pass `null` to disable persistence. Default: `\"tempest-theme\"`. */\n storageKey?: string | null;\n /**\n * Element that receives the theme attribute(s). Defaults to\n * `document.documentElement`. Override when scoping the theme to a subtree.\n */\n target?: () => HTMLElement | null;\n /**\n * Attribute name(s) written on the target with the resolved theme\n * (`\"light\"` / `\"dark\"`). Default: `\"data-tempest-theme\"`.\n *\n * Pass an array to mirror the theme onto more than one attribute — handy\n * when the SDK components read `data-tempest-theme` but the host app's own\n * CSS keys off a different attribute (e.g. `[\"data-tempest-theme\",\n * \"data-theme\"]`). Avoids a separate sync effect in the consumer.\n */\n attribute?: string | string[];\n /**\n * When set, keeps `<meta name=\"theme-color\">` in sync with the resolved\n * theme — `content` becomes `themeColor.dark` in dark mode and\n * `themeColor.light` in light mode. The meta tag must already exist in the\n * document `<head>`. No-op when omitted.\n */\n themeColor?: { light: string; dark: string };\n}\n\nfunction resolve(mode: ThemeMode): ResolvedTheme {\n if (mode === \"dark\" || mode === \"light\") return mode;\n if (typeof window === \"undefined\") return \"light\";\n return window.matchMedia(\"(prefers-color-scheme: dark)\").matches ? \"dark\" : \"light\";\n}\n\n/**\n * Write the resolved theme onto every configured attribute and, when a\n * `themeColor` map is provided, sync the `<meta name=\"theme-color\">` tag.\n */\nfunction applyResolved(\n element: HTMLElement,\n resolved: ResolvedTheme,\n attribute: string | string[],\n themeColor?: { light: string; dark: string },\n): void {\n const attrs = Array.isArray(attribute) ? attribute : [attribute];\n for (const attr of attrs) element.setAttribute(attr, resolved);\n if (themeColor && typeof document !== \"undefined\") {\n const meta = document.querySelector<HTMLMetaElement>('meta[name=\"theme-color\"]');\n if (meta) meta.content = themeColor[resolved];\n }\n}\n\nfunction readStored(storageKey: string | null): ThemeMode | null {\n if (!storageKey) return null;\n const value = storage.getRaw(storageKey);\n if (value === \"light\" || value === \"dark\" || value === \"system\") return value;\n return null;\n}\n\n/**\n * Wire dark/light theming. Writes a data attribute on a target element (the\n * `<html>` element by default) and exposes the current preference via\n * {@link useTheme}.\n *\n * Pair with `themeInitScript()` in the HTML head to prevent the flash of\n * incorrect theme on first paint.\n *\n * @tempest-limits empty-catch — the theme is applied to the DOM before it is\n * persisted, so a `localStorage` write refused by quota or private mode leaves the\n * user with the theme they just picked and only forfeits it on the next load.\n * Throwing from the setter would break the switch that already worked.\n */\nexport function ThemeProvider({\n children,\n defaultTheme = \"system\",\n storageKey = \"tempest-theme\",\n target,\n attribute = \"data-tempest-theme\",\n themeColor,\n}: ThemeProviderProps) {\n const [theme, setThemeState] = useState<ThemeMode>(\n () => readStored(storageKey) ?? defaultTheme,\n );\n const [resolvedTheme, setResolvedTheme] = useState<ResolvedTheme>(() => resolve(theme));\n\n const targetRef = useLatestRef(target);\n\n const attributeKey = Array.isArray(attribute) ? attribute.join(\",\") : attribute;\n const themeColorRef = useLatestRef(themeColor);\n\n useEffect(() => {\n const element = targetRef.current?.() ?? document.documentElement;\n if (!element) return;\n const next = resolve(theme);\n applyResolved(element, next, attribute, themeColorRef.current);\n setResolvedTheme(next);\n // attributeKey is the stable string form of `attribute`.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [theme, attributeKey]);\n\n useEffect(() => {\n if (theme !== \"system\" || typeof window === \"undefined\") return;\n const list = window.matchMedia(\"(prefers-color-scheme: dark)\");\n const handler = (): void => {\n const element = targetRef.current?.() ?? document.documentElement;\n if (!element) return;\n const next: ResolvedTheme = list.matches ? \"dark\" : \"light\";\n applyResolved(element, next, attribute, themeColorRef.current);\n setResolvedTheme(next);\n };\n list.addEventListener(\"change\", handler);\n return () => list.removeEventListener(\"change\", handler);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [theme, attributeKey]);\n\n const setTheme = useCallback(\n (next: ThemeMode) => {\n setThemeState(next);\n if (storageKey) storage.setRaw(storageKey, next);\n },\n [storageKey],\n );\n\n const toggle = useCallback(() => {\n setTheme(resolvedTheme === \"dark\" ? \"light\" : \"dark\");\n }, [resolvedTheme, setTheme]);\n\n const value = useMemo<ThemeContextValue>(\n () => ({ theme, resolvedTheme, setTheme, toggle }),\n [theme, resolvedTheme, setTheme, toggle],\n );\n\n return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;\n}\n\n/**\n * Read and mutate the current theme. Must be used inside a {@link ThemeProvider}.\n */\nexport function useTheme(): ThemeContextValue {\n const ctx = useContext(ThemeContext);\n if (!ctx) throw new Error(\"useTheme must be used inside a <ThemeProvider>\");\n return ctx;\n}\n"],"mappings":"uIAiBA,IAAM,GAAA,EAAe,EAAA,cAAA,CAAwC,IAAI,EAgCjE,SAAS,EAAQ,EAAgC,CAG7C,OAFI,IAAS,QAAU,IAAS,QAAgB,EAC5C,OAAO,OAAW,IAAoB,QACnC,OAAO,WAAW,8BAA8B,CAAC,CAAC,QAAU,OAAS,OAChF,CAMA,SAAS,EACL,EACA,EACA,EACA,EACI,CACJ,IAAM,EAAQ,MAAM,QAAQ,CAAS,EAAI,EAAY,CAAC,CAAS,EAC/D,IAAK,IAAM,KAAQ,EAAO,EAAQ,aAAa,EAAM,CAAQ,EAC7D,GAAI,GAAc,OAAO,SAAa,IAAa,CAC/C,IAAM,EAAO,SAAS,cAA+B,0BAA0B,EAC3E,IAAM,EAAK,QAAU,EAAW,GACxC,CACJ,CAEA,SAAS,EAAW,EAA6C,CAC7D,GAAI,CAAC,EAAY,OAAO,KACxB,IAAM,EAAQ,EAAA,QAAQ,OAAO,CAAU,EAEvC,OADI,IAAU,SAAW,IAAU,QAAU,IAAU,SAAiB,EACjE,IACX,CAeA,SAAgB,EAAc,CAC1B,WACA,eAAe,SACf,aAAa,gBACb,SACA,YAAY,qBACZ,cACmB,CACnB,GAAM,CAAC,EAAO,IAAA,EAAiB,EAAA,SAAA,KACrB,EAAW,CAAU,GAAK,CACpC,EACM,CAAC,EAAe,IAAA,EAAoB,EAAA,SAAA,KAA8B,EAAQ,CAAK,CAAC,EAEhF,EAAY,EAAA,aAAa,CAAM,EAE/B,EAAe,MAAM,QAAQ,CAAS,EAAI,EAAU,KAAK,GAAG,EAAI,EAChE,EAAgB,EAAA,aAAa,CAAU,GAE7C,EAAA,EAAA,UAAA,KAAgB,CACZ,IAAM,EAAU,EAAU,UAAU,GAAK,SAAS,gBAClD,GAAI,CAAC,EAAS,OACd,IAAM,EAAO,EAAQ,CAAK,EAC1B,EAAc,EAAS,EAAM,EAAW,EAAc,OAAO,EAC7D,EAAiB,CAAI,CAGzB,EAAG,CAAC,EAAO,CAAY,CAAC,GAExB,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,IAAU,UAAY,OAAO,OAAW,IAAa,OACzD,IAAM,EAAO,OAAO,WAAW,8BAA8B,EACvD,MAAsB,CACxB,IAAM,EAAU,EAAU,UAAU,GAAK,SAAS,gBAClD,GAAI,CAAC,EAAS,OACd,IAAM,EAAsB,EAAK,QAAU,OAAS,QACpD,EAAc,EAAS,EAAM,EAAW,EAAc,OAAO,EAC7D,EAAiB,CAAI,CACzB,EAEA,OADA,EAAK,iBAAiB,SAAU,CAAO,MAC1B,EAAK,oBAAoB,SAAU,CAAO,CAE3D,EAAG,CAAC,EAAO,CAAY,CAAC,EAExB,IAAM,GAAA,EAAW,EAAA,YAAA,CACZ,GAAoB,CACjB,EAAc,CAAI,EACd,GAAY,EAAA,QAAQ,OAAO,EAAY,CAAI,CACnD,EACA,CAAC,CAAU,CACf,EAEM,GAAA,EAAS,EAAA,YAAA,KAAkB,CAC7B,EAAS,IAAkB,OAAS,QAAU,MAAM,CACxD,EAAG,CAAC,EAAe,CAAQ,CAAC,EAEtB,GAAA,EAAQ,EAAA,QAAA,MACH,CAAE,QAAO,gBAAe,WAAU,QAAO,GAChD,CAAC,EAAO,EAAe,EAAU,CAAM,CAC3C,EAEA,OAAO,EAAA,EAAA,IAAA,CAAC,EAAa,SAAd,CAA8B,QAAQ,UAAgC,CAAA,CACjF,CAKA,SAAgB,GAA8B,CAC1C,IAAM,GAAA,EAAM,EAAA,WAAA,CAAW,CAAY,EACnC,GAAI,CAAC,EAAK,MAAU,MAAM,gDAAgD,EAC1E,OAAO,CACX"}
@@ -1,12 +1,13 @@
1
1
  import { useLatestRef as e } from "../hooks/use-latest-ref.js";
2
- import { createContext as t, useCallback as n, useContext as r, useEffect as i, useMemo as a, useState as o } from "react";
3
- import { jsx as s } from "react/jsx-runtime";
2
+ import { storage as t } from "../utils/storage.js";
3
+ import { createContext as n, useCallback as r, useContext as i, useEffect as a, useMemo as o, useState as s } from "react";
4
+ import { jsx as c } from "react/jsx-runtime";
4
5
  //#region src/theme/ThemeProvider.tsx
5
- var c = t(null);
6
- function l(e) {
6
+ var l = n(null);
7
+ function u(e) {
7
8
  return e === "dark" || e === "light" ? e : typeof window > "u" ? "light" : window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
8
9
  }
9
- function u(e, t, n, r) {
10
+ function d(e, t, n, r) {
10
11
  let i = Array.isArray(n) ? n : [n];
11
12
  for (let n of i) e.setAttribute(n, t);
12
13
  if (r && typeof document < "u") {
@@ -14,60 +15,54 @@ function u(e, t, n, r) {
14
15
  e && (e.content = r[t]);
15
16
  }
16
17
  }
17
- function d(e) {
18
- if (!e || typeof window > "u") return null;
19
- try {
20
- let t = window.localStorage.getItem(e);
21
- return t === "light" || t === "dark" || t === "system" ? t : null;
22
- } catch {
23
- return null;
24
- }
18
+ function f(e) {
19
+ if (!e) return null;
20
+ let n = t.getRaw(e);
21
+ return n === "light" || n === "dark" || n === "system" ? n : null;
25
22
  }
26
- function f({ children: t, defaultTheme: r = "system", storageKey: f = "tempest-theme", target: p, attribute: m = "data-tempest-theme", themeColor: h }) {
27
- let [g, _] = o(() => d(f) ?? r), [v, y] = o(() => l(g)), b = e(p), x = Array.isArray(m) ? m.join(",") : m, S = e(h);
28
- i(() => {
29
- let e = b.current?.() ?? document.documentElement;
23
+ function p({ children: n, defaultTheme: i = "system", storageKey: p = "tempest-theme", target: m, attribute: h = "data-tempest-theme", themeColor: g }) {
24
+ let [_, v] = s(() => f(p) ?? i), [y, b] = s(() => u(_)), x = e(m), S = Array.isArray(h) ? h.join(",") : h, C = e(g);
25
+ a(() => {
26
+ let e = x.current?.() ?? document.documentElement;
30
27
  if (!e) return;
31
- let t = l(g);
32
- u(e, t, m, S.current), y(t);
33
- }, [g, x]), i(() => {
34
- if (g !== "system" || typeof window > "u") return;
28
+ let t = u(_);
29
+ d(e, t, h, C.current), b(t);
30
+ }, [_, S]), a(() => {
31
+ if (_ !== "system" || typeof window > "u") return;
35
32
  let e = window.matchMedia("(prefers-color-scheme: dark)"), t = () => {
36
- let t = b.current?.() ?? document.documentElement;
33
+ let t = x.current?.() ?? document.documentElement;
37
34
  if (!t) return;
38
35
  let n = e.matches ? "dark" : "light";
39
- u(t, n, m, S.current), y(n);
36
+ d(t, n, h, C.current), b(n);
40
37
  };
41
38
  return e.addEventListener("change", t), () => e.removeEventListener("change", t);
42
- }, [g, x]);
43
- let C = n((e) => {
44
- if (_(e), f && typeof window < "u") try {
45
- window.localStorage.setItem(f, e);
46
- } catch {}
47
- }, [f]), w = n(() => {
48
- C(v === "dark" ? "light" : "dark");
49
- }, [v, C]), T = a(() => ({
50
- theme: g,
51
- resolvedTheme: v,
52
- setTheme: C,
53
- toggle: w
39
+ }, [_, S]);
40
+ let w = r((e) => {
41
+ v(e), p && t.setRaw(p, e);
42
+ }, [p]), T = r(() => {
43
+ w(y === "dark" ? "light" : "dark");
44
+ }, [y, w]), E = o(() => ({
45
+ theme: _,
46
+ resolvedTheme: y,
47
+ setTheme: w,
48
+ toggle: T
54
49
  }), [
55
- g,
56
- v,
57
- C,
58
- w
50
+ _,
51
+ y,
52
+ w,
53
+ T
59
54
  ]);
60
- return /* @__PURE__ */ s(c.Provider, {
61
- value: T,
62
- children: t
55
+ return /* @__PURE__ */ c(l.Provider, {
56
+ value: E,
57
+ children: n
63
58
  });
64
59
  }
65
- function p() {
66
- let e = r(c);
60
+ function m() {
61
+ let e = i(l);
67
62
  if (!e) throw Error("useTheme must be used inside a <ThemeProvider>");
68
63
  return e;
69
64
  }
70
65
  //#endregion
71
- export { f as ThemeProvider, p as useTheme };
66
+ export { p as ThemeProvider, m as useTheme };
72
67
 
73
68
  //# sourceMappingURL=ThemeProvider.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ThemeProvider.js","names":[],"sources":["../../src/theme/ThemeProvider.tsx"],"sourcesContent":["import { createContext, useCallback, useContext, useEffect, useMemo, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { useLatestRef } from \"@/hooks/use-latest-ref\";\nimport type { ResolvedTheme, ThemeMode } from \"./types\";\n\nexport interface ThemeContextValue {\n /** Raw user preference (light / dark / system). */\n theme: ThemeMode;\n /** Effective theme actually applied to the DOM (light or dark). */\n resolvedTheme: ResolvedTheme;\n /** Update the preference. Persisted to localStorage when `storageKey` is set. */\n setTheme: (next: ThemeMode) => void;\n /** Convenience: flip light ↔ dark. When in `system` mode, switches to the opposite of the current resolved theme. */\n toggle: () => void;\n}\n\nconst ThemeContext = createContext<ThemeContextValue | null>(null);\n\nexport interface ThemeProviderProps {\n children: ReactNode;\n /** Initial preference when nothing is stored. Default: `\"system\"`. */\n defaultTheme?: ThemeMode;\n /** localStorage key used to persist the preference. Pass `null` to disable persistence. Default: `\"tempest-theme\"`. */\n storageKey?: string | null;\n /**\n * Element that receives the theme attribute(s). Defaults to\n * `document.documentElement`. Override when scoping the theme to a subtree.\n */\n target?: () => HTMLElement | null;\n /**\n * Attribute name(s) written on the target with the resolved theme\n * (`\"light\"` / `\"dark\"`). Default: `\"data-tempest-theme\"`.\n *\n * Pass an array to mirror the theme onto more than one attribute — handy\n * when the SDK components read `data-tempest-theme` but the host app's own\n * CSS keys off a different attribute (e.g. `[\"data-tempest-theme\",\n * \"data-theme\"]`). Avoids a separate sync effect in the consumer.\n */\n attribute?: string | string[];\n /**\n * When set, keeps `<meta name=\"theme-color\">` in sync with the resolved\n * theme — `content` becomes `themeColor.dark` in dark mode and\n * `themeColor.light` in light mode. The meta tag must already exist in the\n * document `<head>`. No-op when omitted.\n */\n themeColor?: { light: string; dark: string };\n}\n\nfunction resolve(mode: ThemeMode): ResolvedTheme {\n if (mode === \"dark\" || mode === \"light\") return mode;\n if (typeof window === \"undefined\") return \"light\";\n return window.matchMedia(\"(prefers-color-scheme: dark)\").matches ? \"dark\" : \"light\";\n}\n\n/**\n * Write the resolved theme onto every configured attribute and, when a\n * `themeColor` map is provided, sync the `<meta name=\"theme-color\">` tag.\n */\nfunction applyResolved(\n element: HTMLElement,\n resolved: ResolvedTheme,\n attribute: string | string[],\n themeColor?: { light: string; dark: string },\n): void {\n const attrs = Array.isArray(attribute) ? attribute : [attribute];\n for (const attr of attrs) element.setAttribute(attr, resolved);\n if (themeColor && typeof document !== \"undefined\") {\n const meta = document.querySelector<HTMLMetaElement>('meta[name=\"theme-color\"]');\n if (meta) meta.content = themeColor[resolved];\n }\n}\n\nfunction readStored(storageKey: string | null): ThemeMode | null {\n if (!storageKey || typeof window === \"undefined\") return null;\n try {\n const value = window.localStorage.getItem(storageKey);\n if (value === \"light\" || value === \"dark\" || value === \"system\") return value;\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * Wire dark/light theming. Writes a data attribute on a target element (the\n * `<html>` element by default) and exposes the current preference via\n * {@link useTheme}.\n *\n * Pair with `themeInitScript()` in the HTML head to prevent the flash of\n * incorrect theme on first paint.\n *\n * @tempest-limits empty-catch — the theme is applied to the DOM before it is\n * persisted, so a `localStorage` write refused by quota or private mode leaves the\n * user with the theme they just picked and only forfeits it on the next load.\n * Throwing from the setter would break the switch that already worked.\n */\nexport function ThemeProvider({\n children,\n defaultTheme = \"system\",\n storageKey = \"tempest-theme\",\n target,\n attribute = \"data-tempest-theme\",\n themeColor,\n}: ThemeProviderProps) {\n const [theme, setThemeState] = useState<ThemeMode>(\n () => readStored(storageKey) ?? defaultTheme,\n );\n const [resolvedTheme, setResolvedTheme] = useState<ResolvedTheme>(() => resolve(theme));\n\n const targetRef = useLatestRef(target);\n\n const attributeKey = Array.isArray(attribute) ? attribute.join(\",\") : attribute;\n const themeColorRef = useLatestRef(themeColor);\n\n useEffect(() => {\n const element = targetRef.current?.() ?? document.documentElement;\n if (!element) return;\n const next = resolve(theme);\n applyResolved(element, next, attribute, themeColorRef.current);\n setResolvedTheme(next);\n // attributeKey is the stable string form of `attribute`.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [theme, attributeKey]);\n\n useEffect(() => {\n if (theme !== \"system\" || typeof window === \"undefined\") return;\n const list = window.matchMedia(\"(prefers-color-scheme: dark)\");\n const handler = (): void => {\n const element = targetRef.current?.() ?? document.documentElement;\n if (!element) return;\n const next: ResolvedTheme = list.matches ? \"dark\" : \"light\";\n applyResolved(element, next, attribute, themeColorRef.current);\n setResolvedTheme(next);\n };\n list.addEventListener(\"change\", handler);\n return () => list.removeEventListener(\"change\", handler);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [theme, attributeKey]);\n\n const setTheme = useCallback(\n (next: ThemeMode) => {\n setThemeState(next);\n if (storageKey && typeof window !== \"undefined\") {\n try {\n window.localStorage.setItem(storageKey, next);\n } catch {\n /* empty */\n }\n }\n },\n [storageKey],\n );\n\n const toggle = useCallback(() => {\n setTheme(resolvedTheme === \"dark\" ? \"light\" : \"dark\");\n }, [resolvedTheme, setTheme]);\n\n const value = useMemo<ThemeContextValue>(\n () => ({ theme, resolvedTheme, setTheme, toggle }),\n [theme, resolvedTheme, setTheme, toggle],\n );\n\n return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;\n}\n\n/**\n * Read and mutate the current theme. Must be used inside a {@link ThemeProvider}.\n */\nexport function useTheme(): ThemeContextValue {\n const ctx = useContext(ThemeContext);\n if (!ctx) throw new Error(\"useTheme must be used inside a <ThemeProvider>\");\n return ctx;\n}\n"],"mappings":";;;;AAgBA,IAAM,IAAe,EAAwC,IAAI;AAgCjE,SAAS,EAAQ,GAAgC;CAG7C,OAFI,MAAS,UAAU,MAAS,UAAgB,IAC5C,OAAO,SAAW,MAAoB,UACnC,OAAO,WAAW,8BAA8B,CAAC,CAAC,UAAU,SAAS;AAChF;AAMA,SAAS,EACL,GACA,GACA,GACA,GACI;CACJ,IAAM,IAAQ,MAAM,QAAQ,CAAS,IAAI,IAAY,CAAC,CAAS;CAC/D,KAAK,IAAM,KAAQ,GAAO,EAAQ,aAAa,GAAM,CAAQ;CAC7D,IAAI,KAAc,OAAO,WAAa,KAAa;EAC/C,IAAM,IAAO,SAAS,cAA+B,4BAA0B;EAC/E,AAAI,MAAM,EAAK,UAAU,EAAW;CACxC;AACJ;AAEA,SAAS,EAAW,GAA6C;CAC7D,IAAI,CAAC,KAAc,OAAO,SAAW,KAAa,OAAO;CACzD,IAAI;EACA,IAAM,IAAQ,OAAO,aAAa,QAAQ,CAAU;EAEpD,OADI,MAAU,WAAW,MAAU,UAAU,MAAU,WAAiB,IACjE;CACX,QAAQ;EACJ,OAAO;CACX;AACJ;AAeA,SAAgB,EAAc,EAC1B,aACA,kBAAe,UACf,gBAAa,iBACb,WACA,eAAY,sBACZ,iBACmB;CACnB,IAAM,CAAC,GAAO,KAAiB,QACrB,EAAW,CAAU,KAAK,CACpC,GACM,CAAC,GAAe,KAAoB,QAA8B,EAAQ,CAAK,CAAC,GAEhF,IAAY,EAAa,CAAM,GAE/B,IAAe,MAAM,QAAQ,CAAS,IAAI,EAAU,KAAK,GAAG,IAAI,GAChE,IAAgB,EAAa,CAAU;CAY7C,AAVA,QAAgB;EACZ,IAAM,IAAU,EAAU,UAAU,KAAK,SAAS;EAClD,IAAI,CAAC,GAAS;EACd,IAAM,IAAO,EAAQ,CAAK;EAE1B,AADA,EAAc,GAAS,GAAM,GAAW,EAAc,OAAO,GAC7D,EAAiB,CAAI;CAGzB,GAAG,CAAC,GAAO,CAAY,CAAC,GAExB,QAAgB;EACZ,IAAI,MAAU,YAAY,OAAO,SAAW,KAAa;EACzD,IAAM,IAAO,OAAO,WAAW,8BAA8B,GACvD,UAAsB;GACxB,IAAM,IAAU,EAAU,UAAU,KAAK,SAAS;GAClD,IAAI,CAAC,GAAS;GACd,IAAM,IAAsB,EAAK,UAAU,SAAS;GAEpD,AADA,EAAc,GAAS,GAAM,GAAW,EAAc,OAAO,GAC7D,EAAiB,CAAI;EACzB;EAEA,OADA,EAAK,iBAAiB,UAAU,CAAO,SAC1B,EAAK,oBAAoB,UAAU,CAAO;CAE3D,GAAG,CAAC,GAAO,CAAY,CAAC;CAExB,IAAM,IAAW,GACZ,MAAoB;EAEjB,IADA,EAAc,CAAI,GACd,KAAc,OAAO,SAAW,KAChC,IAAI;GACA,OAAO,aAAa,QAAQ,GAAY,CAAI;EAChD,QAAQ,CAER;CAER,GACA,CAAC,CAAU,CACf,GAEM,IAAS,QAAkB;EAC7B,EAAS,MAAkB,SAAS,UAAU,MAAM;CACxD,GAAG,CAAC,GAAe,CAAQ,CAAC,GAEtB,IAAQ,SACH;EAAE;EAAO;EAAe;EAAU;CAAO,IAChD;EAAC;EAAO;EAAe;EAAU;CAAM,CAC3C;CAEA,OAAO,kBAAC,EAAa,UAAd;EAA8B;EAAQ;CAAgC,CAAA;AACjF;AAKA,SAAgB,IAA8B;CAC1C,IAAM,IAAM,EAAW,CAAY;CACnC,IAAI,CAAC,GAAK,MAAU,MAAM,gDAAgD;CAC1E,OAAO;AACX"}
1
+ {"version":3,"file":"ThemeProvider.js","names":[],"sources":["../../src/theme/ThemeProvider.tsx"],"sourcesContent":["import { createContext, useCallback, useContext, useEffect, useMemo, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { useLatestRef } from \"@/hooks/use-latest-ref\";\nimport { storage } from \"@/utils/storage\";\nimport type { ResolvedTheme, ThemeMode } from \"./types\";\n\nexport interface ThemeContextValue {\n /** Raw user preference (light / dark / system). */\n theme: ThemeMode;\n /** Effective theme actually applied to the DOM (light or dark). */\n resolvedTheme: ResolvedTheme;\n /** Update the preference. Persisted to localStorage when `storageKey` is set. */\n setTheme: (next: ThemeMode) => void;\n /** Convenience: flip light ↔ dark. When in `system` mode, switches to the opposite of the current resolved theme. */\n toggle: () => void;\n}\n\nconst ThemeContext = createContext<ThemeContextValue | null>(null);\n\nexport interface ThemeProviderProps {\n children: ReactNode;\n /** Initial preference when nothing is stored. Default: `\"system\"`. */\n defaultTheme?: ThemeMode;\n /** localStorage key used to persist the preference. Pass `null` to disable persistence. Default: `\"tempest-theme\"`. */\n storageKey?: string | null;\n /**\n * Element that receives the theme attribute(s). Defaults to\n * `document.documentElement`. Override when scoping the theme to a subtree.\n */\n target?: () => HTMLElement | null;\n /**\n * Attribute name(s) written on the target with the resolved theme\n * (`\"light\"` / `\"dark\"`). Default: `\"data-tempest-theme\"`.\n *\n * Pass an array to mirror the theme onto more than one attribute — handy\n * when the SDK components read `data-tempest-theme` but the host app's own\n * CSS keys off a different attribute (e.g. `[\"data-tempest-theme\",\n * \"data-theme\"]`). Avoids a separate sync effect in the consumer.\n */\n attribute?: string | string[];\n /**\n * When set, keeps `<meta name=\"theme-color\">` in sync with the resolved\n * theme — `content` becomes `themeColor.dark` in dark mode and\n * `themeColor.light` in light mode. The meta tag must already exist in the\n * document `<head>`. No-op when omitted.\n */\n themeColor?: { light: string; dark: string };\n}\n\nfunction resolve(mode: ThemeMode): ResolvedTheme {\n if (mode === \"dark\" || mode === \"light\") return mode;\n if (typeof window === \"undefined\") return \"light\";\n return window.matchMedia(\"(prefers-color-scheme: dark)\").matches ? \"dark\" : \"light\";\n}\n\n/**\n * Write the resolved theme onto every configured attribute and, when a\n * `themeColor` map is provided, sync the `<meta name=\"theme-color\">` tag.\n */\nfunction applyResolved(\n element: HTMLElement,\n resolved: ResolvedTheme,\n attribute: string | string[],\n themeColor?: { light: string; dark: string },\n): void {\n const attrs = Array.isArray(attribute) ? attribute : [attribute];\n for (const attr of attrs) element.setAttribute(attr, resolved);\n if (themeColor && typeof document !== \"undefined\") {\n const meta = document.querySelector<HTMLMetaElement>('meta[name=\"theme-color\"]');\n if (meta) meta.content = themeColor[resolved];\n }\n}\n\nfunction readStored(storageKey: string | null): ThemeMode | null {\n if (!storageKey) return null;\n const value = storage.getRaw(storageKey);\n if (value === \"light\" || value === \"dark\" || value === \"system\") return value;\n return null;\n}\n\n/**\n * Wire dark/light theming. Writes a data attribute on a target element (the\n * `<html>` element by default) and exposes the current preference via\n * {@link useTheme}.\n *\n * Pair with `themeInitScript()` in the HTML head to prevent the flash of\n * incorrect theme on first paint.\n *\n * @tempest-limits empty-catch — the theme is applied to the DOM before it is\n * persisted, so a `localStorage` write refused by quota or private mode leaves the\n * user with the theme they just picked and only forfeits it on the next load.\n * Throwing from the setter would break the switch that already worked.\n */\nexport function ThemeProvider({\n children,\n defaultTheme = \"system\",\n storageKey = \"tempest-theme\",\n target,\n attribute = \"data-tempest-theme\",\n themeColor,\n}: ThemeProviderProps) {\n const [theme, setThemeState] = useState<ThemeMode>(\n () => readStored(storageKey) ?? defaultTheme,\n );\n const [resolvedTheme, setResolvedTheme] = useState<ResolvedTheme>(() => resolve(theme));\n\n const targetRef = useLatestRef(target);\n\n const attributeKey = Array.isArray(attribute) ? attribute.join(\",\") : attribute;\n const themeColorRef = useLatestRef(themeColor);\n\n useEffect(() => {\n const element = targetRef.current?.() ?? document.documentElement;\n if (!element) return;\n const next = resolve(theme);\n applyResolved(element, next, attribute, themeColorRef.current);\n setResolvedTheme(next);\n // attributeKey is the stable string form of `attribute`.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [theme, attributeKey]);\n\n useEffect(() => {\n if (theme !== \"system\" || typeof window === \"undefined\") return;\n const list = window.matchMedia(\"(prefers-color-scheme: dark)\");\n const handler = (): void => {\n const element = targetRef.current?.() ?? document.documentElement;\n if (!element) return;\n const next: ResolvedTheme = list.matches ? \"dark\" : \"light\";\n applyResolved(element, next, attribute, themeColorRef.current);\n setResolvedTheme(next);\n };\n list.addEventListener(\"change\", handler);\n return () => list.removeEventListener(\"change\", handler);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [theme, attributeKey]);\n\n const setTheme = useCallback(\n (next: ThemeMode) => {\n setThemeState(next);\n if (storageKey) storage.setRaw(storageKey, next);\n },\n [storageKey],\n );\n\n const toggle = useCallback(() => {\n setTheme(resolvedTheme === \"dark\" ? \"light\" : \"dark\");\n }, [resolvedTheme, setTheme]);\n\n const value = useMemo<ThemeContextValue>(\n () => ({ theme, resolvedTheme, setTheme, toggle }),\n [theme, resolvedTheme, setTheme, toggle],\n );\n\n return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;\n}\n\n/**\n * Read and mutate the current theme. Must be used inside a {@link ThemeProvider}.\n */\nexport function useTheme(): ThemeContextValue {\n const ctx = useContext(ThemeContext);\n if (!ctx) throw new Error(\"useTheme must be used inside a <ThemeProvider>\");\n return ctx;\n}\n"],"mappings":";;;;;AAiBA,IAAM,IAAe,EAAwC,IAAI;AAgCjE,SAAS,EAAQ,GAAgC;CAG7C,OAFI,MAAS,UAAU,MAAS,UAAgB,IAC5C,OAAO,SAAW,MAAoB,UACnC,OAAO,WAAW,8BAA8B,CAAC,CAAC,UAAU,SAAS;AAChF;AAMA,SAAS,EACL,GACA,GACA,GACA,GACI;CACJ,IAAM,IAAQ,MAAM,QAAQ,CAAS,IAAI,IAAY,CAAC,CAAS;CAC/D,KAAK,IAAM,KAAQ,GAAO,EAAQ,aAAa,GAAM,CAAQ;CAC7D,IAAI,KAAc,OAAO,WAAa,KAAa;EAC/C,IAAM,IAAO,SAAS,cAA+B,4BAA0B;EAC/E,AAAI,MAAM,EAAK,UAAU,EAAW;CACxC;AACJ;AAEA,SAAS,EAAW,GAA6C;CAC7D,IAAI,CAAC,GAAY,OAAO;CACxB,IAAM,IAAQ,EAAQ,OAAO,CAAU;CAEvC,OADI,MAAU,WAAW,MAAU,UAAU,MAAU,WAAiB,IACjE;AACX;AAeA,SAAgB,EAAc,EAC1B,aACA,kBAAe,UACf,gBAAa,iBACb,WACA,eAAY,sBACZ,iBACmB;CACnB,IAAM,CAAC,GAAO,KAAiB,QACrB,EAAW,CAAU,KAAK,CACpC,GACM,CAAC,GAAe,KAAoB,QAA8B,EAAQ,CAAK,CAAC,GAEhF,IAAY,EAAa,CAAM,GAE/B,IAAe,MAAM,QAAQ,CAAS,IAAI,EAAU,KAAK,GAAG,IAAI,GAChE,IAAgB,EAAa,CAAU;CAY7C,AAVA,QAAgB;EACZ,IAAM,IAAU,EAAU,UAAU,KAAK,SAAS;EAClD,IAAI,CAAC,GAAS;EACd,IAAM,IAAO,EAAQ,CAAK;EAE1B,AADA,EAAc,GAAS,GAAM,GAAW,EAAc,OAAO,GAC7D,EAAiB,CAAI;CAGzB,GAAG,CAAC,GAAO,CAAY,CAAC,GAExB,QAAgB;EACZ,IAAI,MAAU,YAAY,OAAO,SAAW,KAAa;EACzD,IAAM,IAAO,OAAO,WAAW,8BAA8B,GACvD,UAAsB;GACxB,IAAM,IAAU,EAAU,UAAU,KAAK,SAAS;GAClD,IAAI,CAAC,GAAS;GACd,IAAM,IAAsB,EAAK,UAAU,SAAS;GAEpD,AADA,EAAc,GAAS,GAAM,GAAW,EAAc,OAAO,GAC7D,EAAiB,CAAI;EACzB;EAEA,OADA,EAAK,iBAAiB,UAAU,CAAO,SAC1B,EAAK,oBAAoB,UAAU,CAAO;CAE3D,GAAG,CAAC,GAAO,CAAY,CAAC;CAExB,IAAM,IAAW,GACZ,MAAoB;EAEjB,AADA,EAAc,CAAI,GACd,KAAY,EAAQ,OAAO,GAAY,CAAI;CACnD,GACA,CAAC,CAAU,CACf,GAEM,IAAS,QAAkB;EAC7B,EAAS,MAAkB,SAAS,UAAU,MAAM;CACxD,GAAG,CAAC,GAAe,CAAQ,CAAC,GAEtB,IAAQ,SACH;EAAE;EAAO;EAAe;EAAU;CAAO,IAChD;EAAC;EAAO;EAAe;EAAU;CAAM,CAC3C;CAEA,OAAO,kBAAC,EAAa,UAAd;EAA8B;EAAQ;CAAgC,CAAA;AACjF;AAKA,SAAgB,IAA8B;CAC1C,IAAM,IAAM,EAAW,CAAY;CACnC,IAAI,CAAC,GAAK,MAAU,MAAM,gDAAgD;CAC1E,OAAO;AACX"}
@@ -1,4 +1,4 @@
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)}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(`
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
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(`
3
3
 
4
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;
@@ -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 * @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}\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\";\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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"}
@@ -133,7 +133,7 @@ function b(e, t, n, a, o) {
133
133
  let s = i(t[500]);
134
134
  e["--tempest-primary-foreground"] = s, e["--tempest-text-on-primary"] = s;
135
135
  let c = t[g(t, a)];
136
- e["--tempest-focus-ring-color"] = o === void 0 || o >= 1 ? c : r(c, o);
136
+ e["--tempest-focus-ring-color"] = o === void 0 || o >= 1 ? c : r(c, o), e["--tempest-selected-indicator"] = c;
137
137
  }
138
138
  function x(e, t, n) {
139
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);
@@ -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 * @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}\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\";\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,4 +1,4 @@
1
- function e(e={}){let{storageKey:t=`tempest-theme`,defaultTheme:n=`system`}=e;if(typeof window>`u`)return n===`dark`?`dark`:`light`;let r;try{r=window.localStorage.getItem(t)}catch{r=null}let i=r??n;return i===`dark`||i===`light`?i:window.matchMedia(`(prefers-color-scheme: dark)`).matches?`dark`:`light`}function t(e={}){let t=e.storageKey??`tempest-theme`,n=e.defaultTheme??`system`;return`
1
+ const e=require("../utils/storage.cjs");function t(t={}){let{storageKey:n=`tempest-theme`,defaultTheme:r=`system`}=t;if(typeof window>`u`)return r===`dark`?`dark`:`light`;let i=e.storage.getRaw(n)??r;return i===`dark`||i===`light`?i:window.matchMedia(`(prefers-color-scheme: dark)`).matches?`dark`:`light`}function n(e={}){let t=e.storageKey??`tempest-theme`,n=e.defaultTheme??`system`;return`
2
2
  (function(){try{
3
3
  var key=${JSON.stringify(t)};
4
4
  var def=${JSON.stringify(n)};
@@ -7,5 +7,5 @@ function e(e={}){let{storageKey:t=`tempest-theme`,defaultTheme:n=`system`}=e;if(
7
7
  var resolved=mode==="dark"||mode==="light"?mode:(matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light");
8
8
  document.documentElement.setAttribute("data-tempest-theme",resolved);
9
9
  }catch(e){}})();
10
- `.trim()}exports.getInitialTheme=e,exports.themeInitScript=t;
10
+ `.trim()}exports.getInitialTheme=t,exports.themeInitScript=n;
11
11
  //# sourceMappingURL=initial-theme.cjs.map