tempest-react-sdk 0.26.1 → 0.28.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 +25 -1
- package/bin/lib/css/analyze.mjs +149 -0
- package/bin/lib/css/analyze.test.mjs +136 -0
- package/bin/lib/css/collect.mjs +152 -0
- package/bin/lib/css/findings.mjs +79 -0
- package/bin/lib/css/fix.e2e.test.mjs +99 -0
- package/bin/lib/css/fix.mjs +138 -0
- package/bin/lib/css/fix.test.mjs +108 -0
- package/bin/lib/css/index.mjs +16 -0
- package/bin/lib/css/parse.mjs +398 -0
- package/bin/lib/css/parse.test.mjs +146 -0
- package/bin/lib/css/properties.mjs +399 -0
- package/bin/lib/css/repetition.mjs +213 -0
- package/bin/lib/css/repetition.test.mjs +166 -0
- package/bin/lib/css/semantic.mjs +327 -0
- package/bin/lib/css/semantic.test.mjs +201 -0
- package/bin/lib/css/tokens.mjs +119 -0
- package/bin/lib/doctor/doctor.e2e.test.mjs +246 -0
- package/bin/tempest.mjs +272 -36
- package/dist/charts/scales.cjs +2 -0
- package/dist/charts/scales.cjs.map +1 -0
- package/dist/charts/scales.js +29 -0
- package/dist/charts/scales.js.map +1 -0
- package/dist/charts.cjs +1 -1
- package/dist/charts.d.ts +91 -0
- package/dist/charts.js +9 -8
- package/dist/components/BottomNavigation/BottomNavigation.module.cjs.map +1 -1
- package/dist/components/BottomNavigation/BottomNavigation.module.js.map +1 -1
- package/dist/components/Carousel/Carousel.module.cjs.map +1 -1
- package/dist/components/Carousel/Carousel.module.js.map +1 -1
- package/dist/components/CodeBlock/CodeBlock.cjs +3 -0
- package/dist/components/CodeBlock/CodeBlock.cjs.map +1 -0
- package/dist/components/CodeBlock/CodeBlock.js +53 -0
- package/dist/components/CodeBlock/CodeBlock.js.map +1 -0
- package/dist/components/CodeBlock/CodeBlock.module.cjs +2 -0
- package/dist/components/CodeBlock/CodeBlock.module.cjs.map +1 -0
- package/dist/components/CodeBlock/CodeBlock.module.js +29 -0
- package/dist/components/CodeBlock/CodeBlock.module.js.map +1 -0
- package/dist/components/CodeBlock/tokenize.cjs +3 -0
- package/dist/components/CodeBlock/tokenize.cjs.map +1 -0
- package/dist/components/CodeBlock/tokenize.js +267 -0
- package/dist/components/CodeBlock/tokenize.js.map +1 -0
- package/dist/components/ImageCropper/ImageCropper.cjs +2 -0
- package/dist/components/ImageCropper/ImageCropper.cjs.map +1 -0
- package/dist/components/ImageCropper/ImageCropper.js +240 -0
- package/dist/components/ImageCropper/ImageCropper.js.map +1 -0
- package/dist/components/ImageCropper/ImageCropper.module.cjs +2 -0
- package/dist/components/ImageCropper/ImageCropper.module.cjs.map +1 -0
- package/dist/components/ImageCropper/ImageCropper.module.js +15 -0
- package/dist/components/ImageCropper/ImageCropper.module.js.map +1 -0
- package/dist/components/ImageCropper/crop-geometry.cjs +2 -0
- package/dist/components/ImageCropper/crop-geometry.cjs.map +1 -0
- package/dist/components/ImageCropper/crop-geometry.js +52 -0
- package/dist/components/ImageCropper/crop-geometry.js.map +1 -0
- package/dist/components/Navbar/Navbar.module.cjs.map +1 -1
- package/dist/components/Navbar/Navbar.module.js.map +1 -1
- package/dist/components/QRCode/QRCode.cjs +2 -0
- package/dist/components/QRCode/QRCode.cjs.map +1 -0
- package/dist/components/QRCode/QRCode.js +49 -0
- package/dist/components/QRCode/QRCode.js.map +1 -0
- package/dist/components/QRCode/QRCode.module.cjs +2 -0
- package/dist/components/QRCode/QRCode.module.cjs.map +1 -0
- package/dist/components/QRCode/QRCode.module.js +9 -0
- package/dist/components/QRCode/QRCode.module.js.map +1 -0
- package/dist/components/QRCode/qr-encode.cjs +2 -0
- package/dist/components/QRCode/qr-encode.cjs.map +1 -0
- package/dist/components/QRCode/qr-encode.js +295 -0
- package/dist/components/QRCode/qr-encode.js.map +1 -0
- package/dist/components/QRCode/qr-tables.cjs +2 -0
- package/dist/components/QRCode/qr-tables.cjs.map +1 -0
- package/dist/components/QRCode/qr-tables.js +366 -0
- package/dist/components/QRCode/qr-tables.js.map +1 -0
- package/dist/components/Scheduler/Scheduler.cjs +2 -0
- package/dist/components/Scheduler/Scheduler.cjs.map +1 -0
- package/dist/components/Scheduler/Scheduler.js +134 -0
- package/dist/components/Scheduler/Scheduler.js.map +1 -0
- package/dist/components/Scheduler/Scheduler.module.cjs +2 -0
- package/dist/components/Scheduler/Scheduler.module.cjs.map +1 -0
- package/dist/components/Scheduler/Scheduler.module.js +26 -0
- package/dist/components/Scheduler/Scheduler.module.js.map +1 -0
- package/dist/components/Scheduler/scheduler-layout.cjs +2 -0
- package/dist/components/Scheduler/scheduler-layout.cjs.map +1 -0
- package/dist/components/Scheduler/scheduler-layout.js +97 -0
- package/dist/components/Scheduler/scheduler-layout.js.map +1 -0
- package/dist/components/ScrollArea/ScrollArea.cjs +1 -1
- package/dist/components/ScrollArea/ScrollArea.cjs.map +1 -1
- package/dist/components/ScrollArea/ScrollArea.js +22 -16
- package/dist/components/ScrollArea/ScrollArea.js.map +1 -1
- package/dist/components/ScrollArea/ScrollArea.module.cjs.map +1 -1
- package/dist/components/ScrollArea/ScrollArea.module.js.map +1 -1
- package/dist/components/Sparkline/Sparkline.cjs +2 -0
- package/dist/components/Sparkline/Sparkline.cjs.map +1 -0
- package/dist/components/Sparkline/Sparkline.js +64 -0
- package/dist/components/Sparkline/Sparkline.js.map +1 -0
- package/dist/components/Sparkline/Sparkline.module.cjs +2 -0
- package/dist/components/Sparkline/Sparkline.module.cjs.map +1 -0
- package/dist/components/Sparkline/Sparkline.module.js +10 -0
- package/dist/components/Sparkline/Sparkline.module.js.map +1 -0
- package/dist/components/Sparkline/sparkline-geometry.cjs +2 -0
- package/dist/components/Sparkline/sparkline-geometry.cjs.map +1 -0
- package/dist/components/Sparkline/sparkline-geometry.js +56 -0
- package/dist/components/Sparkline/sparkline-geometry.js.map +1 -0
- package/dist/components/Table/Table.cjs +1 -1
- package/dist/components/Table/Table.cjs.map +1 -1
- package/dist/components/Table/Table.js +37 -30
- package/dist/components/Table/Table.js.map +1 -1
- package/dist/components/Table/Table.module.cjs.map +1 -1
- package/dist/components/Table/Table.module.js.map +1 -1
- package/dist/components/VirtualList/VirtualList.cjs +1 -1
- package/dist/components/VirtualList/VirtualList.cjs.map +1 -1
- package/dist/components/VirtualList/VirtualList.js +13 -11
- package/dist/components/VirtualList/VirtualList.js.map +1 -1
- package/dist/components/VirtualList/VirtualList.module.cjs.map +1 -1
- package/dist/components/VirtualList/VirtualList.module.js.map +1 -1
- package/dist/hooks/use-scroll-overflow.cjs +2 -0
- package/dist/hooks/use-scroll-overflow.cjs.map +1 -0
- package/dist/hooks/use-scroll-overflow.js +22 -0
- package/dist/hooks/use-scroll-overflow.js.map +1 -0
- package/dist/styles.css +1 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +607 -2
- package/dist/tempest-react-sdk.js +197 -188
- package/dist/theme/create-theme.cjs +3 -3
- package/dist/theme/create-theme.cjs.map +1 -1
- package/dist/theme/create-theme.js +52 -28
- package/dist/theme/create-theme.js.map +1 -1
- package/dist/theme/data-viz-ramps.cjs +2 -0
- package/dist/theme/data-viz-ramps.cjs.map +1 -0
- package/dist/theme/data-viz-ramps.js +58 -0
- package/dist/theme/data-viz-ramps.js.map +1 -0
- package/package.json +2 -1
- package/template/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-theme.cjs","names":[],"sources":["../../src/theme/create-theme.ts"],"sourcesContent":["/**\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\";\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 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":"+BA+DA,IAAM,EAA8D,CAChE,KAAM,CAAE,GAAI,IAAK,GAAI,IAAK,GAAI,IAAK,GAAI,IAAK,GAAI,IAAK,MAAO,GAAI,EAChE,GAAI,CAAE,GAAI,MAAO,GAAI,MAAO,GAAI,MAAO,GAAI,MAAO,GAAI,MAAO,MAAO,MAAO,EAC3E,GAAI,CAAE,GAAI,MAAO,GAAI,MAAO,GAAI,MAAO,GAAI,OAAQ,GAAI,OAAQ,MAAO,MAAO,EAC7E,GAAI,CAAE,GAAI,MAAO,GAAI,MAAO,GAAI,OAAQ,GAAI,OAAQ,GAAI,OAAQ,MAAO,MAAO,EAC9E,GAAI,CAAE,GAAI,MAAO,GAAI,OAAQ,GAAI,OAAQ,GAAI,OAAQ,GAAI,OAAQ,MAAO,MAAO,EAC/E,KAAM,CAAE,GAAI,MAAO,GAAI,MAAO,GAAI,SAAU,GAAI,SAAU,GAAI,SAAU,MAAO,QAAS,CAC5F,EAEM,EAA2B,CAAC,GAAI,IAAK,IAAK,IAAK,IAAK,IAAK,IAAK,IAAK,IAAK,GAAG,EAEjF,SAAS,EAAW,EAAgC,EAAc,EAAyB,CACvF,IAAK,IAAM,KAAQ,EACf,EAAO,aAAa,EAAK,GAAG,KAAU,EAAM,EAEpD,CAGA,IAAM,EAAmB,IAiBzB,SAAS,EACL,EACA,EACA,EACS,CACT,IAAK,IAAM,KAAQ,EACf,GAAI,EAAA,cAAc,EAAM,GAAW,EAAM,EAAK,GAAK,EAAkB,OAAO,EAEhF,OAAO,EAAW,EAAW,OAAS,EAC1C,CASA,SAAS,EACL,EACA,EACA,EACA,EACI,CACJ,EAAO,qBAAuB,6BAC1B,IAAW,SACX,EAAO,2BAA6B,6BACpC,EAAO,4BAA8B,6BACrC,EAAO,0BAA4B,4BACnC,EAAO,gCAAkC,6BACzC,EAAO,6BACH,yBAAyB,EAAe,EAAO,GAAI,CAAC,IAAK,IAAK,IAAK,GAAG,CAAC,EAAE,KAE7E,EAAO,2BAA6B,6BACpC,EAAO,4BAA8B,6BACrC,EAAO,0BAA4B,6BACnC,EAAO,gCAAkC,6BACzC,EAAO,6BACH,yBAAyB,EAAe,EAAO,IAAK,CAAC,IAAK,IAAK,GAAG,CAAC,EAAE,IAG7E,IAAM,EAAa,EAAA,mBAAmB,EAAM,IAAI,EAChD,EAAO,gCAAkC,EACzC,EAAO,6BAA+B,EACtC,EAAO,8BAAgC,EAAA,gBAAgB,EAAM,KAAM,CAAc,CACrF,CASA,SAAS,EACL,EACA,EACA,EACI,CACA,IAAW,SACX,EAAO,gBAAkB,UACzB,EAAO,qBAAuB,yBAC9B,EAAO,uBAAyB,0BAChC,EAAO,uBAAyB,0BAChC,EAAO,oBAAsB,0BAC7B,EAAO,2BAA6B,0BACpC,EAAO,kBAAoB,0BAC3B,EAAO,wBAA0B,0BACjC,EAAO,yBAA2B,4BAElC,EAAO,gBAAkB,EAAM,IAC/B,EAAO,qBAAuB,0BAC9B,EAAO,uBAAyB,0BAChC,EAAO,uBAAyB,0BAChC,EAAO,oBAAsB,0BAC7B,EAAO,2BAA6B,0BACpC,EAAO,kBAAoB,0BAC3B,EAAO,wBAA0B,0BACjC,EAAO,yBAA2B,0BAE1C,CAQA,SAAS,EACL,EACA,EACA,EACA,EACI,CACA,IAAW,SACX,EAAO,aAAa,KAAU,EAAM,KACpC,EAAO,aAAa,EAAK,MAAQ,EAAM,KACvC,EAAO,aAAa,EAAK,MAAQ,EAAM,IACvC,EAAO,aAAa,EAAK,UAAY,EAAM,KAC3C,EAAO,aAAa,EAAK,SAAW,EAAM,OAE1C,EAAO,aAAa,KAAU,EAAM,KACpC,EAAO,aAAa,EAAK,MAAQ,EAAM,KACvC,EAAO,aAAa,EAAK,MAAQ,EAAM,IACvC,EAAO,aAAa,EAAK,UAAY,EAAM,KAC3C,EAAO,aAAa,EAAK,SAAW,EAAM,KAElD,CAEA,SAAS,EAAY,EAAkB,EAAwC,CAC3E,IAAM,EAAU,OAAO,QAAQ,CAAM,EAGrC,OAFI,EAAQ,SAAW,EAAU,GAE1B,GAAG,EAAS,MADN,EAAQ,KAAK,CAAC,EAAM,KAAW,OAAO,EAAK,IAAI,EAAM,EAAE,CAAC,CAAC,KAAK;CAClD,EAAK,IAClC,CAwBA,SAAgB,EAAY,EAA8B,CAAC,EAAmB,CAC1E,GAAM,CACF,UACA,OACA,QACA,SACA,iBAAiB,IACjB,WAAW,QACX,eAAe,+BACf,EAEE,EAAgC,CAAC,EACjC,EAA+B,CAAC,EAEtC,GAAI,EAAS,CACT,IAAM,EAAa,EAAA,iBAAiB,EAAS,OAAO,EAC9C,EAAY,EAAA,iBAAiB,EAAS,MAAM,EAClD,EAAW,EAAO,UAAW,CAAU,EACvC,EAAW,EAAM,UAAW,CAAS,EACrC,EAAoB,EAAO,EAAY,QAAS,CAAc,EAC9D,EAAoB,EAAM,EAAW,OAAQ,CAAc,CAC/D,CAEA,GAAI,EAAM,CAKN,IAAM,EAAa,EAAA,iBAAiB,EAAM,QAAS,CAAE,OAAQ,GAAO,QAAS,EAAK,CAAC,EAC7E,EAAY,EAAA,iBAAiB,EAAM,OAAQ,CAAE,OAAQ,GAAO,QAAS,EAAK,CAAC,EACjF,EAAW,EAAO,OAAQ,CAAU,EACpC,EAAW,EAAM,OAAQ,CAAS,EAClC,EAAoB,EAAO,EAAY,OAAO,EAC9C,EAAoB,EAAM,EAAW,MAAM,CAC/C,CAEA,IAAK,IAAM,IAAU,CAAC,UAAW,UAAW,SAAU,MAAM,EAAY,CACpE,IAAM,EAAQ,EAAQ,GACjB,IACL,EAAY,EAAO,EAAQ,EAAA,iBAAiB,EAAO,OAAO,EAAG,OAAO,EACpE,EAAY,EAAM,EAAQ,EAAA,iBAAiB,EAAO,MAAM,EAAG,MAAM,EACrE,CAaA,GAXI,GAAO,SACP,EAAM,SAAS,EAAO,IAAU,CAC5B,EAAM,mBAAmB,EAAQ,KAAO,CAC5C,CAAC,EAKD,EAAM,yBAA2B,OAAO,EAAM,MAAM,GAGpD,EAAQ,CACR,IAAM,EAAQ,OAAO,GAAW,SAAW,EAAe,GAAU,EACpE,IAAK,GAAM,CAAC,EAAM,KAAU,OAAO,QAAQ,CAAK,EAC5C,EAAM,oBAAoB,KAAU,CAE5C,CAMA,MAAO,CAAE,QAAO,OAAM,IAJV,CAAC,EAAY,EAAU,CAAK,EAAG,EAAY,EAAc,CAAI,CAAC,CAAC,CACtE,OAAO,OAAO,CAAC,CACf,KAAK;;CAEY,CAAI,CAC9B,CAWA,SAAgB,EAAc,EAA4C,CACtE,GAAI,CAAC,EAAQ,QAAS,OAAO,KAC7B,IAAM,EAAQ,EAAA,iBAAiB,EAAQ,QAAS,OAAO,EACvD,OAAO,EAAA,cAAc,EAAM,KAAM,EAAA,mBAAmB,EAAM,IAAI,CAAC,CACnE"}
|
|
1
|
+
{"version":3,"file":"create-theme.cjs","names":[],"sources":["../../src/theme/create-theme.ts"],"sourcesContent":["/**\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,6 +1,7 @@
|
|
|
1
1
|
import { contrastRatio as e, createColorScale as t, hexToRgbaString as n, readableForeground as r } from "./color.js";
|
|
2
|
+
import { buildDivergingRamp as i, buildRamp as a, hueOf as o } from "./data-viz-ramps.js";
|
|
2
3
|
//#region src/theme/create-theme.ts
|
|
3
|
-
var
|
|
4
|
+
var s = {
|
|
4
5
|
none: {
|
|
5
6
|
xs: "0",
|
|
6
7
|
sm: "0",
|
|
@@ -49,7 +50,7 @@ var i = {
|
|
|
49
50
|
xl: "9999px",
|
|
50
51
|
"2xl": "9999px"
|
|
51
52
|
}
|
|
52
|
-
},
|
|
53
|
+
}, c = [
|
|
53
54
|
50,
|
|
54
55
|
100,
|
|
55
56
|
200,
|
|
@@ -61,21 +62,21 @@ var i = {
|
|
|
61
62
|
800,
|
|
62
63
|
900
|
|
63
64
|
];
|
|
64
|
-
function
|
|
65
|
-
for (let r of
|
|
65
|
+
function l(e, t, n) {
|
|
66
|
+
for (let r of c) e[`--tempest-${t}-${r}`] = n[r];
|
|
66
67
|
}
|
|
67
|
-
var
|
|
68
|
-
function
|
|
69
|
-
for (let i of r) if (e(t[n], t[i]) >=
|
|
68
|
+
var u = 4.5;
|
|
69
|
+
function d(t, n, r) {
|
|
70
|
+
for (let i of r) if (e(t[n], t[i]) >= u) return i;
|
|
70
71
|
return r[r.length - 1];
|
|
71
72
|
}
|
|
72
|
-
function
|
|
73
|
-
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-${
|
|
73
|
+
function f(e, t, i, a) {
|
|
74
|
+
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, [
|
|
74
75
|
600,
|
|
75
76
|
700,
|
|
76
77
|
800,
|
|
77
78
|
900
|
|
78
|
-
])})`) : (e["--tempest-primary-hover"] = "var(--tempest-primary-400)", e["--tempest-primary-active"] = "var(--tempest-primary-300)", e["--tempest-primary-soft"] = "var(--tempest-primary-100)", e["--tempest-primary-soft-hover"] = "var(--tempest-primary-200)", e["--tempest-primary-on-soft"] = `var(--tempest-primary-${
|
|
79
|
+
])})`) : (e["--tempest-primary-hover"] = "var(--tempest-primary-400)", e["--tempest-primary-active"] = "var(--tempest-primary-300)", e["--tempest-primary-soft"] = "var(--tempest-primary-100)", e["--tempest-primary-soft-hover"] = "var(--tempest-primary-200)", e["--tempest-primary-on-soft"] = `var(--tempest-primary-${d(t, 100, [
|
|
79
80
|
700,
|
|
80
81
|
800,
|
|
81
82
|
900
|
|
@@ -83,21 +84,21 @@ function l(e, t, i, a) {
|
|
|
83
84
|
let o = r(t[500]);
|
|
84
85
|
e["--tempest-primary-foreground"] = o, e["--tempest-text-on-primary"] = o, e["--tempest-focus-ring-color"] = n(t[500], a);
|
|
85
86
|
}
|
|
86
|
-
function
|
|
87
|
+
function p(e, t, n) {
|
|
87
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
89
|
}
|
|
89
|
-
function
|
|
90
|
+
function m(e, t, n, r) {
|
|
90
91
|
r === "light" ? (e[`--tempest-${t}`] = n[700], e[`--tempest-${t}-fg`] = n[800], e[`--tempest-${t}-bg`] = n[50], e[`--tempest-${t}-border`] = n[200], e[`--tempest-${t}-solid`] = n[600]) : (e[`--tempest-${t}`] = 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]);
|
|
91
92
|
}
|
|
92
|
-
function
|
|
93
|
+
function h(e, t) {
|
|
93
94
|
let n = Object.entries(t);
|
|
94
95
|
return n.length === 0 ? "" : `${e} {\n${n.map(([e, t]) => ` ${e}: ${t};`).join("\n")}\n}`;
|
|
95
96
|
}
|
|
96
|
-
function
|
|
97
|
-
let { primary: n, gray: r, chart:
|
|
97
|
+
function g(e = {}) {
|
|
98
|
+
let { primary: n, gray: r, chart: c, radius: u, focusRingAlpha: d = .35, selector: g = ":root", darkSelector: _ = "[data-tempest-theme=\"dark\"]" } = e, v = {}, y = {};
|
|
98
99
|
if (n) {
|
|
99
100
|
let e = t(n, "light"), r = t(n, "dark");
|
|
100
|
-
|
|
101
|
+
l(v, "primary", e), l(y, "primary", r), f(v, e, "light", d), f(y, r, "dark", d);
|
|
101
102
|
}
|
|
102
103
|
if (r) {
|
|
103
104
|
let e = t(r, "light", {
|
|
@@ -107,7 +108,7 @@ function p(e = {}) {
|
|
|
107
108
|
anchor: !1,
|
|
108
109
|
neutral: !0
|
|
109
110
|
});
|
|
110
|
-
|
|
111
|
+
l(v, "gray", e), l(y, "gray", n), p(v, e, "light"), p(y, n, "dark");
|
|
111
112
|
}
|
|
112
113
|
for (let n of [
|
|
113
114
|
"success",
|
|
@@ -116,26 +117,49 @@ function p(e = {}) {
|
|
|
116
117
|
"info"
|
|
117
118
|
]) {
|
|
118
119
|
let r = e[n];
|
|
119
|
-
r && (
|
|
120
|
+
r && (m(v, n, t(r, "light"), "light"), m(y, n, t(r, "dark"), "dark"));
|
|
120
121
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}),
|
|
124
|
-
|
|
125
|
-
|
|
122
|
+
c?.length && (c.forEach((e, t) => {
|
|
123
|
+
v[`--tempest-chart-${t + 1}`] = e;
|
|
124
|
+
}), v["--tempest-chart-count"] = String(c.length));
|
|
125
|
+
let b = c?.[0] ?? n;
|
|
126
|
+
if (b) {
|
|
127
|
+
let t = o(b), n = e.danger ? o(e.danger) : (t + 180) % 360;
|
|
128
|
+
for (let [e, r] of [[v, "light"], [y, "dark"]]) {
|
|
129
|
+
a(t, r).forEach((t, n) => {
|
|
130
|
+
e[`--tempest-chart-sequential-${n + 1}`] = t;
|
|
131
|
+
});
|
|
132
|
+
let o = i({
|
|
133
|
+
coolHue: t,
|
|
134
|
+
warmHue: n,
|
|
135
|
+
mid: r === "light" ? "#e4e7ec" : "#262d3f",
|
|
136
|
+
mode: r
|
|
137
|
+
});
|
|
138
|
+
[
|
|
139
|
+
...o.cool,
|
|
140
|
+
o.mid,
|
|
141
|
+
...o.warm
|
|
142
|
+
].forEach((t, n) => {
|
|
143
|
+
e[`--tempest-chart-diverging-${n + 1}`] = t;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (u) {
|
|
148
|
+
let e = typeof u == "string" ? s[u] : u;
|
|
149
|
+
for (let [t, n] of Object.entries(e)) v[`--tempest-radius-${t}`] = n;
|
|
126
150
|
}
|
|
127
151
|
return {
|
|
128
|
-
light:
|
|
129
|
-
dark:
|
|
130
|
-
css: [
|
|
152
|
+
light: v,
|
|
153
|
+
dark: y,
|
|
154
|
+
css: [h(g, v), h(_, y)].filter(Boolean).join("\n\n")
|
|
131
155
|
};
|
|
132
156
|
}
|
|
133
|
-
function
|
|
157
|
+
function _(n) {
|
|
134
158
|
if (!n.primary) return null;
|
|
135
159
|
let i = t(n.primary, "light");
|
|
136
160
|
return e(i[500], r(i[500]));
|
|
137
161
|
}
|
|
138
162
|
//#endregion
|
|
139
|
-
export {
|
|
163
|
+
export { g as createTheme, _ as themeContrast };
|
|
140
164
|
|
|
141
165
|
//# sourceMappingURL=create-theme.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-theme.js","names":[],"sources":["../../src/theme/create-theme.ts"],"sourcesContent":["/**\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\";\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 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 * 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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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./color.cjs");var t={light:{from:.93,to:.34},dark:{from:.28,to:.86}};function n(e){return .35+.65*(1-Math.abs(2*e-1)**1.6)}function r(r,i,a=7,o=.19){let s=t[i],c=[];for(let t=0;t<a;t+=1){let i=a===1?0:t/(a-1);c.push(e.oklchToHex({l:s.from+(s.to-s.from)*i,c:o*n(i),h:r}))}return c}function i({coolHue:t,warmHue:r,mid:i,mode:a,steps:o=4}){let s=a===`light`?{from:.86,to:.4}:{from:.82,to:.42},c=t=>{let r=[];for(let i=0;i<o;i+=1){let a=o===1?0:i/(o-1);r.push(e.oklchToHex({l:s.from+(s.to-s.from)*a,c:.17*n(1-a*.5),h:t}))}return r.reverse()};return{cool:c(t),mid:i,warm:[...c(r)].reverse()}}function a(t){return e.hexToOklch(t).h}exports.buildDivergingRamp=i,exports.buildRamp=r,exports.hueOf=a;
|
|
2
|
+
//# sourceMappingURL=data-viz-ramps.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data-viz-ramps.cjs","names":[],"sources":["../../src/theme/data-viz-ramps.ts"],"sourcesContent":["import { contrastRatio, hexToOklch, oklchToHex } from \"./color\";\n\n/** Number of steps in a sequential ramp. */\nexport const SEQUENTIAL_STEPS = 7;\n\n/** Steps per arm of a diverging scale, excluding the neutral midpoint. */\nexport const DIVERGING_ARM_STEPS = 4;\n\n/** A diverging scale: cool arm (extreme → near-mid), the midpoint, then the warm arm. */\nexport interface DivergingRamp {\n cool: string[];\n mid: string;\n warm: string[];\n}\n\n/**\n * Lightness range a ramp spans, per mode.\n *\n * Dark mode is **selected, not flipped**: its own band, chosen for the dark surface.\n * Mechanically inverting the light ramp yields steps that are either invisible\n * against a near-black surface or so bright they read as highlights.\n */\nconst BAND = {\n light: { from: 0.93, to: 0.34 },\n dark: { from: 0.28, to: 0.86 },\n} as const;\n\n/**\n * Chroma shaped as a dome across the ramp.\n *\n * A constant chroma makes the pale end look muddy and the dark end look neon. The\n * dome keeps the extremes believable while the middle carries the hue, which is\n * what makes a heatmap readable at a glance.\n *\n * @param t - Position along the ramp, `0`–`1`.\n * @returns A multiplier for the peak chroma.\n */\nfunction chromaDome(t: number): number {\n return 0.35 + 0.65 * (1 - Math.abs(2 * t - 1) ** 1.6);\n}\n\n/**\n * Build a single-hue ramp with evenly spaced perceptual lightness.\n *\n * Even spacing in OKLCH lightness — not in RGB — is what makes equal data steps look\n * like equal colour steps.\n *\n * Steps come back in **token order**: index 0 is the end that means \"near zero\", so\n * it is the lightest on a light canvas and the darkest on a dark one. That is the\n * order `--tempest-chart-sequential-1…7` is written in, and it makes index 0 the step\n * nearest the surface in both modes — which is what `ordinalStart` walks from.\n * Returning a fixed light→dark order instead would put token 1 at opposite ends of\n * the scale depending on the mode, and a dark theme would paint every heatmap\n * inverted.\n *\n * @param hue - OKLCH hue in degrees.\n * @param mode - Which lightness band to span.\n * @param steps - How many steps to emit.\n * @param peakChroma - Chroma at the middle of the ramp.\n * @returns Hex steps, near-zero end first.\n */\nexport function buildRamp(\n hue: number,\n mode: \"light\" | \"dark\",\n steps = SEQUENTIAL_STEPS,\n peakChroma = 0.19,\n): string[] {\n const band = BAND[mode];\n const out: string[] = [];\n for (let i = 0; i < steps; i += 1) {\n const t = steps === 1 ? 0 : i / (steps - 1);\n out.push(\n oklchToHex({\n l: band.from + (band.to - band.from) * t,\n c: peakChroma * chromaDome(t),\n h: hue,\n }),\n );\n }\n return out;\n}\n\n/**\n * The first index of `ramp` that clears `minContrast` against `surface`.\n *\n * A **sequential** ramp may let its near-zero end recede into the surface — that is\n * what \"almost nothing\" should look like on a heatmap. An **ordinal** ramp may not:\n * every step is a discrete mark a reader has to see. This is how a consumer finds\n * where the ordinal-safe slice of a sequential ramp begins.\n *\n * @param ramp - Hex steps in token order, near-zero end first.\n * @param surface - The chart surface the ramp sits on.\n * @param minContrast - Floor to clear. Default `2` — the ordinal floor.\n * @returns The first safe index, or `0` when every step already clears it.\n */\nexport function ordinalStart(ramp: string[], surface: string, minContrast = 2): number {\n const index = ramp.findIndex((step) => contrastRatio(step, surface) >= minContrast);\n return index < 0 ? ramp.length - 1 : index;\n}\n\n/**\n * Build a diverging scale from two opposing hues around a neutral midpoint.\n *\n * The midpoint is **grey, never a hue**: a coloured midpoint reads as a third\n * category instead of as \"no deviation\", which is the one thing a diverging scale\n * exists to show. The arms get equal step counts so neither side looks like it\n * carries more range than the other.\n *\n * @param params.coolHue - OKLCH hue for the negative arm.\n * @param params.warmHue - OKLCH hue for the positive arm.\n * @param params.mid - Neutral midpoint, in hex.\n * @param params.mode - Which lightness band to span.\n * @param params.steps - Steps per arm.\n * @returns The scale, each arm ordered extreme → nearest the midpoint.\n */\nexport function buildDivergingRamp({\n coolHue,\n warmHue,\n mid,\n mode,\n steps = DIVERGING_ARM_STEPS,\n}: {\n coolHue: number;\n warmHue: number;\n mid: string;\n mode: \"light\" | \"dark\";\n steps?: number;\n}): DivergingRamp {\n /*\n * Each arm spans from its extreme to just short of the midpoint, so the two\n * arms together read as one continuous scale rather than two ramps abutting.\n */\n const band = mode === \"light\" ? { from: 0.86, to: 0.4 } : { from: 0.82, to: 0.42 };\n const arm = (hue: number): string[] => {\n const out: string[] = [];\n for (let i = 0; i < steps; i += 1) {\n const t = steps === 1 ? 0 : i / (steps - 1);\n out.push(\n oklchToHex({\n l: band.from + (band.to - band.from) * t,\n c: 0.17 * chromaDome(1 - t * 0.5),\n h: hue,\n }),\n );\n }\n return out.reverse();\n };\n return { cool: arm(coolHue), mid, warm: [...arm(warmHue)].reverse() };\n}\n\n/**\n * The hue of a hex colour, for feeding the ramp builders.\n *\n * @param hex - Any hex colour.\n * @returns Its OKLCH hue in degrees.\n */\nexport function hueOf(hex: string): number {\n return hexToOklch(hex).h;\n}\n"],"mappings":"+BAsBA,IAAM,EAAO,CACT,MAAO,CAAE,KAAM,IAAM,GAAI,GAAK,EAC9B,KAAM,CAAE,KAAM,IAAM,GAAI,GAAK,CACjC,EAYA,SAAS,EAAW,EAAmB,CACnC,MAAO,KAAO,KAAQ,EAAI,KAAK,IAAI,EAAI,EAAI,CAAC,GAAK,IACrD,CAsBA,SAAgB,EACZ,EACA,EACA,EAAA,EACA,EAAa,IACL,CACR,IAAM,EAAO,EAAK,GACZ,EAAgB,CAAC,EACvB,IAAK,IAAI,EAAI,EAAG,EAAI,EAAO,GAAK,EAAG,CAC/B,IAAM,EAAI,IAAU,EAAI,EAAI,GAAK,EAAQ,GACzC,EAAI,KACA,EAAA,WAAW,CACP,EAAG,EAAK,MAAQ,EAAK,GAAK,EAAK,MAAQ,EACvC,EAAG,EAAa,EAAW,CAAC,EAC5B,EAAG,CACP,CAAC,CACL,CACJ,CACA,OAAO,CACX,CAmCA,SAAgB,EAAmB,CAC/B,UACA,UACA,MACA,OACA,QAAA,GAOc,CAKd,IAAM,EAAO,IAAS,QAAU,CAAE,KAAM,IAAM,GAAI,EAAI,EAAI,CAAE,KAAM,IAAM,GAAI,GAAK,EAC3E,EAAO,GAA0B,CACnC,IAAM,EAAgB,CAAC,EACvB,IAAK,IAAI,EAAI,EAAG,EAAI,EAAO,GAAK,EAAG,CAC/B,IAAM,EAAI,IAAU,EAAI,EAAI,GAAK,EAAQ,GACzC,EAAI,KACA,EAAA,WAAW,CACP,EAAG,EAAK,MAAQ,EAAK,GAAK,EAAK,MAAQ,EACvC,EAAG,IAAO,EAAW,EAAI,EAAI,EAAG,EAChC,EAAG,CACP,CAAC,CACL,CACJ,CACA,OAAO,EAAI,QAAQ,CACvB,EACA,MAAO,CAAE,KAAM,EAAI,CAAO,EAAG,MAAK,KAAM,CAAC,GAAG,EAAI,CAAO,CAAC,CAAC,CAAC,QAAQ,CAAE,CACxE,CAQA,SAAgB,EAAM,EAAqB,CACvC,OAAO,EAAA,WAAW,CAAG,CAAC,CAAC,CAC3B"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { hexToOklch as e, oklchToHex as t } from "./color.js";
|
|
2
|
+
var n = {
|
|
3
|
+
light: {
|
|
4
|
+
from: .93,
|
|
5
|
+
to: .34
|
|
6
|
+
},
|
|
7
|
+
dark: {
|
|
8
|
+
from: .28,
|
|
9
|
+
to: .86
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
function r(e) {
|
|
13
|
+
return .35 + .65 * (1 - Math.abs(2 * e - 1) ** 1.6);
|
|
14
|
+
}
|
|
15
|
+
function i(e, i, a = 7, o = .19) {
|
|
16
|
+
let s = n[i], c = [];
|
|
17
|
+
for (let n = 0; n < a; n += 1) {
|
|
18
|
+
let i = a === 1 ? 0 : n / (a - 1);
|
|
19
|
+
c.push(t({
|
|
20
|
+
l: s.from + (s.to - s.from) * i,
|
|
21
|
+
c: o * r(i),
|
|
22
|
+
h: e
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
return c;
|
|
26
|
+
}
|
|
27
|
+
function a({ coolHue: e, warmHue: n, mid: i, mode: a, steps: o = 4 }) {
|
|
28
|
+
let s = a === "light" ? {
|
|
29
|
+
from: .86,
|
|
30
|
+
to: .4
|
|
31
|
+
} : {
|
|
32
|
+
from: .82,
|
|
33
|
+
to: .42
|
|
34
|
+
}, c = (e) => {
|
|
35
|
+
let n = [];
|
|
36
|
+
for (let i = 0; i < o; i += 1) {
|
|
37
|
+
let a = o === 1 ? 0 : i / (o - 1);
|
|
38
|
+
n.push(t({
|
|
39
|
+
l: s.from + (s.to - s.from) * a,
|
|
40
|
+
c: .17 * r(1 - a * .5),
|
|
41
|
+
h: e
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
return n.reverse();
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
cool: c(e),
|
|
48
|
+
mid: i,
|
|
49
|
+
warm: [...c(n)].reverse()
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function o(t) {
|
|
53
|
+
return e(t).h;
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
export { a as buildDivergingRamp, i as buildRamp, o as hueOf };
|
|
57
|
+
|
|
58
|
+
//# sourceMappingURL=data-viz-ramps.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data-viz-ramps.js","names":[],"sources":["../../src/theme/data-viz-ramps.ts"],"sourcesContent":["import { contrastRatio, hexToOklch, oklchToHex } from \"./color\";\n\n/** Number of steps in a sequential ramp. */\nexport const SEQUENTIAL_STEPS = 7;\n\n/** Steps per arm of a diverging scale, excluding the neutral midpoint. */\nexport const DIVERGING_ARM_STEPS = 4;\n\n/** A diverging scale: cool arm (extreme → near-mid), the midpoint, then the warm arm. */\nexport interface DivergingRamp {\n cool: string[];\n mid: string;\n warm: string[];\n}\n\n/**\n * Lightness range a ramp spans, per mode.\n *\n * Dark mode is **selected, not flipped**: its own band, chosen for the dark surface.\n * Mechanically inverting the light ramp yields steps that are either invisible\n * against a near-black surface or so bright they read as highlights.\n */\nconst BAND = {\n light: { from: 0.93, to: 0.34 },\n dark: { from: 0.28, to: 0.86 },\n} as const;\n\n/**\n * Chroma shaped as a dome across the ramp.\n *\n * A constant chroma makes the pale end look muddy and the dark end look neon. The\n * dome keeps the extremes believable while the middle carries the hue, which is\n * what makes a heatmap readable at a glance.\n *\n * @param t - Position along the ramp, `0`–`1`.\n * @returns A multiplier for the peak chroma.\n */\nfunction chromaDome(t: number): number {\n return 0.35 + 0.65 * (1 - Math.abs(2 * t - 1) ** 1.6);\n}\n\n/**\n * Build a single-hue ramp with evenly spaced perceptual lightness.\n *\n * Even spacing in OKLCH lightness — not in RGB — is what makes equal data steps look\n * like equal colour steps.\n *\n * Steps come back in **token order**: index 0 is the end that means \"near zero\", so\n * it is the lightest on a light canvas and the darkest on a dark one. That is the\n * order `--tempest-chart-sequential-1…7` is written in, and it makes index 0 the step\n * nearest the surface in both modes — which is what `ordinalStart` walks from.\n * Returning a fixed light→dark order instead would put token 1 at opposite ends of\n * the scale depending on the mode, and a dark theme would paint every heatmap\n * inverted.\n *\n * @param hue - OKLCH hue in degrees.\n * @param mode - Which lightness band to span.\n * @param steps - How many steps to emit.\n * @param peakChroma - Chroma at the middle of the ramp.\n * @returns Hex steps, near-zero end first.\n */\nexport function buildRamp(\n hue: number,\n mode: \"light\" | \"dark\",\n steps = SEQUENTIAL_STEPS,\n peakChroma = 0.19,\n): string[] {\n const band = BAND[mode];\n const out: string[] = [];\n for (let i = 0; i < steps; i += 1) {\n const t = steps === 1 ? 0 : i / (steps - 1);\n out.push(\n oklchToHex({\n l: band.from + (band.to - band.from) * t,\n c: peakChroma * chromaDome(t),\n h: hue,\n }),\n );\n }\n return out;\n}\n\n/**\n * The first index of `ramp` that clears `minContrast` against `surface`.\n *\n * A **sequential** ramp may let its near-zero end recede into the surface — that is\n * what \"almost nothing\" should look like on a heatmap. An **ordinal** ramp may not:\n * every step is a discrete mark a reader has to see. This is how a consumer finds\n * where the ordinal-safe slice of a sequential ramp begins.\n *\n * @param ramp - Hex steps in token order, near-zero end first.\n * @param surface - The chart surface the ramp sits on.\n * @param minContrast - Floor to clear. Default `2` — the ordinal floor.\n * @returns The first safe index, or `0` when every step already clears it.\n */\nexport function ordinalStart(ramp: string[], surface: string, minContrast = 2): number {\n const index = ramp.findIndex((step) => contrastRatio(step, surface) >= minContrast);\n return index < 0 ? ramp.length - 1 : index;\n}\n\n/**\n * Build a diverging scale from two opposing hues around a neutral midpoint.\n *\n * The midpoint is **grey, never a hue**: a coloured midpoint reads as a third\n * category instead of as \"no deviation\", which is the one thing a diverging scale\n * exists to show. The arms get equal step counts so neither side looks like it\n * carries more range than the other.\n *\n * @param params.coolHue - OKLCH hue for the negative arm.\n * @param params.warmHue - OKLCH hue for the positive arm.\n * @param params.mid - Neutral midpoint, in hex.\n * @param params.mode - Which lightness band to span.\n * @param params.steps - Steps per arm.\n * @returns The scale, each arm ordered extreme → nearest the midpoint.\n */\nexport function buildDivergingRamp({\n coolHue,\n warmHue,\n mid,\n mode,\n steps = DIVERGING_ARM_STEPS,\n}: {\n coolHue: number;\n warmHue: number;\n mid: string;\n mode: \"light\" | \"dark\";\n steps?: number;\n}): DivergingRamp {\n /*\n * Each arm spans from its extreme to just short of the midpoint, so the two\n * arms together read as one continuous scale rather than two ramps abutting.\n */\n const band = mode === \"light\" ? { from: 0.86, to: 0.4 } : { from: 0.82, to: 0.42 };\n const arm = (hue: number): string[] => {\n const out: string[] = [];\n for (let i = 0; i < steps; i += 1) {\n const t = steps === 1 ? 0 : i / (steps - 1);\n out.push(\n oklchToHex({\n l: band.from + (band.to - band.from) * t,\n c: 0.17 * chromaDome(1 - t * 0.5),\n h: hue,\n }),\n );\n }\n return out.reverse();\n };\n return { cool: arm(coolHue), mid, warm: [...arm(warmHue)].reverse() };\n}\n\n/**\n * The hue of a hex colour, for feeding the ramp builders.\n *\n * @param hex - Any hex colour.\n * @returns Its OKLCH hue in degrees.\n */\nexport function hueOf(hex: string): number {\n return hexToOklch(hex).h;\n}\n"],"mappings":";AAsBA,IAAM,IAAO;CACT,OAAO;EAAE,MAAM;EAAM,IAAI;CAAK;CAC9B,MAAM;EAAE,MAAM;EAAM,IAAI;CAAK;AACjC;AAYA,SAAS,EAAW,GAAmB;CACnC,OAAO,MAAO,OAAQ,IAAI,KAAK,IAAI,IAAI,IAAI,CAAC,KAAK;AACrD;AAsBA,SAAgB,EACZ,GACA,GACA,IAAA,GACA,IAAa,KACL;CACR,IAAM,IAAO,EAAK,IACZ,IAAgB,CAAC;CACvB,KAAK,IAAI,IAAI,GAAG,IAAI,GAAO,KAAK,GAAG;EAC/B,IAAM,IAAI,MAAU,IAAI,IAAI,KAAK,IAAQ;EACzC,EAAI,KACA,EAAW;GACP,GAAG,EAAK,QAAQ,EAAK,KAAK,EAAK,QAAQ;GACvC,GAAG,IAAa,EAAW,CAAC;GAC5B,GAAG;EACP,CAAC,CACL;CACJ;CACA,OAAO;AACX;AAmCA,SAAgB,EAAmB,EAC/B,YACA,YACA,QACA,SACA,WAAA,KAOc;CAKd,IAAM,IAAO,MAAS,UAAU;EAAE,MAAM;EAAM,IAAI;CAAI,IAAI;EAAE,MAAM;EAAM,IAAI;CAAK,GAC3E,KAAO,MAA0B;EACnC,IAAM,IAAgB,CAAC;EACvB,KAAK,IAAI,IAAI,GAAG,IAAI,GAAO,KAAK,GAAG;GAC/B,IAAM,IAAI,MAAU,IAAI,IAAI,KAAK,IAAQ;GACzC,EAAI,KACA,EAAW;IACP,GAAG,EAAK,QAAQ,EAAK,KAAK,EAAK,QAAQ;IACvC,GAAG,MAAO,EAAW,IAAI,IAAI,EAAG;IAChC,GAAG;GACP,CAAC,CACL;EACJ;EACA,OAAO,EAAI,QAAQ;CACvB;CACA,OAAO;EAAE,MAAM,EAAI,CAAO;EAAG;EAAK,MAAM,CAAC,GAAG,EAAI,CAAO,CAAC,CAAC,CAAC,QAAQ;CAAE;AACxE;AAQA,SAAgB,EAAM,GAAqB;CACvC,OAAO,EAAW,CAAG,CAAC,CAAC;AAC3B"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tempest-react-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "SDK público da Tempest com componentes, hooks e integrações para projetos React.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -194,6 +194,7 @@
|
|
|
194
194
|
"globals": "^17.7.0",
|
|
195
195
|
"husky": "^9.1.7",
|
|
196
196
|
"jsdom": "^29.1.1",
|
|
197
|
+
"jsqr": "^1.4.0",
|
|
197
198
|
"lint-staged": "^17.0.5",
|
|
198
199
|
"lucide-react": "^1.26.0",
|
|
199
200
|
"onnxruntime-web": "^1.27.0",
|