tempest-react-sdk 0.58.0 → 0.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -3
- package/bin/lib/css/findings.mjs +1 -0
- package/bin/lib/css/semantic.mjs +70 -6
- package/bin/lib/css/semantic.test.mjs +52 -1
- package/dist/components/Alert/Alert.module.cjs.map +1 -1
- package/dist/components/Alert/Alert.module.js.map +1 -1
- package/dist/components/Badge/Badge.module.cjs.map +1 -1
- package/dist/components/Badge/Badge.module.js.map +1 -1
- package/dist/components/BarList/BarList.module.cjs.map +1 -1
- package/dist/components/BarList/BarList.module.js.map +1 -1
- package/dist/components/BottomSheet/BottomSheet.module.cjs.map +1 -1
- package/dist/components/BottomSheet/BottomSheet.module.js.map +1 -1
- package/dist/components/DataTable/DataTable.module.cjs.map +1 -1
- package/dist/components/DataTable/DataTable.module.js.map +1 -1
- package/dist/components/Dropzone/Dropzone.module.cjs.map +1 -1
- package/dist/components/Dropzone/Dropzone.module.js.map +1 -1
- package/dist/components/ErrorText/ErrorText.module.cjs.map +1 -1
- package/dist/components/ErrorText/ErrorText.module.js.map +1 -1
- package/dist/components/Markdown/Markdown.module.cjs.map +1 -1
- package/dist/components/Markdown/Markdown.module.js.map +1 -1
- package/dist/components/PasswordInput/PasswordInput.module.cjs.map +1 -1
- package/dist/components/PasswordInput/PasswordInput.module.js.map +1 -1
- package/dist/components/SegmentedControl/SegmentedControl.module.cjs.map +1 -1
- package/dist/components/SegmentedControl/SegmentedControl.module.js.map +1 -1
- package/dist/components/Sidebar/Sidebar.module.cjs.map +1 -1
- package/dist/components/Sidebar/Sidebar.module.js.map +1 -1
- package/dist/components/StepperInput/StepperInput.module.cjs.map +1 -1
- package/dist/components/StepperInput/StepperInput.module.js.map +1 -1
- package/dist/components/Tag/Tag.module.cjs.map +1 -1
- package/dist/components/Tag/Tag.module.js.map +1 -1
- package/dist/components/Timeline/Timeline.module.cjs.map +1 -1
- package/dist/components/Timeline/Timeline.module.js.map +1 -1
- package/dist/components/Toggle/Toggle.module.cjs.map +1 -1
- package/dist/components/Toggle/Toggle.module.js.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroup.module.cjs.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroup.module.js.map +1 -1
- package/dist/http/describe-api-error.cjs +1 -1
- package/dist/http/describe-api-error.cjs.map +1 -1
- package/dist/http/describe-api-error.js +12 -8
- package/dist/http/describe-api-error.js.map +1 -1
- package/dist/http/errors.cjs +1 -1
- package/dist/http/errors.cjs.map +1 -1
- package/dist/http/errors.js +21 -16
- package/dist/http/errors.js.map +1 -1
- package/dist/styles/Alert.css +1 -1
- package/dist/styles/Badge.css +1 -1
- package/dist/styles/BarList.css +3 -3
- package/dist/styles/BottomSheet.css +1 -1
- package/dist/styles/DataTable.css +1 -1
- package/dist/styles/Dropzone.css +1 -1
- package/dist/styles/ErrorText.css +1 -1
- package/dist/styles/Markdown.css +1 -1
- package/dist/styles/PasswordInput.css +1 -1
- package/dist/styles/SegmentedControl.css +1 -1
- package/dist/styles/Sidebar.css +2 -2
- package/dist/styles/StepperInput.css +1 -1
- package/dist/styles/Tag.css +1 -1
- package/dist/styles/Timeline.css +1 -1
- package/dist/styles/Toggle.css +1 -1
- package/dist/styles/ToggleGroup.css +1 -1
- package/dist/styles/advanced.css +2 -2
- package/dist/styles/auto.css +4 -0
- package/dist/styles/base.css +33 -0
- package/dist/styles/core.css +4 -2
- package/dist/styles/data.css +6 -6
- package/dist/styles/feedback.css +3 -3
- package/dist/styles/forms.css +3 -3
- package/dist/styles/manifest.json +496 -0
- package/dist/styles/navigation.css +3 -3
- package/dist/styles/overlay.css +1 -1
- package/dist/styles/scoped.css +131 -0
- package/dist/styles/tokens.css +10 -0
- package/dist/styles/utility.css +1 -1
- package/dist/styles.css +1 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +81 -7
- package/dist/tempest-react-sdk.js +347 -346
- package/dist/theme/create-theme.cjs +3 -3
- package/dist/theme/create-theme.cjs.map +1 -1
- package/dist/theme/create-theme.js +15 -14
- package/dist/theme/create-theme.js.map +1 -1
- package/dist/utils/dev-mode.cjs +1 -1
- package/dist/utils/dev-mode.cjs.map +1 -1
- package/dist/utils/dev-mode.js +7 -2
- package/dist/utils/dev-mode.js.map +1 -1
- package/dist/vite/tempest-styles.cjs +5 -0
- package/dist/vite/tempest-styles.cjs.map +1 -0
- package/dist/vite/tempest-styles.js +103 -0
- package/dist/vite/tempest-styles.js.map +1 -0
- package/dist/vite.cjs +1 -1
- package/dist/vite.d.ts +82 -0
- package/dist/vite.js +2 -1
- package/package.json +3 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
const e=require("./color.cjs"),t=require("./data-viz-ramps.cjs");var n={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`}},r=[50,100,200,300,400,500,600,700,800,900];function i(e,t,n){for(let i of r)e[`--tempest-${t}-${i}`]=n[i]}var a=4.5;function o(t,n,r){for(let i of r)if(e.contrastRatio(t[n],t[i])>=a)return i;return r[r.length-1]}function
|
|
2
|
-
`)}\n}`}function
|
|
1
|
+
const e=require("./color.cjs"),t=require("./data-viz-ramps.cjs");var n={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`}},r=[50,100,200,300,400,500,600,700,800,900];function i(e,t,n){for(let i of r)e[`--tempest-${t}-${i}`]=n[i]}var a=4.5;function o(t,n,r){for(let i of r)if(e.contrastRatio(t[n],t[i])>=a)return i;return r[r.length-1]}var s=`#1f0606`;function c(t,n,r,i){t[`--tempest-primary`]=`var(--tempest-primary-500)`,r===`light`?(t[`--tempest-primary-hover`]=`var(--tempest-primary-600)`,t[`--tempest-primary-active`]=`var(--tempest-primary-700)`,t[`--tempest-primary-soft`]=`var(--tempest-primary-50)`,t[`--tempest-primary-soft-hover`]=`var(--tempest-primary-100)`,t[`--tempest-primary-on-soft`]=`var(--tempest-primary-${o(n,50,[600,700,800,900])})`):(t[`--tempest-primary-hover`]=`var(--tempest-primary-400)`,t[`--tempest-primary-active`]=`var(--tempest-primary-300)`,t[`--tempest-primary-soft`]=`var(--tempest-primary-100)`,t[`--tempest-primary-soft-hover`]=`var(--tempest-primary-200)`,t[`--tempest-primary-on-soft`]=`var(--tempest-primary-${o(n,100,[700,800,900])})`);let a=e.readableForeground(n[500]);t[`--tempest-primary-foreground`]=a,t[`--tempest-text-on-primary`]=a,t[`--tempest-focus-ring-color`]=e.hexToRgbaString(n[500],i)}function l(t,n,r){r===`light`?(t[`--tempest-bg`]=`#ffffff`,t[`--tempest-surface`]=`var(--tempest-gray-50)`,t[`--tempest-surface-2`]=`var(--tempest-gray-100)`,t[`--tempest-surface-3`]=`var(--tempest-gray-200)`,t[`--tempest-border`]=`var(--tempest-gray-200)`,t[`--tempest-border-strong`]=`var(--tempest-gray-300)`,t[`--tempest-text`]=`var(--tempest-gray-900)`,t[`--tempest-text-muted`]=`var(--tempest-gray-600)`,t[`--tempest-text-subtle`]=`var(--tempest-gray-500)`):(t[`--tempest-bg`]=n[50],t[`--tempest-surface`]=`var(--tempest-gray-100)`,t[`--tempest-surface-2`]=`var(--tempest-gray-200)`,t[`--tempest-surface-3`]=`var(--tempest-gray-300)`,t[`--tempest-border`]=`var(--tempest-gray-300)`,t[`--tempest-border-strong`]=`var(--tempest-gray-400)`,t[`--tempest-text`]=`var(--tempest-gray-900)`,t[`--tempest-text-muted`]=`var(--tempest-gray-700)`,t[`--tempest-text-subtle`]=`var(--tempest-gray-600)`),t[`--tempest-neutral-on-solid`]=e.readableForeground(n[700],`#ffffff`,s)}function u(t,n,r,i){i===`light`?(t[`--tempest-${n}`]=r[700],t[`--tempest-${n}-fg`]=r[800],t[`--tempest-${n}-bg`]=r[50],t[`--tempest-${n}-border`]=r[200],t[`--tempest-${n}-solid`]=r[600],t[`--tempest-${n}-on-solid`]=e.readableForeground(r[600],`#ffffff`,s)):(t[`--tempest-${n}`]=r[700],t[`--tempest-${n}-fg`]=r[700],t[`--tempest-${n}-bg`]=r[50],t[`--tempest-${n}-border`]=r[200],t[`--tempest-${n}-solid`]=r[500],t[`--tempest-${n}-on-solid`]=e.readableForeground(r[500],`#ffffff`,s))}function d(e,t){let n=Object.entries(t);return n.length===0?``:`${e} {\n${n.map(([e,t])=>` ${e}: ${t};`).join(`
|
|
2
|
+
`)}\n}`}function f(r={}){let{primary:a,gray:o,chart:s,radius:f,focusRingAlpha:p=.35,selector:m=`:root`,darkSelector:h=`[data-tempest-theme="dark"]`}=r,g={},_={};if(a){let t=e.createColorScale(a,`light`),n=e.createColorScale(a,`dark`);i(g,`primary`,t),i(_,`primary`,n),c(g,t,`light`,p),c(_,n,`dark`,p)}if(o){let t=e.createColorScale(o,`light`,{anchor:!1,neutral:!0}),n=e.createColorScale(o,`dark`,{anchor:!1,neutral:!0});i(g,`gray`,t),i(_,`gray`,n),l(g,t,`light`),l(_,n,`dark`)}for(let t of[`success`,`warning`,`danger`,`info`]){let n=r[t];n&&(u(g,t,e.createColorScale(n,`light`),`light`),u(_,t,e.createColorScale(n,`dark`),`dark`))}s?.length&&(s.forEach((e,t)=>{g[`--tempest-chart-${t+1}`]=e}),g[`--tempest-chart-count`]=String(s.length));let v=s?.[0]??a;if(v){let e=t.hueOf(v),n=r.danger?t.hueOf(r.danger):(e+180)%360;for(let[r,i]of[[g,`light`],[_,`dark`]]){t.buildRamp(e,i).forEach((e,t)=>{r[`--tempest-chart-sequential-${t+1}`]=e});let a=t.buildDivergingRamp({coolHue:e,warmHue:n,mid:i===`light`?`#e4e7ec`:`#262d3f`,mode:i});[...a.cool,a.mid,...a.warm].forEach((e,t)=>{r[`--tempest-chart-diverging-${t+1}`]=e})}}if(f){let e=typeof f==`string`?n[f]:f;for(let[t,n]of Object.entries(e))g[`--tempest-radius-${t}`]=n}return{light:g,dark:_,css:[d(m,g),d(h,_)].filter(Boolean).join(`
|
|
3
3
|
|
|
4
|
-
`)}}function
|
|
4
|
+
`)}}function p(t){if(!t.primary)return null;let n=e.createColorScale(t.primary,`light`);return e.contrastRatio(n[500],e.readableForeground(n[500]))}exports.createTheme=f,exports.themeContrast=p;
|
|
5
5
|
//# sourceMappingURL=create-theme.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-theme.cjs","names":[],"sources":["../../src/theme/create-theme.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — one pass that emits the whole token set — surfaces,\n * text, borders, states, the chart ramps — for both modes, with every value checked\n * against the surface it will sit on. The checks are what make the length: each\n * token is derived and then verified against the same background table.\n */\n/**\n * Theme factory: turns a handful of brand colors into the full set of\n * `--tempest-*` token overrides, for both light and dark.\n *\n * Rebranding used to mean hand-writing ~167 custom properties (and getting the\n * dark ramp inversion right by hand). `createTheme({ primary: \"#7c3aed\" })`\n * emits the same thing, derived in OKLCH so the ramp stays perceptually even.\n */\nimport {\n contrastRatio,\n createColorScale,\n hexToRgbaString,\n readableForeground,\n type ColorScale,\n type ScaleStep,\n} from \"./color\";\nimport { buildDivergingRamp, buildRamp, hueOf } from \"./data-viz-ramps\";\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 /** Alpha of `--tempest-focus-ring-color`, derived from the brand color. Default `0.35`. */\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/**\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 */\nfunction writePrimaryAliases(\n tokens: Record<string, string>,\n scale: ColorScale,\n scheme: \"light\" | \"dark\",\n focusRingAlpha: number,\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 tokens[\"--tempest-focus-ring-color\"] = hexToRgbaString(scale[500], 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}\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 */\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 } 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 }\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 = 0.35,\n selector = \":root\",\n darkSelector = '[data-tempest-theme=\"dark\"]',\n } = options;\n\n const light: Record<string, string> = {};\n const dark: Record<string, string> = {};\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(light, lightScale, \"light\", focusRingAlpha);\n writePrimaryAliases(dark, darkScale, \"dark\", focusRingAlpha);\n }\n\n if (gray) {\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 lightScale = createColorScale(gray, \"light\", { anchor: false, neutral: true });\n const darkScale = createColorScale(gray, \"dark\", { anchor: false, neutral: true });\n writeScale(light, \"gray\", lightScale);\n writeScale(dark, \"gray\", darkScale);\n writeNeutralAliases(light, lightScale, \"light\");\n writeNeutralAliases(dark, darkScale, \"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\";\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 /** Alpha of `--tempest-focus-ring-color`, derived from the brand color. Default `0.35`. */\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/**\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 */\nfunction writePrimaryAliases(\n tokens: Record<string, string>,\n scale: ColorScale,\n scheme: \"light\" | \"dark\",\n focusRingAlpha: number,\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 tokens[\"--tempest-focus-ring-color\"] = hexToRgbaString(scale[500], 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 = 0.35,\n selector = \":root\",\n darkSelector = '[data-tempest-theme=\"dark\"]',\n } = options;\n\n const light: Record<string, string> = {};\n const dark: Record<string, string> = {};\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(light, lightScale, \"light\", focusRingAlpha);\n writePrimaryAliases(dark, darkScale, \"dark\", focusRingAlpha);\n }\n\n if (gray) {\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 lightScale = createColorScale(gray, \"light\", { anchor: false, neutral: true });\n const darkScale = createColorScale(gray, \"dark\", { anchor: false, neutral: true });\n writeScale(light, \"gray\", lightScale);\n writeScale(dark, \"gray\", darkScale);\n writeNeutralAliases(light, lightScale, \"light\");\n writeNeutralAliases(dark, darkScale, \"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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"}
|
|
@@ -70,7 +70,8 @@ function d(t, n, r) {
|
|
|
70
70
|
for (let i of r) if (e(t[n], t[i]) >= u) return i;
|
|
71
71
|
return r[r.length - 1];
|
|
72
72
|
}
|
|
73
|
-
|
|
73
|
+
var f = "#1f0606";
|
|
74
|
+
function p(e, t, i, a) {
|
|
74
75
|
e["--tempest-primary"] = "var(--tempest-primary-500)", i === "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-${d(t, 50, [
|
|
75
76
|
600,
|
|
76
77
|
700,
|
|
@@ -84,21 +85,21 @@ function f(e, t, i, a) {
|
|
|
84
85
|
let o = r(t[500]);
|
|
85
86
|
e["--tempest-primary-foreground"] = o, e["--tempest-text-on-primary"] = o, e["--tempest-focus-ring-color"] = n(t[500], a);
|
|
86
87
|
}
|
|
87
|
-
function
|
|
88
|
-
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)");
|
|
88
|
+
function m(e, t, n) {
|
|
89
|
+
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"] = r(t[700], "#ffffff", f);
|
|
89
90
|
}
|
|
90
|
-
function
|
|
91
|
-
|
|
91
|
+
function h(e, t, n, i) {
|
|
92
|
+
i === "light" ? (e[`--tempest-${t}`] = n[700], e[`--tempest-${t}-fg`] = n[800], e[`--tempest-${t}-bg`] = n[50], e[`--tempest-${t}-border`] = n[200], e[`--tempest-${t}-solid`] = n[600], e[`--tempest-${t}-on-solid`] = r(n[600], "#ffffff", f)) : (e[`--tempest-${t}`] = n[700], e[`--tempest-${t}-fg`] = n[700], e[`--tempest-${t}-bg`] = n[50], e[`--tempest-${t}-border`] = n[200], e[`--tempest-${t}-solid`] = n[500], e[`--tempest-${t}-on-solid`] = r(n[500], "#ffffff", f));
|
|
92
93
|
}
|
|
93
|
-
function
|
|
94
|
+
function g(e, t) {
|
|
94
95
|
let n = Object.entries(t);
|
|
95
96
|
return n.length === 0 ? "" : `${e} {\n${n.map(([e, t]) => ` ${e}: ${t};`).join("\n")}\n}`;
|
|
96
97
|
}
|
|
97
|
-
function
|
|
98
|
-
let { primary: n, gray: r, chart: c, radius: u, focusRingAlpha: d = .35, selector:
|
|
98
|
+
function _(e = {}) {
|
|
99
|
+
let { primary: n, gray: r, chart: c, radius: u, focusRingAlpha: d = .35, selector: f = ":root", darkSelector: _ = "[data-tempest-theme=\"dark\"]" } = e, v = {}, y = {};
|
|
99
100
|
if (n) {
|
|
100
101
|
let e = t(n, "light"), r = t(n, "dark");
|
|
101
|
-
l(v, "primary", e), l(y, "primary", r),
|
|
102
|
+
l(v, "primary", e), l(y, "primary", r), p(v, e, "light", d), p(y, r, "dark", d);
|
|
102
103
|
}
|
|
103
104
|
if (r) {
|
|
104
105
|
let e = t(r, "light", {
|
|
@@ -108,7 +109,7 @@ function g(e = {}) {
|
|
|
108
109
|
anchor: !1,
|
|
109
110
|
neutral: !0
|
|
110
111
|
});
|
|
111
|
-
l(v, "gray", e), l(y, "gray", n),
|
|
112
|
+
l(v, "gray", e), l(y, "gray", n), m(v, e, "light"), m(y, n, "dark");
|
|
112
113
|
}
|
|
113
114
|
for (let n of [
|
|
114
115
|
"success",
|
|
@@ -117,7 +118,7 @@ function g(e = {}) {
|
|
|
117
118
|
"info"
|
|
118
119
|
]) {
|
|
119
120
|
let r = e[n];
|
|
120
|
-
r && (
|
|
121
|
+
r && (h(v, n, t(r, "light"), "light"), h(y, n, t(r, "dark"), "dark"));
|
|
121
122
|
}
|
|
122
123
|
c?.length && (c.forEach((e, t) => {
|
|
123
124
|
v[`--tempest-chart-${t + 1}`] = e;
|
|
@@ -151,15 +152,15 @@ function g(e = {}) {
|
|
|
151
152
|
return {
|
|
152
153
|
light: v,
|
|
153
154
|
dark: y,
|
|
154
|
-
css: [
|
|
155
|
+
css: [g(f, v), g(_, y)].filter(Boolean).join("\n\n")
|
|
155
156
|
};
|
|
156
157
|
}
|
|
157
|
-
function
|
|
158
|
+
function v(n) {
|
|
158
159
|
if (!n.primary) return null;
|
|
159
160
|
let i = t(n.primary, "light");
|
|
160
161
|
return e(i[500], r(i[500]));
|
|
161
162
|
}
|
|
162
163
|
//#endregion
|
|
163
|
-
export {
|
|
164
|
+
export { _ as createTheme, v as themeContrast };
|
|
164
165
|
|
|
165
166
|
//# sourceMappingURL=create-theme.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-theme.js","names":[],"sources":["../../src/theme/create-theme.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — one pass that emits the whole token set — surfaces,\n * text, borders, states, the chart ramps — for both modes, with every value checked\n * against the surface it will sit on. The checks are what make the length: each\n * token is derived and then verified against the same background table.\n */\n/**\n * Theme factory: turns a handful of brand colors into the full set of\n * `--tempest-*` token overrides, for both light and dark.\n *\n * Rebranding used to mean hand-writing ~167 custom properties (and getting the\n * dark ramp inversion right by hand). `createTheme({ primary: \"#7c3aed\" })`\n * emits the same thing, derived in OKLCH so the ramp stays perceptually even.\n */\nimport {\n contrastRatio,\n createColorScale,\n hexToRgbaString,\n readableForeground,\n type ColorScale,\n type ScaleStep,\n} from \"./color\";\nimport { buildDivergingRamp, buildRamp, hueOf } from \"./data-viz-ramps\";\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 /** Alpha of `--tempest-focus-ring-color`, derived from the brand color. Default `0.35`. */\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/**\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 */\nfunction writePrimaryAliases(\n tokens: Record<string, string>,\n scale: ColorScale,\n scheme: \"light\" | \"dark\",\n focusRingAlpha: number,\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 tokens[\"--tempest-focus-ring-color\"] = hexToRgbaString(scale[500], 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}\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 */\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 } 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 }\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 = 0.35,\n selector = \":root\",\n darkSelector = '[data-tempest-theme=\"dark\"]',\n } = options;\n\n const light: Record<string, string> = {};\n const dark: Record<string, string> = {};\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(light, lightScale, \"light\", focusRingAlpha);\n writePrimaryAliases(dark, darkScale, \"dark\", focusRingAlpha);\n }\n\n if (gray) {\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 lightScale = createColorScale(gray, \"light\", { anchor: false, neutral: true });\n const darkScale = createColorScale(gray, \"dark\", { anchor: false, neutral: true });\n writeScale(light, \"gray\", lightScale);\n writeScale(dark, \"gray\", darkScale);\n writeNeutralAliases(light, lightScale, \"light\");\n writeNeutralAliases(dark, darkScale, \"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\";\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 /** Alpha of `--tempest-focus-ring-color`, derived from the brand color. Default `0.35`. */\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/**\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 */\nfunction writePrimaryAliases(\n tokens: Record<string, string>,\n scale: ColorScale,\n scheme: \"light\" | \"dark\",\n focusRingAlpha: number,\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 tokens[\"--tempest-focus-ring-color\"] = hexToRgbaString(scale[500], 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 = 0.35,\n selector = \":root\",\n darkSelector = '[data-tempest-theme=\"dark\"]',\n } = options;\n\n const light: Record<string, string> = {};\n const dark: Record<string, string> = {};\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(light, lightScale, \"light\", focusRingAlpha);\n writePrimaryAliases(dark, darkScale, \"dark\", focusRingAlpha);\n }\n\n if (gray) {\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 lightScale = createColorScale(gray, \"light\", { anchor: false, neutral: true });\n const darkScale = createColorScale(gray, \"dark\", { anchor: false, neutral: true });\n writeScale(light, \"gray\", lightScale);\n writeScale(dark, \"gray\", darkScale);\n writeNeutralAliases(light, lightScale, \"light\");\n writeNeutralAliases(dark, darkScale, \"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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"}
|
package/dist/utils/dev-mode.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
function e(){try{return process.env.NODE_ENV!==`production`}catch{return!1}}exports.isDevBuild=
|
|
1
|
+
var e;function t(t){e=t}function n(){if(e!==void 0)return e;try{return process.env.NODE_ENV!==`production`}catch{return!1}}exports.isDevBuild=n,exports.setDevBuild=t;
|
|
2
2
|
//# sourceMappingURL=dev-mode.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev-mode.cjs","names":[],"sources":["../../src/utils/dev-mode.ts"],"sourcesContent":["/**\n *
|
|
1
|
+
{"version":3,"file":"dev-mode.cjs","names":[],"sources":["../../src/utils/dev-mode.ts"],"sourcesContent":["/**\n * {@link isDevBuild} stays internal, imported by path (`../utils/dev-mode`)\n * rather than through the `utils` barrel: re-exporting it would make a one-line\n * env read part of the package's public API, with the semver weight that\n * carries, for something no consumer asked for. Every dev-only diagnostic in\n * the SDK routes through it — `grep -rn \"dev-mode\" src/` for the current list,\n * which an enumeration written here would only drift away from.\n *\n * {@link setDevBuild} is public, because the environment the SDK cannot read is\n * the one it most needs to: a Vite app. See its doc for why.\n */\n\n/**\n * Whether the consuming app was built for development.\n *\n * Reads `process.env.NODE_ENV`, which every supported bundler (Vite, webpack,\n * Rspack, Parcel) replaces with a literal **while building the app**. That\n * timing is the whole point: `import.meta.env.DEV` looks equivalent and is not,\n * because Vite replaces it while building *this package*, so a published\n * artifact carries the constant `false` and every guard behind it becomes dead\n * code the app's own dev server can no longer switch on.\n *\n * The expression is written out in full, and the failure is caught rather than\n * guarded against. A `typeof process === \"undefined\"` check would read as the\n * careful version and quietly reintroduce the bug: bundlers substitute the\n * member expression `process.env.NODE_ENV` and nothing else, so in a browser —\n * where the identifier `process` does not exist — the guard would return early\n * while the literal right after it had already been replaced with\n * `\"development\"`.\n *\n * Returns `false` when the read throws, which is the environment that defines\n * neither symbol: a raw service-worker context, a plain\n * `<script type=\"module\">`, a bundler substituting nothing. **A Vite app is in\n * that set**, `vite dev` included — Vite replaces neither `process` nor\n * `process.env.NODE_ENV` in the app it builds, so the read throws there like\n * anywhere else. Staying quiet is deliberate — a dev-only warning that cannot\n * prove it is in development is better silent than shouting in someone's\n * production console.\n *\n * {@link setDevBuild} is how that app says so, and it is checked first. The\n * automatic read stays exactly as written underneath, bare member expression\n * and all: a `typeof process === \"undefined\"` guard in front of it would return\n * early in a webpack build whose literal had already been substituted, which is\n * the one environment this currently gets right.\n *\n * @returns Whether development-only diagnostics should run.\n *\n * @example\n * if (isDevBuild()) console.warn(\"[my-app] this prop combination does nothing\");\n *\n * @tempest-limits empty-catch — the only thing the read can throw is the\n * environment answering \"not defined\", which is the return value, not an error\n * worth reporting. Logging it would print on every call in exactly the context\n * that has nowhere to print.\n */\nlet configuredDevBuild: boolean | undefined;\n\n/**\n * Tell the SDK whether the app around it was built for development.\n *\n * Call it once, at bootstrap, from a build the SDK cannot inspect:\n *\n * ```ts\n * import { setDevBuild } from \"tempest-react-sdk\";\n *\n * setDevBuild(import.meta.env.DEV);\n * ```\n *\n * **Why the SDK cannot work this out on its own in a Vite app.** The automatic\n * detection reads `process.env.NODE_ENV`, which webpack, Rspack and Parcel\n * replace with a literal while building the *app*. Vite replaces neither half:\n * `process` is not defined in a browser bundle, so the read throws and the\n * answer is `false` — including under `vite dev`. Its own signal,\n * `import.meta.env.DEV`, cannot be used here either, because Vite would replace\n * it while building *this package* and the published artifact would ship the\n * constant. Only the app is compiled at the moment the answer is knowable, so\n * only the app can supply it.\n *\n * The default stays `false` on purpose. `parseResponse` puts the raw response\n * payload in its message when this is on, so a wrong guess in the other\n * direction leaks a payload into a production error string. Silence is the safe\n * default; the report is one line away for anyone who wants it.\n *\n * Passing `undefined` clears the override and returns to automatic detection,\n * which is what a test that set it should do on the way out.\n *\n * @param value - `true` for a development build, `false` for production,\n * `undefined` to go back to detecting it.\n *\n * @example\n * // Vite — the case this exists for\n * setDevBuild(import.meta.env.DEV);\n *\n * @example\n * // A test that flips it, and puts it back\n * afterEach(() => setDevBuild(undefined));\n */\nexport function setDevBuild(value: boolean | undefined): void {\n configuredDevBuild = value;\n}\n\nexport function isDevBuild(): boolean {\n if (configuredDevBuild !== undefined) return configuredDevBuild;\n try {\n return process.env.NODE_ENV !== \"production\";\n } catch {\n return false;\n }\n}\n"],"mappings":"AAuDA,IAAI,EA0CJ,SAAgB,EAAY,EAAkC,CAC1D,EAAqB,CACzB,CAEA,SAAgB,GAAsB,CAClC,GAAI,IAAuB,IAAA,GAAW,OAAO,EAC7C,GAAI,CACA,OAAA,QAAA,IAAA,WAAgC,YACpC,MAAQ,CACJ,MAAO,EACX,CACJ"}
|
package/dist/utils/dev-mode.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
//#region src/utils/dev-mode.ts
|
|
2
|
-
|
|
2
|
+
var e;
|
|
3
|
+
function t(t) {
|
|
4
|
+
e = t;
|
|
5
|
+
}
|
|
6
|
+
function n() {
|
|
7
|
+
if (e !== void 0) return e;
|
|
3
8
|
try {
|
|
4
9
|
return process.env.NODE_ENV !== "production";
|
|
5
10
|
} catch {
|
|
@@ -7,6 +12,6 @@ function e() {
|
|
|
7
12
|
}
|
|
8
13
|
}
|
|
9
14
|
//#endregion
|
|
10
|
-
export {
|
|
15
|
+
export { n as isDevBuild, t as setDevBuild };
|
|
11
16
|
|
|
12
17
|
//# sourceMappingURL=dev-mode.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev-mode.js","names":[],"sources":["../../src/utils/dev-mode.ts"],"sourcesContent":["/**\n *
|
|
1
|
+
{"version":3,"file":"dev-mode.js","names":[],"sources":["../../src/utils/dev-mode.ts"],"sourcesContent":["/**\n * {@link isDevBuild} stays internal, imported by path (`../utils/dev-mode`)\n * rather than through the `utils` barrel: re-exporting it would make a one-line\n * env read part of the package's public API, with the semver weight that\n * carries, for something no consumer asked for. Every dev-only diagnostic in\n * the SDK routes through it — `grep -rn \"dev-mode\" src/` for the current list,\n * which an enumeration written here would only drift away from.\n *\n * {@link setDevBuild} is public, because the environment the SDK cannot read is\n * the one it most needs to: a Vite app. See its doc for why.\n */\n\n/**\n * Whether the consuming app was built for development.\n *\n * Reads `process.env.NODE_ENV`, which every supported bundler (Vite, webpack,\n * Rspack, Parcel) replaces with a literal **while building the app**. That\n * timing is the whole point: `import.meta.env.DEV` looks equivalent and is not,\n * because Vite replaces it while building *this package*, so a published\n * artifact carries the constant `false` and every guard behind it becomes dead\n * code the app's own dev server can no longer switch on.\n *\n * The expression is written out in full, and the failure is caught rather than\n * guarded against. A `typeof process === \"undefined\"` check would read as the\n * careful version and quietly reintroduce the bug: bundlers substitute the\n * member expression `process.env.NODE_ENV` and nothing else, so in a browser —\n * where the identifier `process` does not exist — the guard would return early\n * while the literal right after it had already been replaced with\n * `\"development\"`.\n *\n * Returns `false` when the read throws, which is the environment that defines\n * neither symbol: a raw service-worker context, a plain\n * `<script type=\"module\">`, a bundler substituting nothing. **A Vite app is in\n * that set**, `vite dev` included — Vite replaces neither `process` nor\n * `process.env.NODE_ENV` in the app it builds, so the read throws there like\n * anywhere else. Staying quiet is deliberate — a dev-only warning that cannot\n * prove it is in development is better silent than shouting in someone's\n * production console.\n *\n * {@link setDevBuild} is how that app says so, and it is checked first. The\n * automatic read stays exactly as written underneath, bare member expression\n * and all: a `typeof process === \"undefined\"` guard in front of it would return\n * early in a webpack build whose literal had already been substituted, which is\n * the one environment this currently gets right.\n *\n * @returns Whether development-only diagnostics should run.\n *\n * @example\n * if (isDevBuild()) console.warn(\"[my-app] this prop combination does nothing\");\n *\n * @tempest-limits empty-catch — the only thing the read can throw is the\n * environment answering \"not defined\", which is the return value, not an error\n * worth reporting. Logging it would print on every call in exactly the context\n * that has nowhere to print.\n */\nlet configuredDevBuild: boolean | undefined;\n\n/**\n * Tell the SDK whether the app around it was built for development.\n *\n * Call it once, at bootstrap, from a build the SDK cannot inspect:\n *\n * ```ts\n * import { setDevBuild } from \"tempest-react-sdk\";\n *\n * setDevBuild(import.meta.env.DEV);\n * ```\n *\n * **Why the SDK cannot work this out on its own in a Vite app.** The automatic\n * detection reads `process.env.NODE_ENV`, which webpack, Rspack and Parcel\n * replace with a literal while building the *app*. Vite replaces neither half:\n * `process` is not defined in a browser bundle, so the read throws and the\n * answer is `false` — including under `vite dev`. Its own signal,\n * `import.meta.env.DEV`, cannot be used here either, because Vite would replace\n * it while building *this package* and the published artifact would ship the\n * constant. Only the app is compiled at the moment the answer is knowable, so\n * only the app can supply it.\n *\n * The default stays `false` on purpose. `parseResponse` puts the raw response\n * payload in its message when this is on, so a wrong guess in the other\n * direction leaks a payload into a production error string. Silence is the safe\n * default; the report is one line away for anyone who wants it.\n *\n * Passing `undefined` clears the override and returns to automatic detection,\n * which is what a test that set it should do on the way out.\n *\n * @param value - `true` for a development build, `false` for production,\n * `undefined` to go back to detecting it.\n *\n * @example\n * // Vite — the case this exists for\n * setDevBuild(import.meta.env.DEV);\n *\n * @example\n * // A test that flips it, and puts it back\n * afterEach(() => setDevBuild(undefined));\n */\nexport function setDevBuild(value: boolean | undefined): void {\n configuredDevBuild = value;\n}\n\nexport function isDevBuild(): boolean {\n if (configuredDevBuild !== undefined) return configuredDevBuild;\n try {\n return process.env.NODE_ENV !== \"production\";\n } catch {\n return false;\n }\n}\n"],"mappings":";AAuDA,IAAI;AA0CJ,SAAgB,EAAY,GAAkC;CAC1D,IAAqB;AACzB;AAEA,SAAgB,IAAsB;CAClC,IAAI,MAAuB,KAAA,GAAW,OAAO;CAC7C,IAAI;EACA,OAAA,QAAA,IAAA,aAAgC;CACpC,QAAQ;EACJ,OAAO;CACX;AACJ"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
let e=require("node:path"),t=require("node:fs/promises"),n=require("node:fs"),r=require("node:url");var i=`tempest-react-sdk/styles/auto.css`,a=`\0tempest-styles/auto.css`,o=/^tempest-react-sdk(\/(charts|editor|br|icons))?$/,s=/\.[cm]?[jt]sx?$/,c=new Set([`node_modules`,`dist`,`build`,`coverage`]);function l(e){let t=new Set;for(let n of e.matchAll(/\bimport\s+(type\s+)?([^;'"]*?)\s+from\s*["']([^"']+)["']/g)){let[,e,r,i]=n;if(!o.test(i??``)||e)continue;if(/^\s*\*\s+as\s/.test(r??``))return null;let a=/\{([^}]*)\}/.exec(r??``);if(a)for(let e of(a[1]??``).split(`,`)){let n=e.trim();if(!n||n.startsWith(`type `))continue;let r=n.split(/\s+as\s+/)[0]?.trim();r&&t.add(r)}}return[...t]}function u(e,t,n=`scoped`){let r=`/* Generated by tempestStyles() — the CSS this app actually uses. */
|
|
2
|
+
`,i=n===`global`?[t.core]:n===`scoped`?[`tokens.css`,`scoped.css`]:[`tokens.css`];if(e===null)return`${r}${(n===`global`?[`../styles.css`]:[...i,`../styles.css`]).map(e=>`@import "tempest-react-sdk/styles/${e}";`).join(`
|
|
3
|
+
`)}\n`;let a=new Set;for(let n of e)for(let e of t.components[n]??[])a.add(e);return`${r}${[...i,...[...a].sort()].map(e=>`@import "tempest-react-sdk/styles/${e}";`).join(`
|
|
4
|
+
`)}\n`}async function d(n,r){let i=[],a;try{a=await(0,t.readdir)(n,{withFileTypes:!0})}catch{return i}for(let t of a)if(t.isDirectory()){if(r.has(t.name)||t.name.startsWith(`.`))continue;i.push(...await d((0,e.join)(n,t.name),r))}else t.isFile()&&s.test(t.name)&&i.push((0,e.join)(n,t.name));return i}function f(t){let i=(0,e.join)(`tempest-react-sdk`,`dist`,`styles`,`manifest.json`),a=[],o=(0,e.resolve)(t);for(;;){a.push((0,e.join)(o,`node_modules`,i));let t=(0,e.dirname)(o);if(t===o)break;o=t}a.push((0,e.join)((0,e.dirname)((0,r.fileURLToPath)({}.url)),`..`,`styles`,`manifest.json`));for(let e of a)try{return JSON.parse((0,n.readFileSync)(e,`utf8`))}catch{continue}throw Error(`tempestStyles(): tempest-react-sdk/dist/styles/manifest.json not found — the installed SDK predates it. Upgrade, or drop the plugin and import tempest-react-sdk/styles.css.`)}function p(n={}){let{dir:r=`src`,include:i=[],skipDirs:o,reset:s=`scoped`}=n,p=new Set(o??c),m=process.cwd(),h=[...i],g=async()=>{let n=new Set(i);for(let i of await d((0,e.resolve)(m,r),p)){let e;try{e=await(0,t.readFile)(i,`utf8`)}catch{continue}let r=l(e);if(r===null){h=null;return}for(let e of r)n.add(e)}h=[...n]};return{name:`tempest-styles`,enforce:`pre`,configResolved(e){m=e.root??m},async buildStart(){await g()},resolveId(e){return e===`tempest-react-sdk/styles/auto.css`?a:null},load(e){return e===a?u(h,f(m),s):null}}}exports.TEMPEST_STYLES_ID=i,exports.buildStyleEntry=u,exports.scanStyleImports=l,exports.tempestStyles=p;
|
|
5
|
+
//# sourceMappingURL=tempest-styles.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tempest-styles.cjs","names":[],"sources":["../../src/vite/tempest-styles.ts"],"sourcesContent":["import { readdir, readFile } from \"node:fs/promises\";\nimport { readFileSync } from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport type { TempestVitePlugin } from \"./tempest-pwa-manifest\";\n\n/** The stylesheet id an app imports to get exactly the CSS it uses. */\nexport const TEMPEST_STYLES_ID = \"tempest-react-sdk/styles/auto.css\";\n\nconst RESOLVED_ID = \"\\0tempest-styles/auto.css\";\n\n/** Package subpaths whose exports are components with stylesheets. */\nconst SDK_SPECIFIER = /^tempest-react-sdk(\\/(charts|editor|br|icons))?$/;\n\nconst SOURCE_FILE = /\\.[cm]?[jt]sx?$/;\nconst SKIP_DIRS = new Set([\"node_modules\", \"dist\", \"build\", \"coverage\"]);\n\n/**\n * How much of the reset an app takes.\n *\n * The components are written against the reset — `.tempest_button` assumes\n * `button { background: none; border: 0; padding: 0 }`, every width assumes\n * `box-sizing: border-box` — so `\"none\"` is not \"unstyled document, styled\n * components\": it is components missing their box model. It exists for the app\n * that already ships an equivalent reset of its own.\n */\nexport type ResetMode = \"scoped\" | \"global\" | \"none\";\n\n/** Shape of `dist/styles/manifest.json`. */\nexport interface StyleManifest {\n /** The foundation sheet — tokens and reset together, the pre-split default. */\n core: string;\n /** Public export name → the stylesheets it needs, transitive closure included. */\n components: Record<string, readonly string[]>;\n}\n\nexport interface TempestStylesOptions {\n /** Directory to scan, relative to the Vite root. Default: `\"src\"`. */\n dir?: string;\n /**\n * Which reset to emit alongside the tokens. Default: `\"scoped\"`.\n *\n * `\"scoped\"` confines the reset to `:where([class*=\"tempest_\"])`, so components\n * keep the normalisation they are written against and the app keeps `html`,\n * `body` and `#root`. `\"global\"` is the pre-split behaviour, correct when the\n * SDK owns the whole page. `\"none\"` emits tokens only.\n */\n reset?: ResetMode;\n /**\n * Extra export names to style even when the scan cannot see them — a component\n * reached only through a namespace import, or rendered by a sibling package.\n */\n include?: readonly string[];\n /** Directory names to skip. Default: `node_modules`, `dist`, `build`, `coverage`. */\n skipDirs?: readonly string[];\n}\n\n/**\n * Collect the SDK export names a source file imports.\n *\n * Reads the import specifier rather than the JSX, because a bare `<Button>` in the\n * markup says nothing about where it came from — the app may well define its own.\n * Type-only bindings are dropped: they are erased before any component renders, so\n * paying for their CSS would be paying for nothing.\n *\n * @param code - File contents.\n * @returns The imported names, or `null` when the file namespace-imports the SDK\n * and the set therefore cannot be known.\n */\nexport function scanStyleImports(code: string): string[] | null {\n const names = new Set<string>();\n\n for (const match of code.matchAll(\n /\\bimport\\s+(type\\s+)?([^;'\"]*?)\\s+from\\s*[\"']([^\"']+)[\"']/g,\n )) {\n const [, typeOnly, clause, specifier] = match;\n if (!SDK_SPECIFIER.test(specifier ?? \"\")) continue;\n if (typeOnly) continue;\n if (/^\\s*\\*\\s+as\\s/.test(clause ?? \"\")) return null;\n\n const braces = /\\{([^}]*)\\}/.exec(clause ?? \"\");\n if (!braces) continue;\n for (const part of (braces[1] ?? \"\").split(\",\")) {\n const binding = part.trim();\n if (!binding || binding.startsWith(\"type \")) continue;\n const name = binding.split(/\\s+as\\s+/)[0]?.trim();\n if (name) names.add(name);\n }\n }\n return [...names];\n}\n\n/**\n * Build the stylesheet for a set of export names.\n *\n * Emits `@import` statements rather than inlined rules so Vite's own CSS pipeline\n * resolves, deduplicates and minifies them — the same treatment a hand-written\n * list of imports gets, which is what this replaces.\n *\n * @param names - Export names the app imports, or `null` to take everything.\n * @param manifest - The published style manifest.\n * @returns CSS source.\n */\nexport function buildStyleEntry(\n names: readonly string[] | null,\n manifest: StyleManifest,\n reset: ResetMode = \"scoped\",\n): string {\n const banner = \"/* Generated by tempestStyles() — the CSS this app actually uses. */\\n\";\n const foundation =\n reset === \"global\"\n ? [manifest.core]\n : reset === \"scoped\"\n ? [\"tokens.css\", \"scoped.css\"]\n : [\"tokens.css\"];\n\n if (names === null) {\n const whole = reset === \"global\" ? [\"../styles.css\"] : [...foundation, \"../styles.css\"];\n return `${banner}${whole.map((sheet) => `@import \"tempest-react-sdk/styles/${sheet}\";`).join(\"\\n\")}\\n`;\n }\n\n const sheets = new Set<string>();\n for (const name of names) {\n for (const sheet of manifest.components[name] ?? []) sheets.add(sheet);\n }\n const lines = [...foundation, ...[...sheets].sort()].map(\n (sheet) => `@import \"tempest-react-sdk/styles/${sheet}\";`,\n );\n return `${banner}${lines.join(\"\\n\")}\\n`;\n}\n\n/**\n * Walk a directory and collect every source file path.\n *\n * @param dir - Absolute directory to walk.\n * @param skip - Directory names to skip.\n * @returns Absolute file paths.\n */\nasync function collectSourceFiles(dir: string, skip: ReadonlySet<string>): Promise<string[]> {\n const out: string[] = [];\n let entries;\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch {\n return out;\n }\n for (const entry of entries) {\n if (entry.isDirectory()) {\n if (skip.has(entry.name) || entry.name.startsWith(\".\")) continue;\n out.push(...(await collectSourceFiles(join(dir, entry.name), skip)));\n } else if (entry.isFile() && SOURCE_FILE.test(entry.name)) {\n out.push(join(dir, entry.name));\n }\n }\n return out;\n}\n\n/**\n * Read the style manifest the installed package ships.\n *\n * Resolved by walking the filesystem rather than through `createRequire`, which\n * does not survive the trip: Vite bundles `vite.config.ts` into a temporary module\n * before running it, and in that bundle the `node:module` namespace import comes\n * back without the function — `(0, a.createRequire) is not a function`, at load\n * time, inside the build. The same rewrite is why `import.meta.url` is only the\n * fallback here and the app's `node_modules` is tried first.\n *\n * @param root - The Vite root, where the walk for `node_modules` starts.\n * @returns The manifest.\n * @throws If no installed copy carries one — an SDK older than the manifest.\n */\nfunction readManifest(root: string): StyleManifest {\n const relative = join(\"tempest-react-sdk\", \"dist\", \"styles\", \"manifest.json\");\n const candidates: string[] = [];\n\n let dir = resolve(root);\n for (;;) {\n candidates.push(join(dir, \"node_modules\", relative));\n const parent = dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n candidates.push(join(dirname(fileURLToPath(import.meta.url)), \"..\", \"styles\", \"manifest.json\"));\n\n for (const candidate of candidates) {\n try {\n return JSON.parse(readFileSync(candidate, \"utf8\")) as StyleManifest;\n } catch {\n continue;\n }\n }\n throw new Error(\n \"tempestStyles(): tempest-react-sdk/dist/styles/manifest.json not found — \" +\n \"the installed SDK predates it. Upgrade, or drop the plugin and import \" +\n \"tempest-react-sdk/styles.css.\",\n );\n}\n\n/**\n * Import only the SDK stylesheets your app can actually reach.\n *\n * `styles.css` carries every component the SDK ships — measured at 236.71 kB raw\n * against the 38.94 kB an app mounting twelve of them can reach. The per-component\n * sheets under `styles/` have always closed that gap, but by hand: the app lists\n * them and keeps the list honest as it grows. This plugin makes the list the build's\n * problem, resolving it from the imports the source already writes.\n *\n * The closure matters more than the scan. A component pays for the CSS of every SDK\n * component it renders internally, so `<DataTable>` alone needs six sheets and\n * `<AIChat>` seven — a hand-kept list of one sheet per component the app names is\n * wrong in a way that only shows up as an unstyled child.\n *\n * The reset is the other half, and the half that breaks apps. `styles.css` claims\n * `html`, `body` and `#root`; an app with its own layout that imports it loses its\n * document surface, and an app that drops the import loses the box model its\n * components are written against — components render unstyled, which is what the\n * \"just remove the import\" fix actually costs. The default `reset: \"scoped\"` is the\n * third option: the same normalisation, confined to\n * `:where([class*=\"tempest_\"])`, so it reaches inside a Tempest component and\n * nowhere else.\n *\n * `tempest-react-sdk/styles/auto.css` is a real file, so it resolves without the\n * plugin too — to the complete sheet. Dropping the plugin costs bytes, never\n * correctness.\n *\n * @param options - Scan configuration.\n * @returns The Vite plugin.\n *\n * @example\n * // vite.config.ts\n * import { createViteConfig, tempestStyles } from \"tempest-react-sdk/vite\";\n *\n * export default createViteConfig({ plugins: [tempestStyles()] });\n *\n * @example\n * // src/main.tsx — the one import, instead of a maintained list\n * import \"tempest-react-sdk/styles/auto.css\";\n */\nexport function tempestStyles(options: TempestStylesOptions = {}): TempestVitePlugin {\n const { dir = \"src\", include = [], skipDirs, reset = \"scoped\" } = options;\n const skip = new Set<string>(skipDirs ?? SKIP_DIRS);\n let root = process.cwd();\n let names: string[] | null = [...include];\n\n /**\n * Rescan the source tree, keeping the explicitly included names.\n *\n * @tempest-limits empty-catch — the scan races the editor and the file system:\n * a file listed a moment ago can be renamed, deleted, or held by another process\n * by the time it is read. An unreadable file contributes no names and the build\n * continues; failing the whole scan would break `vite dev` over a temp file that\n * no longer exists.\n */\n const rescan = async (): Promise<void> => {\n const found = new Set<string>(include);\n for (const file of await collectSourceFiles(resolve(root, dir), skip)) {\n let code: string;\n try {\n code = await readFile(file, \"utf8\");\n } catch {\n continue;\n }\n const scanned = scanStyleImports(code);\n if (scanned === null) {\n names = null;\n return;\n }\n for (const name of scanned) found.add(name);\n }\n names = [...found];\n };\n\n return {\n name: \"tempest-styles\",\n enforce: \"pre\",\n\n configResolved(config: { root?: string }) {\n root = config.root ?? root;\n },\n\n async buildStart() {\n await rescan();\n },\n\n resolveId(id: string) {\n return id === TEMPEST_STYLES_ID ? RESOLVED_ID : null;\n },\n\n load(id: string) {\n return id === RESOLVED_ID ? buildStyleEntry(names, readManifest(root), reset) : null;\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { dirname as e, join as t, resolve as n } from "node:path";
|
|
2
|
+
import { readFile as r, readdir as i } from "node:fs/promises";
|
|
3
|
+
import { readFileSync as a } from "node:fs";
|
|
4
|
+
import { fileURLToPath as o } from "node:url";
|
|
5
|
+
//#region src/vite/tempest-styles.ts
|
|
6
|
+
var s = "tempest-react-sdk/styles/auto.css", c = "\0tempest-styles/auto.css", l = /^tempest-react-sdk(\/(charts|editor|br|icons))?$/, u = /\.[cm]?[jt]sx?$/, d = /* @__PURE__ */ new Set([
|
|
7
|
+
"node_modules",
|
|
8
|
+
"dist",
|
|
9
|
+
"build",
|
|
10
|
+
"coverage"
|
|
11
|
+
]);
|
|
12
|
+
function f(e) {
|
|
13
|
+
let t = /* @__PURE__ */ new Set();
|
|
14
|
+
for (let n of e.matchAll(/\bimport\s+(type\s+)?([^;'"]*?)\s+from\s*["']([^"']+)["']/g)) {
|
|
15
|
+
let [, e, r, i] = n;
|
|
16
|
+
if (!l.test(i ?? "") || e) continue;
|
|
17
|
+
if (/^\s*\*\s+as\s/.test(r ?? "")) return null;
|
|
18
|
+
let a = /\{([^}]*)\}/.exec(r ?? "");
|
|
19
|
+
if (a) for (let e of (a[1] ?? "").split(",")) {
|
|
20
|
+
let n = e.trim();
|
|
21
|
+
if (!n || n.startsWith("type ")) continue;
|
|
22
|
+
let r = n.split(/\s+as\s+/)[0]?.trim();
|
|
23
|
+
r && t.add(r);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return [...t];
|
|
27
|
+
}
|
|
28
|
+
function p(e, t, n = "scoped") {
|
|
29
|
+
let r = "/* Generated by tempestStyles() — the CSS this app actually uses. */\n", i = n === "global" ? [t.core] : n === "scoped" ? ["tokens.css", "scoped.css"] : ["tokens.css"];
|
|
30
|
+
if (e === null) return `${r}${(n === "global" ? ["../styles.css"] : [...i, "../styles.css"]).map((e) => `@import "tempest-react-sdk/styles/${e}";`).join("\n")}\n`;
|
|
31
|
+
let a = /* @__PURE__ */ new Set();
|
|
32
|
+
for (let n of e) for (let e of t.components[n] ?? []) a.add(e);
|
|
33
|
+
return `${r}${[...i, ...[...a].sort()].map((e) => `@import "tempest-react-sdk/styles/${e}";`).join("\n")}\n`;
|
|
34
|
+
}
|
|
35
|
+
async function m(e, n) {
|
|
36
|
+
let r = [], a;
|
|
37
|
+
try {
|
|
38
|
+
a = await i(e, { withFileTypes: !0 });
|
|
39
|
+
} catch {
|
|
40
|
+
return r;
|
|
41
|
+
}
|
|
42
|
+
for (let i of a) if (i.isDirectory()) {
|
|
43
|
+
if (n.has(i.name) || i.name.startsWith(".")) continue;
|
|
44
|
+
r.push(...await m(t(e, i.name), n));
|
|
45
|
+
} else i.isFile() && u.test(i.name) && r.push(t(e, i.name));
|
|
46
|
+
return r;
|
|
47
|
+
}
|
|
48
|
+
function h(r) {
|
|
49
|
+
let i = t("tempest-react-sdk", "dist", "styles", "manifest.json"), s = [], c = n(r);
|
|
50
|
+
for (;;) {
|
|
51
|
+
s.push(t(c, "node_modules", i));
|
|
52
|
+
let n = e(c);
|
|
53
|
+
if (n === c) break;
|
|
54
|
+
c = n;
|
|
55
|
+
}
|
|
56
|
+
s.push(t(e(o(import.meta.url)), "..", "styles", "manifest.json"));
|
|
57
|
+
for (let e of s) try {
|
|
58
|
+
return JSON.parse(a(e, "utf8"));
|
|
59
|
+
} catch {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
throw Error("tempestStyles(): tempest-react-sdk/dist/styles/manifest.json not found — the installed SDK predates it. Upgrade, or drop the plugin and import tempest-react-sdk/styles.css.");
|
|
63
|
+
}
|
|
64
|
+
function g(e = {}) {
|
|
65
|
+
let { dir: t = "src", include: i = [], skipDirs: a, reset: o = "scoped" } = e, s = new Set(a ?? d), l = process.cwd(), u = [...i], g = async () => {
|
|
66
|
+
let e = new Set(i);
|
|
67
|
+
for (let i of await m(n(l, t), s)) {
|
|
68
|
+
let t;
|
|
69
|
+
try {
|
|
70
|
+
t = await r(i, "utf8");
|
|
71
|
+
} catch {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
let n = f(t);
|
|
75
|
+
if (n === null) {
|
|
76
|
+
u = null;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
for (let t of n) e.add(t);
|
|
80
|
+
}
|
|
81
|
+
u = [...e];
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
name: "tempest-styles",
|
|
85
|
+
enforce: "pre",
|
|
86
|
+
configResolved(e) {
|
|
87
|
+
l = e.root ?? l;
|
|
88
|
+
},
|
|
89
|
+
async buildStart() {
|
|
90
|
+
await g();
|
|
91
|
+
},
|
|
92
|
+
resolveId(e) {
|
|
93
|
+
return e === "tempest-react-sdk/styles/auto.css" ? c : null;
|
|
94
|
+
},
|
|
95
|
+
load(e) {
|
|
96
|
+
return e === c ? p(u, h(l), o) : null;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
//#endregion
|
|
101
|
+
export { s as TEMPEST_STYLES_ID, p as buildStyleEntry, f as scanStyleImports, g as tempestStyles };
|
|
102
|
+
|
|
103
|
+
//# sourceMappingURL=tempest-styles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tempest-styles.js","names":[],"sources":["../../src/vite/tempest-styles.ts"],"sourcesContent":["import { readdir, readFile } from \"node:fs/promises\";\nimport { readFileSync } from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport type { TempestVitePlugin } from \"./tempest-pwa-manifest\";\n\n/** The stylesheet id an app imports to get exactly the CSS it uses. */\nexport const TEMPEST_STYLES_ID = \"tempest-react-sdk/styles/auto.css\";\n\nconst RESOLVED_ID = \"\\0tempest-styles/auto.css\";\n\n/** Package subpaths whose exports are components with stylesheets. */\nconst SDK_SPECIFIER = /^tempest-react-sdk(\\/(charts|editor|br|icons))?$/;\n\nconst SOURCE_FILE = /\\.[cm]?[jt]sx?$/;\nconst SKIP_DIRS = new Set([\"node_modules\", \"dist\", \"build\", \"coverage\"]);\n\n/**\n * How much of the reset an app takes.\n *\n * The components are written against the reset — `.tempest_button` assumes\n * `button { background: none; border: 0; padding: 0 }`, every width assumes\n * `box-sizing: border-box` — so `\"none\"` is not \"unstyled document, styled\n * components\": it is components missing their box model. It exists for the app\n * that already ships an equivalent reset of its own.\n */\nexport type ResetMode = \"scoped\" | \"global\" | \"none\";\n\n/** Shape of `dist/styles/manifest.json`. */\nexport interface StyleManifest {\n /** The foundation sheet — tokens and reset together, the pre-split default. */\n core: string;\n /** Public export name → the stylesheets it needs, transitive closure included. */\n components: Record<string, readonly string[]>;\n}\n\nexport interface TempestStylesOptions {\n /** Directory to scan, relative to the Vite root. Default: `\"src\"`. */\n dir?: string;\n /**\n * Which reset to emit alongside the tokens. Default: `\"scoped\"`.\n *\n * `\"scoped\"` confines the reset to `:where([class*=\"tempest_\"])`, so components\n * keep the normalisation they are written against and the app keeps `html`,\n * `body` and `#root`. `\"global\"` is the pre-split behaviour, correct when the\n * SDK owns the whole page. `\"none\"` emits tokens only.\n */\n reset?: ResetMode;\n /**\n * Extra export names to style even when the scan cannot see them — a component\n * reached only through a namespace import, or rendered by a sibling package.\n */\n include?: readonly string[];\n /** Directory names to skip. Default: `node_modules`, `dist`, `build`, `coverage`. */\n skipDirs?: readonly string[];\n}\n\n/**\n * Collect the SDK export names a source file imports.\n *\n * Reads the import specifier rather than the JSX, because a bare `<Button>` in the\n * markup says nothing about where it came from — the app may well define its own.\n * Type-only bindings are dropped: they are erased before any component renders, so\n * paying for their CSS would be paying for nothing.\n *\n * @param code - File contents.\n * @returns The imported names, or `null` when the file namespace-imports the SDK\n * and the set therefore cannot be known.\n */\nexport function scanStyleImports(code: string): string[] | null {\n const names = new Set<string>();\n\n for (const match of code.matchAll(\n /\\bimport\\s+(type\\s+)?([^;'\"]*?)\\s+from\\s*[\"']([^\"']+)[\"']/g,\n )) {\n const [, typeOnly, clause, specifier] = match;\n if (!SDK_SPECIFIER.test(specifier ?? \"\")) continue;\n if (typeOnly) continue;\n if (/^\\s*\\*\\s+as\\s/.test(clause ?? \"\")) return null;\n\n const braces = /\\{([^}]*)\\}/.exec(clause ?? \"\");\n if (!braces) continue;\n for (const part of (braces[1] ?? \"\").split(\",\")) {\n const binding = part.trim();\n if (!binding || binding.startsWith(\"type \")) continue;\n const name = binding.split(/\\s+as\\s+/)[0]?.trim();\n if (name) names.add(name);\n }\n }\n return [...names];\n}\n\n/**\n * Build the stylesheet for a set of export names.\n *\n * Emits `@import` statements rather than inlined rules so Vite's own CSS pipeline\n * resolves, deduplicates and minifies them — the same treatment a hand-written\n * list of imports gets, which is what this replaces.\n *\n * @param names - Export names the app imports, or `null` to take everything.\n * @param manifest - The published style manifest.\n * @returns CSS source.\n */\nexport function buildStyleEntry(\n names: readonly string[] | null,\n manifest: StyleManifest,\n reset: ResetMode = \"scoped\",\n): string {\n const banner = \"/* Generated by tempestStyles() — the CSS this app actually uses. */\\n\";\n const foundation =\n reset === \"global\"\n ? [manifest.core]\n : reset === \"scoped\"\n ? [\"tokens.css\", \"scoped.css\"]\n : [\"tokens.css\"];\n\n if (names === null) {\n const whole = reset === \"global\" ? [\"../styles.css\"] : [...foundation, \"../styles.css\"];\n return `${banner}${whole.map((sheet) => `@import \"tempest-react-sdk/styles/${sheet}\";`).join(\"\\n\")}\\n`;\n }\n\n const sheets = new Set<string>();\n for (const name of names) {\n for (const sheet of manifest.components[name] ?? []) sheets.add(sheet);\n }\n const lines = [...foundation, ...[...sheets].sort()].map(\n (sheet) => `@import \"tempest-react-sdk/styles/${sheet}\";`,\n );\n return `${banner}${lines.join(\"\\n\")}\\n`;\n}\n\n/**\n * Walk a directory and collect every source file path.\n *\n * @param dir - Absolute directory to walk.\n * @param skip - Directory names to skip.\n * @returns Absolute file paths.\n */\nasync function collectSourceFiles(dir: string, skip: ReadonlySet<string>): Promise<string[]> {\n const out: string[] = [];\n let entries;\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch {\n return out;\n }\n for (const entry of entries) {\n if (entry.isDirectory()) {\n if (skip.has(entry.name) || entry.name.startsWith(\".\")) continue;\n out.push(...(await collectSourceFiles(join(dir, entry.name), skip)));\n } else if (entry.isFile() && SOURCE_FILE.test(entry.name)) {\n out.push(join(dir, entry.name));\n }\n }\n return out;\n}\n\n/**\n * Read the style manifest the installed package ships.\n *\n * Resolved by walking the filesystem rather than through `createRequire`, which\n * does not survive the trip: Vite bundles `vite.config.ts` into a temporary module\n * before running it, and in that bundle the `node:module` namespace import comes\n * back without the function — `(0, a.createRequire) is not a function`, at load\n * time, inside the build. The same rewrite is why `import.meta.url` is only the\n * fallback here and the app's `node_modules` is tried first.\n *\n * @param root - The Vite root, where the walk for `node_modules` starts.\n * @returns The manifest.\n * @throws If no installed copy carries one — an SDK older than the manifest.\n */\nfunction readManifest(root: string): StyleManifest {\n const relative = join(\"tempest-react-sdk\", \"dist\", \"styles\", \"manifest.json\");\n const candidates: string[] = [];\n\n let dir = resolve(root);\n for (;;) {\n candidates.push(join(dir, \"node_modules\", relative));\n const parent = dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n candidates.push(join(dirname(fileURLToPath(import.meta.url)), \"..\", \"styles\", \"manifest.json\"));\n\n for (const candidate of candidates) {\n try {\n return JSON.parse(readFileSync(candidate, \"utf8\")) as StyleManifest;\n } catch {\n continue;\n }\n }\n throw new Error(\n \"tempestStyles(): tempest-react-sdk/dist/styles/manifest.json not found — \" +\n \"the installed SDK predates it. Upgrade, or drop the plugin and import \" +\n \"tempest-react-sdk/styles.css.\",\n );\n}\n\n/**\n * Import only the SDK stylesheets your app can actually reach.\n *\n * `styles.css` carries every component the SDK ships — measured at 236.71 kB raw\n * against the 38.94 kB an app mounting twelve of them can reach. The per-component\n * sheets under `styles/` have always closed that gap, but by hand: the app lists\n * them and keeps the list honest as it grows. This plugin makes the list the build's\n * problem, resolving it from the imports the source already writes.\n *\n * The closure matters more than the scan. A component pays for the CSS of every SDK\n * component it renders internally, so `<DataTable>` alone needs six sheets and\n * `<AIChat>` seven — a hand-kept list of one sheet per component the app names is\n * wrong in a way that only shows up as an unstyled child.\n *\n * The reset is the other half, and the half that breaks apps. `styles.css` claims\n * `html`, `body` and `#root`; an app with its own layout that imports it loses its\n * document surface, and an app that drops the import loses the box model its\n * components are written against — components render unstyled, which is what the\n * \"just remove the import\" fix actually costs. The default `reset: \"scoped\"` is the\n * third option: the same normalisation, confined to\n * `:where([class*=\"tempest_\"])`, so it reaches inside a Tempest component and\n * nowhere else.\n *\n * `tempest-react-sdk/styles/auto.css` is a real file, so it resolves without the\n * plugin too — to the complete sheet. Dropping the plugin costs bytes, never\n * correctness.\n *\n * @param options - Scan configuration.\n * @returns The Vite plugin.\n *\n * @example\n * // vite.config.ts\n * import { createViteConfig, tempestStyles } from \"tempest-react-sdk/vite\";\n *\n * export default createViteConfig({ plugins: [tempestStyles()] });\n *\n * @example\n * // src/main.tsx — the one import, instead of a maintained list\n * import \"tempest-react-sdk/styles/auto.css\";\n */\nexport function tempestStyles(options: TempestStylesOptions = {}): TempestVitePlugin {\n const { dir = \"src\", include = [], skipDirs, reset = \"scoped\" } = options;\n const skip = new Set<string>(skipDirs ?? SKIP_DIRS);\n let root = process.cwd();\n let names: string[] | null = [...include];\n\n /**\n * Rescan the source tree, keeping the explicitly included names.\n *\n * @tempest-limits empty-catch — the scan races the editor and the file system:\n * a file listed a moment ago can be renamed, deleted, or held by another process\n * by the time it is read. An unreadable file contributes no names and the build\n * continues; failing the whole scan would break `vite dev` over a temp file that\n * no longer exists.\n */\n const rescan = async (): Promise<void> => {\n const found = new Set<string>(include);\n for (const file of await collectSourceFiles(resolve(root, dir), skip)) {\n let code: string;\n try {\n code = await readFile(file, \"utf8\");\n } catch {\n continue;\n }\n const scanned = scanStyleImports(code);\n if (scanned === null) {\n names = null;\n return;\n }\n for (const name of scanned) found.add(name);\n }\n names = [...found];\n };\n\n return {\n name: \"tempest-styles\",\n enforce: \"pre\",\n\n configResolved(config: { root?: string }) {\n root = config.root ?? root;\n },\n\n async buildStart() {\n await rescan();\n },\n\n resolveId(id: string) {\n return id === TEMPEST_STYLES_ID ? RESOLVED_ID : null;\n },\n\n load(id: string) {\n return id === RESOLVED_ID ? buildStyleEntry(names, readManifest(root), reset) : null;\n },\n };\n}\n"],"mappings":"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"}
|
package/dist/vite.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./vite/tempest-icons.cjs"),t=require("./vite/create-vite-config.cjs"),n=require("./vite/tempest-pwa-manifest.cjs"),r=require("./vite/tempest-pwa-icons.cjs"),i=require("./vite/tempest-pwa-dev-sw.cjs");exports.createViteConfig=t.createViteConfig,exports.tempestIcons=e.tempestIcons,exports.tempestPwaDevSw=i.tempestPwaDevSw,exports.tempestPwaIcons=r.tempestPwaIcons,exports.tempestPwaManifest=n.tempestPwaManifest;
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./vite/tempest-icons.cjs"),t=require("./vite/create-vite-config.cjs"),n=require("./vite/tempest-pwa-manifest.cjs"),r=require("./vite/tempest-pwa-icons.cjs"),i=require("./vite/tempest-pwa-dev-sw.cjs"),a=require("./vite/tempest-styles.cjs");exports.createViteConfig=t.createViteConfig,exports.tempestIcons=e.tempestIcons,exports.tempestPwaDevSw=i.tempestPwaDevSw,exports.tempestPwaIcons=r.tempestPwaIcons,exports.tempestPwaManifest=n.tempestPwaManifest,exports.tempestStyles=a.tempestStyles;
|