tempest-react-sdk 0.74.0 → 0.75.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/README.md +1 -1
  2. package/dist/br/BrazilMap.module.cjs.map +1 -1
  3. package/dist/br/BrazilMap.module.js.map +1 -1
  4. package/dist/br/MunicipalitySearch.cjs +1 -1
  5. package/dist/br/MunicipalitySearch.cjs.map +1 -1
  6. package/dist/br/MunicipalitySearch.js +66 -48
  7. package/dist/br/MunicipalitySearch.js.map +1 -1
  8. package/dist/br/MunicipalitySearch.module.cjs +1 -1
  9. package/dist/br/MunicipalitySearch.module.cjs.map +1 -1
  10. package/dist/br/MunicipalitySearch.module.js +4 -3
  11. package/dist/br/MunicipalitySearch.module.js.map +1 -1
  12. package/dist/br.d.ts +12 -0
  13. package/dist/components/AIChat/AIChat.module.cjs.map +1 -1
  14. package/dist/components/AIChat/AIChat.module.js.map +1 -1
  15. package/dist/components/AppBar/AppBar.cjs +1 -1
  16. package/dist/components/AppBar/AppBar.cjs.map +1 -1
  17. package/dist/components/AppBar/AppBar.js +1 -0
  18. package/dist/components/AppBar/AppBar.js.map +1 -1
  19. package/dist/components/AppBar/AppBar.module.cjs +1 -1
  20. package/dist/components/AppBar/AppBar.module.cjs.map +1 -1
  21. package/dist/components/AppBar/AppBar.module.js +12 -11
  22. package/dist/components/AppBar/AppBar.module.js.map +1 -1
  23. package/dist/components/Button/Button.module.cjs.map +1 -1
  24. package/dist/components/Button/Button.module.js.map +1 -1
  25. package/dist/components/Button/ButtonContent.cjs.map +1 -1
  26. package/dist/components/Button/ButtonContent.js.map +1 -1
  27. package/dist/components/Carousel/Carousel.module.cjs.map +1 -1
  28. package/dist/components/Carousel/Carousel.module.js.map +1 -1
  29. package/dist/components/Chat/Chat.module.cjs.map +1 -1
  30. package/dist/components/Chat/Chat.module.js.map +1 -1
  31. package/dist/components/Combobox/Combobox.cjs +1 -1
  32. package/dist/components/Combobox/Combobox.cjs.map +1 -1
  33. package/dist/components/Combobox/Combobox.js +18 -23
  34. package/dist/components/Combobox/Combobox.js.map +1 -1
  35. package/dist/components/Combobox/use-listbox-dismiss.cjs +2 -0
  36. package/dist/components/Combobox/use-listbox-dismiss.cjs.map +1 -0
  37. package/dist/components/Combobox/use-listbox-dismiss.js +34 -0
  38. package/dist/components/Combobox/use-listbox-dismiss.js.map +1 -0
  39. package/dist/components/Dropzone/Dropzone.module.cjs.map +1 -1
  40. package/dist/components/Dropzone/Dropzone.module.js.map +1 -1
  41. package/dist/components/FilterBar/FilterBar.module.cjs.map +1 -1
  42. package/dist/components/FilterBar/FilterBar.module.js.map +1 -1
  43. package/dist/components/FloatingActionButton/FloatingActionButton.module.cjs.map +1 -1
  44. package/dist/components/FloatingActionButton/FloatingActionButton.module.js.map +1 -1
  45. package/dist/components/ImageCropper/ImageCropper.module.cjs.map +1 -1
  46. package/dist/components/ImageCropper/ImageCropper.module.js.map +1 -1
  47. package/dist/components/ListTile/ListTile.module.cjs.map +1 -1
  48. package/dist/components/ListTile/ListTile.module.js.map +1 -1
  49. package/dist/components/Markdown/Markdown.module.cjs.map +1 -1
  50. package/dist/components/Markdown/Markdown.module.js.map +1 -1
  51. package/dist/components/Menubar/Menubar.module.cjs.map +1 -1
  52. package/dist/components/Menubar/Menubar.module.js.map +1 -1
  53. package/dist/components/MultiSelect/MultiSelect.cjs +1 -1
  54. package/dist/components/MultiSelect/MultiSelect.cjs.map +1 -1
  55. package/dist/components/MultiSelect/MultiSelect.js +55 -54
  56. package/dist/components/MultiSelect/MultiSelect.js.map +1 -1
  57. package/dist/components/Navbar/Navbar.cjs +1 -1
  58. package/dist/components/Navbar/Navbar.cjs.map +1 -1
  59. package/dist/components/Navbar/Navbar.js +1 -0
  60. package/dist/components/Navbar/Navbar.js.map +1 -1
  61. package/dist/components/Navbar/Navbar.module.cjs +1 -1
  62. package/dist/components/Navbar/Navbar.module.cjs.map +1 -1
  63. package/dist/components/Navbar/Navbar.module.js +9 -8
  64. package/dist/components/Navbar/Navbar.module.js.map +1 -1
  65. package/dist/components/NavigationMenu/NavigationMenu.module.cjs.map +1 -1
  66. package/dist/components/NavigationMenu/NavigationMenu.module.js.map +1 -1
  67. package/dist/components/NavigationRail/NavigationRail.module.cjs.map +1 -1
  68. package/dist/components/NavigationRail/NavigationRail.module.js.map +1 -1
  69. package/dist/components/NotificationCenter/NotificationCenter.module.cjs.map +1 -1
  70. package/dist/components/NotificationCenter/NotificationCenter.module.js.map +1 -1
  71. package/dist/components/PasswordInput/PasswordInput.module.cjs.map +1 -1
  72. package/dist/components/PasswordInput/PasswordInput.module.js.map +1 -1
  73. package/dist/components/PinInput/PinInput.module.cjs.map +1 -1
  74. package/dist/components/PinInput/PinInput.module.js.map +1 -1
  75. package/dist/components/SectionHeader/SectionHeader.cjs +1 -1
  76. package/dist/components/SectionHeader/SectionHeader.cjs.map +1 -1
  77. package/dist/components/SectionHeader/SectionHeader.js +33 -25
  78. package/dist/components/SectionHeader/SectionHeader.js.map +1 -1
  79. package/dist/components/Tour/Tour.module.cjs.map +1 -1
  80. package/dist/components/Tour/Tour.module.js.map +1 -1
  81. package/dist/components/Transfer/Transfer.module.cjs.map +1 -1
  82. package/dist/components/Transfer/Transfer.module.js.map +1 -1
  83. package/dist/components/VirtualTable/VirtualTable.module.cjs.map +1 -1
  84. package/dist/components/VirtualTable/VirtualTable.module.js.map +1 -1
  85. package/dist/geo/TrajectoryMap.module.cjs.map +1 -1
  86. package/dist/geo/TrajectoryMap.module.js.map +1 -1
  87. package/dist/icons/IconPicker.module.cjs.map +1 -1
  88. package/dist/icons/IconPicker.module.js.map +1 -1
  89. package/dist/styles/actions.css +2 -2
  90. package/dist/styles/br.css +3 -3
  91. package/dist/styles/chat.css +5 -5
  92. package/dist/styles/component/AIChat.css +3 -3
  93. package/dist/styles/component/AppBar.css +1 -0
  94. package/dist/styles/component/BrazilMap.css +2 -2
  95. package/dist/styles/component/Button.css +1 -1
  96. package/dist/styles/component/Carousel.css +3 -3
  97. package/dist/styles/component/Chat.css +2 -2
  98. package/dist/styles/component/Dropzone.css +2 -1
  99. package/dist/styles/component/FilterBar.css +2 -2
  100. package/dist/styles/component/FloatingActionButton.css +1 -1
  101. package/dist/styles/component/IconPicker.css +1 -1
  102. package/dist/styles/component/ImageCropper.css +2 -2
  103. package/dist/styles/component/ListTile.css +1 -1
  104. package/dist/styles/component/Markdown.css +1 -1
  105. package/dist/styles/component/Menubar.css +1 -1
  106. package/dist/styles/component/MunicipalitySearch.css +1 -1
  107. package/dist/styles/component/Navbar.css +1 -0
  108. package/dist/styles/component/NavigationMenu.css +1 -1
  109. package/dist/styles/component/NavigationRail.css +1 -1
  110. package/dist/styles/component/NotificationCenter.css +3 -3
  111. package/dist/styles/component/PasswordInput.css +1 -1
  112. package/dist/styles/component/PinInput.css +1 -1
  113. package/dist/styles/component/Tour.css +2 -2
  114. package/dist/styles/component/TrajectoryMap.css +1 -1
  115. package/dist/styles/component/Transfer.css +2 -2
  116. package/dist/styles/component/VirtualTable.css +2 -2
  117. package/dist/styles/data.css +10 -10
  118. package/dist/styles/feedback.css +3 -3
  119. package/dist/styles/forms.css +6 -5
  120. package/dist/styles/geo.css +1 -1
  121. package/dist/styles/icons.css +1 -1
  122. package/dist/styles/inverse.css +225 -0
  123. package/dist/styles/navigation.css +8 -6
  124. package/dist/styles.css +1 -1
  125. package/dist/tempest-react-sdk.d.ts +130 -17
  126. package/dist/theme/create-theme.cjs +1 -1
  127. package/dist/theme/create-theme.cjs.map +1 -1
  128. package/dist/theme/create-theme.js +6 -1
  129. package/dist/theme/create-theme.js.map +1 -1
  130. package/dist/theme/inverse-render.cjs +1 -1
  131. package/dist/theme/inverse-render.cjs.map +1 -1
  132. package/dist/theme/inverse-render.js +1 -1
  133. package/dist/theme/inverse-render.js.map +1 -1
  134. package/dist/theme/inverse-restore.cjs +5 -0
  135. package/dist/theme/inverse-restore.cjs.map +1 -0
  136. package/dist/theme/inverse-restore.js +30 -0
  137. package/dist/theme/inverse-restore.js.map +1 -0
  138. package/dist/theme/inverse-surface.cjs +1 -1
  139. package/dist/theme/inverse-surface.cjs.map +1 -1
  140. package/dist/theme/inverse-surface.js +69 -72
  141. package/dist/theme/inverse-surface.js.map +1 -1
  142. package/package.json +1 -1
@@ -976,7 +976,22 @@ export declare interface AppBarProps extends Omit<HTMLAttributes<HTMLElement>, "
976
976
  * development the bar says so in the console when it detects it.
977
977
  */
978
978
  sticky?: boolean;
979
- /** Visual tone. Default `"surface"`. */
979
+ /**
980
+ * Visual tone. Default `"surface"`.
981
+ *
982
+ * `"inverse"` writes `data-tempest-tone="inverse"` and paints the bar with the
983
+ * inverse surface's `--tempest-bg`, so the back button, the title and the
984
+ * components in `actions` invert with it. The tokens are opt-in:
985
+ * `tempest-react-sdk/styles/inverse.css` for the SDK's own blue, or
986
+ * `createTheme({ primary, inverse: createInverseSurface })` for yours. With
987
+ * neither loaded the bar falls back to the page's `--tempest-bg` and
988
+ * `--tempest-text` — legible, not inverted.
989
+ *
990
+ * A separate tone rather than a change to `"primary"`, because the inverse
991
+ * fill is not always the brand's `500`: for `#0066ff` it is the `800`
992
+ * (`#042e75`), and turning `"primary"` into it would repaint every bar that
993
+ * uses it today.
994
+ */
980
995
  tone?: AppBarTone;
981
996
  /** Thin bottom border. Default `true`. */
982
997
  bordered?: boolean;
@@ -984,7 +999,21 @@ export declare interface AppBarProps extends Omit<HTMLAttributes<HTMLElement>, "
984
999
  safeArea?: boolean;
985
1000
  }
986
1001
 
987
- export declare type AppBarTone = "surface" | "primary" | "transparent";
1002
+ /**
1003
+ * Visual tone of an {@link AppBar}.
1004
+ *
1005
+ * - `"surface"` — the page's surface and text.
1006
+ * - `"primary"` — the brand fill (`--tempest-primary`) with
1007
+ * `--tempest-primary-foreground` ink. Only the bar's own text and back button
1008
+ * follow the fill: components in `actions` keep the page's tokens. Measured in
1009
+ * Chromium over the SDK's `#0066ff`, a primary `Button` inside it has the same
1010
+ * color as the bar (1:1) and its focus ring reads 1.41:1.
1011
+ * - `"inverse"` — the inverse surface: every color token inverted inside the bar,
1012
+ * so the components in it are measured against the fill. See
1013
+ * {@link AppBarProps.tone}.
1014
+ * - `"transparent"` — no background; the text inherits.
1015
+ */
1016
+ export declare type AppBarTone = "surface" | "primary" | "inverse" | "transparent";
988
1017
 
989
1018
  /**
990
1019
  * The `code` and `message` a thrown value becomes on the wire.
@@ -3224,7 +3253,9 @@ declare interface ColorScaleOptions {
3224
3253
  * Combobox — text input with a filterable dropdown of options.
3225
3254
  *
3226
3255
  * Selecting an option fires `onChange(value)`. Typing filters the list.
3227
- * Keyboard: ArrowUp/ArrowDown to navigate, Enter to select, Esc to close.
3256
+ * Keyboard: ArrowUp/ArrowDown to navigate — the active option is announced
3257
+ * through `aria-activedescendant`, focus stays on the input — Enter to select,
3258
+ * Esc to close, and Tab/Shift+Tab leave the field closing the list.
3228
3259
  */
3229
3260
  export declare function Combobox({ options, value, onChange, label, placeholder, helperText, error, disabled, filter, emptyMessage, portal, className, }: ComboboxProps): JSX.Element;
3230
3261
 
@@ -4052,8 +4083,14 @@ export declare function createInMemoryFlags(options?: InMemoryFlagsOptions): Fea
4052
4083
  * section paints the same in the light and the dark page scheme: the brand is
4053
4084
  * the brand in both.
4054
4085
  *
4055
- * @param options - The brand, plus the status and chart colors the theme names.
4056
- * @returns The token map, the scheme it reads as, and the fill step used.
4086
+ * The result also carries the rules that let a region inside the surface go
4087
+ * back to the page's tokens (`data-tempest-tone="default"`), written for the
4088
+ * selectors in `options`.
4089
+ *
4090
+ * @param options - The brand, plus the status and chart colors the theme names
4091
+ * and the selectors the surface and the page are written under.
4092
+ * @returns The token map, the scheme it reads as, the fill step used and the
4093
+ * default-tone rules.
4057
4094
  */
4058
4095
  export declare function createInverseSurface(options: InverseSurfaceOptions): InverseSurface;
4059
4096
 
@@ -8487,6 +8524,12 @@ export declare interface InverseSurface {
8487
8524
  scheme: InverseScheme;
8488
8525
  /** Step of the brand's light ramp used as the fill. */
8489
8526
  step: ScaleStep;
8527
+ /**
8528
+ * CSS written after the surface: the rules that let a region inside it go
8529
+ * back to the page's tokens with `data-tempest-tone="default"`. Rendered by
8530
+ * the generator, so `createTheme` alone does not ship the code that writes it.
8531
+ */
8532
+ rules?: string;
8490
8533
  }
8491
8534
 
8492
8535
  /**
@@ -8512,6 +8555,18 @@ export declare interface InverseSurfaceOptions {
8512
8555
  statuses?: Partial<Record<ThemeStatus, string>>;
8513
8556
  /** Categorical chart colors the theme names; else the SDK's defaults. */
8514
8557
  chart?: readonly string[];
8558
+ /**
8559
+ * Selector the surface is written under, which the default-tone rules keep
8560
+ * out of the elements that save the page's values. Default
8561
+ * `'[data-tempest-tone="inverse"]'`.
8562
+ */
8563
+ selector?: string;
8564
+ /**
8565
+ * Selectors the page's schemes are written under, so a region restores a
8566
+ * theme scoped to them too. Default `createTheme`'s: `:root` and
8567
+ * `'[data-tempest-theme="dark"]'`.
8568
+ */
8569
+ page?: PageSelectors;
8515
8570
  }
8516
8571
 
8517
8572
  /**
@@ -10215,8 +10270,10 @@ export declare function moveItem<T>(items: T[], from: number, to: number): T[];
10215
10270
  * removable chips inside the field. Selecting toggles a value; Backspace on an
10216
10271
  * empty query removes the last chip.
10217
10272
  *
10218
- * Keyboard: ArrowUp/ArrowDown navigate, Enter toggles the active option, Esc
10219
- * closes, Backspace (empty input) pops the last chip.
10273
+ * Keyboard: ArrowUp/ArrowDown navigate — the active option is announced through
10274
+ * `aria-activedescendant`, focus stays on the input — Enter toggles the active
10275
+ * option, Esc closes, Backspace (empty input) pops the last chip, and
10276
+ * Tab/Shift+Tab leave the field closing the list.
10220
10277
  */
10221
10278
  export declare function MultiSelect({ options, value, onChange, label, placeholder, helperText, error, disabled, maxItems, filter, emptyMessage, portal, className, }: MultiSelectProps): JSX.Element;
10222
10279
 
@@ -10281,13 +10338,41 @@ export declare interface NavbarProps extends HTMLAttributes<HTMLElement> {
10281
10338
  actions?: ReactNode;
10282
10339
  /** Make the bar sticky at the top of the scroll container. Default `true`. */
10283
10340
  sticky?: boolean;
10284
- /** Visual tone. Default `"surface"`. */
10341
+ /**
10342
+ * Visual tone. Default `"surface"`.
10343
+ *
10344
+ * `"inverse"` writes `data-tempest-tone="inverse"` and paints the bar with the
10345
+ * inverse surface's `--tempest-bg`, so the components in the slots invert with
10346
+ * it. The tokens are opt-in: `tempest-react-sdk/styles/inverse.css` for the
10347
+ * SDK's own blue, or `createTheme({ primary, inverse: createInverseSurface })`
10348
+ * for yours. With neither loaded the bar falls back to the page's
10349
+ * `--tempest-bg` and `--tempest-text` — legible, not inverted.
10350
+ *
10351
+ * A separate tone rather than a change to `"primary"`, because the inverse
10352
+ * fill is not always the brand's `500`: for `#0066ff` it is the `800`
10353
+ * (`#042e75`), and turning `"primary"` into it would repaint every bar that
10354
+ * uses it today.
10355
+ */
10285
10356
  tone?: NavbarTone;
10286
10357
  /** When set, renders a thin bottom border. Default `true`. */
10287
10358
  bordered?: boolean;
10288
10359
  }
10289
10360
 
10290
- export declare type NavbarTone = "surface" | "primary" | "transparent";
10361
+ /**
10362
+ * Visual tone of a {@link Navbar}.
10363
+ *
10364
+ * - `"surface"` — the page's surface and text.
10365
+ * - `"primary"` — the brand fill (`--tempest-primary`) with
10366
+ * `--tempest-primary-foreground` ink. Only the bar's own text follows the fill:
10367
+ * components inside it keep the page's tokens. Measured in Chromium over the
10368
+ * SDK's `#0066ff`, a link button inside it reads 1:1, muted text 1.59:1 and the
10369
+ * focus ring 1.41:1. Fine for a bar that holds a logo and plain text.
10370
+ * - `"inverse"` — the inverse surface: every color token inverted inside the bar,
10371
+ * so a primary `Button`, a link, the focus ring and muted text are measured
10372
+ * against the fill. See {@link NavbarProps.tone}.
10373
+ * - `"transparent"` — no background; the text inherits.
10374
+ */
10375
+ export declare type NavbarTone = "surface" | "primary" | "inverse" | "transparent";
10291
10376
 
10292
10377
  export { Navigate }
10293
10378
 
@@ -11212,6 +11297,14 @@ export declare interface PageProps extends Omit<HTMLAttributes<HTMLElement>, "ti
11212
11297
  children?: ReactNode;
11213
11298
  }
11214
11299
 
11300
+ /** Where the page's schemes are written: the selectors `createTheme` uses. */
11301
+ export declare interface PageSelectors {
11302
+ /** Selector of the light scheme. Default `:root`. */
11303
+ selector: string;
11304
+ /** Selector of the dark scheme. Default `'[data-tempest-theme="dark"]'`. */
11305
+ darkSelector: string;
11306
+ }
11307
+
11215
11308
  /**
11216
11309
  * Numeric pagination controls. Pair with {@link usePagination} for state.
11217
11310
  *
@@ -13529,12 +13622,24 @@ export declare interface SearchBarProps extends Omit<InputHTMLAttributes<HTMLInp
13529
13622
  * accessible name; without one it is an anonymous group and screen-reader
13530
13623
  * landmark navigation skips it. Wiring `aria-labelledby` to the heading id is
13531
13624
  * the same mechanical step on every page, so the section does it: every
13532
- * `SectionHeader` with a `title` below it registers its id through context (so
13533
- * wrappers in between do not break the link) and the first one mounted names
13534
- * the section; when it unmounts, the next one takes over. An explicit
13535
- * `aria-labelledby` or `aria-label` wins over the registered one, and with no
13625
+ * `SectionHeader` with a `title` below it reaches the section through context
13626
+ * (so wrappers in between do not break the link) and the first one names it;
13627
+ * when it unmounts, the next one takes over. An explicit `aria-labelledby` or
13628
+ * `aria-label` wins over the automatic one, and once mounted with no titled
13536
13629
  * heading the attribute is left off rather than pointing at nothing.
13537
13630
  *
13631
+ * The section owns the id (`useId`), so the link is already in the first
13632
+ * render — `renderToString` and the HTML a server sends — where no effect runs.
13633
+ * The parent's attributes are written before any child renders, so the section
13634
+ * cannot know what is below it: it always writes its own id, and the first
13635
+ * titled `SectionHeader` in render order puts that id on its heading. Two
13636
+ * cases cannot be known on the server, and both are settled on hydration
13637
+ * without a mismatch warning: with no titled heading the server attribute is
13638
+ * dropped (until then it resolves to nothing, which Chromium 153 exposes as
13639
+ * the same anonymous group as no attribute at all), and when that first header
13640
+ * has an explicit `id` the attribute moves to it. A consumer that needs the
13641
+ * explicit id in the server HTML passes `aria-labelledby` itself.
13642
+ *
13538
13643
  * @example
13539
13644
  * <Section>
13540
13645
  * <SectionHeader title="Planos" />
@@ -13555,9 +13660,10 @@ export declare function Section({ children, tone, ...props }: SectionProps): JSX
13555
13660
  *
13556
13661
  * The heading level is a prop because one page needs one `<h1>` and many
13557
13662
  * `<h2>` that look alike; the visual stays the same at every level so the
13558
- * outline is picked by meaning, not by size. Inside a `<Section>` the heading
13559
- * id is registered with it, which is what gives the `region` landmark its
13560
- * accessible name.
13663
+ * outline is picked by meaning, not by size. Inside a `<Section>` the first
13664
+ * titled header takes the section's own id for its heading (unless `id` is
13665
+ * given) and registers it, which is what gives the `region` landmark its
13666
+ * accessible name — in the server HTML too.
13561
13667
  *
13562
13668
  * @example
13563
13669
  * <Section>
@@ -13600,12 +13706,19 @@ export declare interface SectionProps extends HTMLAttributes<HTMLElement> {
13600
13706
  * the section keeps the page's tokens — legible, not inverted. The prop
13601
13707
  * exists so the tone is typed: a typo in a hand-written attribute fails
13602
13708
  * silently, a typo here fails the build.
13709
+ *
13710
+ * `"default"` hands a region inside an inverse section back to the page's
13711
+ * tokens — light or dark, whichever the page is on — by writing
13712
+ * `data-tempest-tone="default"`: a white form or price card in a navy hero.
13713
+ * The rules ship with the inverse tokens, so it costs nothing until those are
13714
+ * loaded, and outside an inverse section the tokens it restores are the ones
13715
+ * already there.
13603
13716
  */
13604
13717
  tone?: SectionTone;
13605
13718
  }
13606
13719
 
13607
13720
  /** Surface tone a {@link Section} can take. */
13608
- export declare type SectionTone = "inverse";
13721
+ export declare type SectionTone = "inverse" | "default";
13609
13722
 
13610
13723
  /**
13611
13724
  * iOS-style segmented control. Two-to-five mutually-exclusive options
@@ -1,5 +1,5 @@
1
1
  const e=require("../utils/dev-mode.cjs"),t=require("./color.cjs"),n=require("./data-viz-ramps.cjs"),r=require("./inverse-render.cjs"),i=require("./status-tokens.cjs");var a={none:{xs:`0`,sm:`0`,md:`0`,lg:`0`,xl:`0`,"2xl":`0`},sm:{xs:`1px`,sm:`2px`,md:`4px`,lg:`6px`,xl:`8px`,"2xl":`12px`},md:{xs:`2px`,sm:`4px`,md:`8px`,lg:`12px`,xl:`16px`,"2xl":`24px`},lg:{xs:`4px`,sm:`6px`,md:`12px`,lg:`16px`,xl:`22px`,"2xl":`32px`},xl:{xs:`6px`,sm:`10px`,md:`16px`,lg:`24px`,xl:`32px`,"2xl":`44px`},full:{xs:`4px`,sm:`8px`,md:`9999px`,lg:`9999px`,xl:`9999px`,"2xl":`9999px`}},o=[50,100,200,300,400,500,600,700,800,900];function s(e,t,n){for(let r of o)e[`--tempest-${t}-${r}`]=n[r]}var c=4.5;function l(e,n,r){for(let i of r)if(t.contrastRatio(e[n],e[i])>=c)return i;return r[r.length-1]}var u=3,d={light:[`#ffffff`,`#f8f9fb`,`#f1f3f6`,`#e4e7ec`],dark:[`#0b0d12`,`#14171f`,`#1d2230`,`#262d3f`]},f=[500,600,700,800,900];function p(e,n){let r=r=>Math.min(...n.map(n=>t.contrastRatio(e[r],n))),i=f[0],a=-1;for(let e of f){let t=r(e);if(t>=u)return e;t>a&&(i=e,a=t)}return i}function m(e,t){if(!t)return d[e];let n=t[e];return e===`light`?[`#ffffff`,n[50],n[100],n[200]]:[n[50],n[100],n[200],n[300]]}function h(t){t===void 0||t>=1||e.isDevBuild()&&console.warn(`[tempest-react-sdk] createTheme({ focusRingAlpha: ${t} }) makes the focus ring translucent, so its contrast becomes whatever it composites over — measured below the 3:1 of WCAG 2.2 SC 1.4.11 on every surface the SDK paints. Drop the option to get the opaque ring, which is derived from your brand ramp and measured against those surfaces.`)}function g(e,n,r,i,a){e[`--tempest-primary`]=`var(--tempest-primary-500)`,r===`light`?(e[`--tempest-primary-hover`]=`var(--tempest-primary-600)`,e[`--tempest-primary-active`]=`var(--tempest-primary-700)`,e[`--tempest-primary-soft`]=`var(--tempest-primary-50)`,e[`--tempest-primary-soft-hover`]=`var(--tempest-primary-100)`,e[`--tempest-primary-on-soft`]=`var(--tempest-primary-${l(n,50,[600,700,800,900])})`):(e[`--tempest-primary-hover`]=`var(--tempest-primary-400)`,e[`--tempest-primary-active`]=`var(--tempest-primary-300)`,e[`--tempest-primary-soft`]=`var(--tempest-primary-100)`,e[`--tempest-primary-soft-hover`]=`var(--tempest-primary-200)`,e[`--tempest-primary-on-soft`]=`var(--tempest-primary-${l(n,100,[700,800,900])})`);let o=t.readableForeground(n[500]);e[`--tempest-primary-foreground`]=o,e[`--tempest-text-on-primary`]=o;let s=n[p(n,i)];e[`--tempest-focus-ring-color`]=a===void 0||a>=1?s:t.hexToRgbaString(s,a),e[`--tempest-selected-indicator`]=s}function _(e,n,r){r===`light`?(e[`--tempest-bg`]=`#ffffff`,e[`--tempest-surface`]=`var(--tempest-gray-50)`,e[`--tempest-surface-2`]=`var(--tempest-gray-100)`,e[`--tempest-surface-3`]=`var(--tempest-gray-200)`,e[`--tempest-border`]=`var(--tempest-gray-200)`,e[`--tempest-border-strong`]=`var(--tempest-gray-300)`,e[`--tempest-text`]=`var(--tempest-gray-900)`,e[`--tempest-text-muted`]=`var(--tempest-gray-600)`,e[`--tempest-text-subtle`]=`var(--tempest-gray-500)`):(e[`--tempest-bg`]=n[50],e[`--tempest-surface`]=`var(--tempest-gray-100)`,e[`--tempest-surface-2`]=`var(--tempest-gray-200)`,e[`--tempest-surface-3`]=`var(--tempest-gray-300)`,e[`--tempest-border`]=`var(--tempest-gray-300)`,e[`--tempest-border-strong`]=`var(--tempest-gray-400)`,e[`--tempest-text`]=`var(--tempest-gray-900)`,e[`--tempest-text-muted`]=`var(--tempest-gray-700)`,e[`--tempest-text-subtle`]=`var(--tempest-gray-600)`),e[`--tempest-neutral-on-solid`]=t.readableForeground(n[700],`#ffffff`,i.ON_SOLID_INK)}function v(e,t){let n=Object.entries(t);return n.length===0?``:`${e} {\n${n.map(([e,t])=>` ${e}: ${t};`).join(`
2
- `)}\n}`}function y(e={}){let{primary:o,gray:c,chart:l,radius:u,focusRingAlpha:d,selector:f=`:root`,darkSelector:p=`[data-tempest-theme="dark"]`,inverse:y,inverseSelector:b=r.INVERSE_SELECTOR}=e;h(d);let x={},S={},C=c?{light:t.createColorScale(c,`light`,{anchor:!1,neutral:!0}),dark:t.createColorScale(c,`dark`,{anchor:!1,neutral:!0})}:void 0;if(o){let e=t.createColorScale(o,`light`),n=t.createColorScale(o,`dark`);s(x,`primary`,e),s(S,`primary`,n),g(x,e,`light`,m(`light`,C),d),g(S,n,`dark`,m(`dark`,C),d)}C&&(s(x,`gray`,C.light),s(S,`gray`,C.dark),_(x,C.light,`light`),_(S,C.dark,`dark`));for(let n of i.THEME_STATUSES){let r=e[n];r&&(i.writeStatus(x,n,t.createColorScale(r,`light`),`light`),i.writeStatus(S,n,t.createColorScale(r,`dark`),`dark`))}l?.length&&(l.forEach((e,t)=>{x[`--tempest-chart-${t+1}`]=e}),x[`--tempest-chart-count`]=String(l.length));let w=l?.[0]??o;if(w){let t=n.hueOf(w),r=e.danger?n.hueOf(e.danger):(t+180)%360;for(let[e,i]of[[x,`light`],[S,`dark`]]){n.buildRamp(t,i).forEach((t,n)=>{e[`--tempest-chart-sequential-${n+1}`]=t});let a=n.buildDivergingRamp({coolHue:t,warmHue:r,mid:i===`light`?`#e4e7ec`:`#262d3f`,mode:i});[...a.cool,a.mid,...a.warm].forEach((t,n)=>{e[`--tempest-chart-diverging-${n+1}`]=t})}}if(u){let e=typeof u==`string`?a[u]:u;for(let[t,n]of Object.entries(e))x[`--tempest-radius-${t}`]=n}let T=o&&y?y({primary:o,statuses:{success:e.success,warning:e.warning,danger:e.danger,info:e.info},chart:l}):null,E=[v(f,x),v(p,S),T?r.renderInverseSurface(b,T):``].filter(Boolean).join(`
2
+ `)}\n}`}function y(e={}){let{primary:o,gray:c,chart:l,radius:u,focusRingAlpha:d,selector:f=`:root`,darkSelector:p=`[data-tempest-theme="dark"]`,inverse:y,inverseSelector:b=r.INVERSE_SELECTOR}=e;h(d);let x={},S={},C=c?{light:t.createColorScale(c,`light`,{anchor:!1,neutral:!0}),dark:t.createColorScale(c,`dark`,{anchor:!1,neutral:!0})}:void 0;if(o){let e=t.createColorScale(o,`light`),n=t.createColorScale(o,`dark`);s(x,`primary`,e),s(S,`primary`,n),g(x,e,`light`,m(`light`,C),d),g(S,n,`dark`,m(`dark`,C),d)}C&&(s(x,`gray`,C.light),s(S,`gray`,C.dark),_(x,C.light,`light`),_(S,C.dark,`dark`));for(let n of i.THEME_STATUSES){let r=e[n];r&&(i.writeStatus(x,n,t.createColorScale(r,`light`),`light`),i.writeStatus(S,n,t.createColorScale(r,`dark`),`dark`))}l?.length&&(l.forEach((e,t)=>{x[`--tempest-chart-${t+1}`]=e}),x[`--tempest-chart-count`]=String(l.length));let w=l?.[0]??o;if(w){let t=n.hueOf(w),r=e.danger?n.hueOf(e.danger):(t+180)%360;for(let[e,i]of[[x,`light`],[S,`dark`]]){n.buildRamp(t,i).forEach((t,n)=>{e[`--tempest-chart-sequential-${n+1}`]=t});let a=n.buildDivergingRamp({coolHue:t,warmHue:r,mid:i===`light`?`#e4e7ec`:`#262d3f`,mode:i});[...a.cool,a.mid,...a.warm].forEach((t,n)=>{e[`--tempest-chart-diverging-${n+1}`]=t})}}if(u){let e=typeof u==`string`?a[u]:u;for(let[t,n]of Object.entries(e))x[`--tempest-radius-${t}`]=n}let T=o&&y?y({primary:o,statuses:{success:e.success,warning:e.warning,danger:e.danger,info:e.info},chart:l,selector:b,page:{selector:f,darkSelector:p}}):null,E=[v(f,x),v(p,S),T?r.renderInverseSurface(b,T):``].filter(Boolean).join(`
3
3
 
4
4
  `);return T?{light:x,dark:S,inverse:T.tokens,css:E}:{light:x,dark:S,css:E}}function b(e){if(!e.primary)return null;let n=t.createColorScale(e.primary,`light`);return t.contrastRatio(n[500],t.readableForeground(n[500]))}exports.createTheme=y,exports.themeContrast=b;
5
5
  //# sourceMappingURL=create-theme.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"create-theme.cjs","names":[],"sources":["../../src/theme/create-theme.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — one pass that emits the whole token set — surfaces,\n * text, borders, states, the chart ramps — for both modes, with every value checked\n * against the surface it will sit on. The checks are what make the length: each\n * token is derived and then verified against the same background table.\n */\n/**\n * Theme factory: turns a handful of brand colors into the full set of\n * `--tempest-*` token overrides, for both light and dark.\n *\n * Rebranding used to mean hand-writing ~167 custom properties (and getting the\n * dark ramp inversion right by hand). `createTheme({ primary: \"#7c3aed\" })`\n * emits the same thing, derived in OKLCH so the ramp stays perceptually even.\n */\nimport {\n contrastRatio,\n createColorScale,\n hexToRgbaString,\n readableForeground,\n type ColorScale,\n type ScaleStep,\n} from \"./color\";\nimport { buildDivergingRamp, buildRamp, hueOf } from \"./data-viz-ramps\";\nimport { isDevBuild } from \"../utils/dev-mode\";\nimport {\n INVERSE_SELECTOR,\n renderInverseSurface,\n type InverseSurfaceGenerator,\n} from \"./inverse-render\";\nimport { ON_SOLID_INK, THEME_STATUSES, writeStatus } from \"./status-tokens\";\n\n/** Radius presets, applied to the whole `--tempest-radius-*` family at once. */\nexport type ThemeRadius = \"none\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"full\";\n\nexport type { ThemeStatus } from \"./status-tokens\";\n\n/** Input for {@link createTheme}. Every field is optional — omitted families keep the built-in tokens. */\nexport interface CreateThemeOptions {\n /** Brand color, used as the `500` step of the primary scale. */\n primary?: string;\n /** Neutral color, used as the `500` step of the gray scale (surfaces, borders, text). */\n gray?: string;\n /** Success color (`--tempest-success*`). */\n success?: string;\n /** Warning color (`--tempest-warning*`). */\n warning?: string;\n /** Danger color (`--tempest-danger*`). */\n danger?: string;\n /** Info color (`--tempest-info*`). */\n info?: string;\n /**\n * Categorical series colors for `tempest-react-sdk/charts`, in cycle order.\n * Written to `--tempest-chart-1` … `--tempest-chart-N`, so charts follow the\n * theme instead of a hardcoded palette.\n */\n chart?: string[];\n /** Corner radius scale. A preset name, or explicit per-step values. */\n radius?: ThemeRadius | Partial<Record<\"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\", string>>;\n /**\n * Opacity of `--tempest-focus-ring-color`. Omit it — the default ring is opaque.\n *\n * A translucent ring has no contrast of its own; it has the contrast of\n * whatever it composites over. Passing a value below `1` reintroduces the\n * failure 0.61.0 fixed in the built-in tokens, and logs a warning in a\n * development build. Kept because a translucent ring over a background you\n * control is a legitimate choice.\n */\n focusRingAlpha?: number;\n /** Selector the light tokens are written under. Default `\":root\"`. */\n selector?: string;\n /** Selector the dark tokens are written under. Default `'[data-tempest-theme=\"dark\"]'`. */\n darkSelector?: string;\n /**\n * Also emit the inverse surface — the token set for a section painted in the\n * brand color — by passing its generator, `createInverseSurface`. Off by\n * default; ignored without `primary`.\n *\n * The generator is passed in rather than switched on with `true` so that\n * `createTheme` never imports it: an app that does not paint a section in\n * its brand does not ship the 1.68 kB brotli it costs.\n *\n * @example\n * ```ts\n * import { createInverseSurface, createTheme } from \"tempest-react-sdk\";\n *\n * createTheme({ primary: \"#03184b\", inverse: createInverseSurface });\n * ```\n */\n inverse?: InverseSurfaceGenerator;\n /** Selector the inverse tokens are written under. Default `'[data-tempest-tone=\"inverse\"]'`. */\n inverseSelector?: string;\n}\n\n/** A generated theme: token maps per color scheme, plus the CSS text that carries them. */\nexport interface GeneratedTheme {\n /** Light-scheme custom properties, without the leading `--`-less names (keys include `--`). */\n light: Record<string, string>;\n /** Dark-scheme custom properties. */\n dark: Record<string, string>;\n /**\n * Inverse-surface custom properties — literal colors, identical under the\n * light and the dark page scheme. Present only when the theme names a\n * `primary` and passes `inverse`.\n */\n inverse?: Record<string, string>;\n /** Both blocks rendered as CSS, ready for {@link applyTheme} or a stylesheet. */\n css: string;\n}\n\nconst RADIUS_PRESETS: Record<ThemeRadius, Record<string, string>> = {\n none: { xs: \"0\", sm: \"0\", md: \"0\", lg: \"0\", xl: \"0\", \"2xl\": \"0\" },\n sm: { xs: \"1px\", sm: \"2px\", md: \"4px\", lg: \"6px\", xl: \"8px\", \"2xl\": \"12px\" },\n md: { xs: \"2px\", sm: \"4px\", md: \"8px\", lg: \"12px\", xl: \"16px\", \"2xl\": \"24px\" },\n lg: { xs: \"4px\", sm: \"6px\", md: \"12px\", lg: \"16px\", xl: \"22px\", \"2xl\": \"32px\" },\n xl: { xs: \"6px\", sm: \"10px\", md: \"16px\", lg: \"24px\", xl: \"32px\", \"2xl\": \"44px\" },\n full: { xs: \"4px\", sm: \"8px\", md: \"9999px\", lg: \"9999px\", xl: \"9999px\", \"2xl\": \"9999px\" },\n};\n\nconst SCALE_STEPS: ScaleStep[] = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900];\n\nfunction writeScale(tokens: Record<string, string>, name: string, scale: ColorScale): void {\n for (const step of SCALE_STEPS) {\n tokens[`--tempest-${name}-${step}`] = scale[step];\n }\n}\n\n/** Minimum contrast for body text, WCAG 2.x AA. */\nconst AA_TEXT_CONTRAST = 4.5;\n\n/**\n * Choose the ramp step for text sitting on the soft tint.\n *\n * A fixed step does not survive an arbitrary brand color: the built-in blue only\n * reaches 4.37:1 at `500` over its own `50` tint, and a generated emerald lands\n * at 4.41:1 at `600` — both fail AA for body text by a hair. So the step is\n * picked by measuring, walking away from the tint until it clears AA, and\n * falling back to the most extreme candidate when even that cannot (a very light\n * or very desaturated brand).\n *\n * @param scale - The generated ramp.\n * @param softStep - Step used as `--tempest-primary-soft`.\n * @param candidates - Steps to try, in order of preference.\n * @returns The first candidate clearing {@link AA_TEXT_CONTRAST}, else the last.\n */\nfunction pickOnSoftStep(\n scale: ColorScale,\n softStep: ScaleStep,\n candidates: ScaleStep[],\n): ScaleStep {\n for (const step of candidates) {\n if (contrastRatio(scale[softStep], scale[step]) >= AA_TEXT_CONTRAST) return step;\n }\n return candidates[candidates.length - 1];\n}\n\n/** Minimum contrast for a non-text indicator, WCAG 2.2 SC 1.4.11. */\nconst INDICATOR_CONTRAST = 3;\n\n/**\n * The four surfaces a focus ring can land on, as the SDK's own `colors.css`\n * paints them.\n *\n * Ported from `src/styles/colors.css` (`--tempest-bg`, `--tempest-surface`,\n * `--tempest-surface-2`, `--tempest-surface-3`, resolved through the gray ramp),\n * and pinned by a test that reads that file — a theme that only names `primary`\n * keeps these, so the ring has to be measured against them and not against a\n * guess. The light values resolve through `--tempest-gray-50/100/200`; the dark\n * ones are literals in the dark block.\n */\nconst SDK_SURFACES: Record<\"light\" | \"dark\", readonly string[]> = {\n light: [\"#ffffff\", \"#f8f9fb\", \"#f1f3f6\", \"#e4e7ec\"],\n dark: [\"#0b0d12\", \"#14171f\", \"#1d2230\", \"#262d3f\"],\n};\n\n/**\n * Ramp steps to try for the focus ring, in order.\n *\n * `500` first, because the ring should read as the brand when it can, then up\n * the ramp — which moves away from the surfaces in **both** schemes, because\n * `createColorScale` inverts the dark ramp: there `50` is the darkest step (it\n * is what `--tempest-bg` is made of) and `900` the lightest. Walking down in\n * dark mode looks like the right inversion and is the wrong direction; measured,\n * it left a near-black brand's ring at 1.04:1 over its own background.\n */\nconst FOCUS_RING_CANDIDATES: readonly ScaleStep[] = [500, 600, 700, 800, 900];\n\n/**\n * Choose the ramp step for the focus ring, by measuring it against the surfaces\n * it will actually be drawn on.\n *\n * A fixed step does not survive an arbitrary brand, and the measurement says so\n * loudly: of twelve brands checked against the four surfaces in both schemes —\n * 96 pairings — `500` opaque clears 3:1 in 58 of them, and `500` at the old\n * default alpha of 0.35 in 3. Picking the step by measurement clears all 96. The\n * brand from the report that opened this\n * (`#8100D7`) passes in light at 7.20:1 and fails in **dark** at 2.70:1 down to\n * 1.91:1 on `surface-3`; a yellow fails in light at 1.43:1. So the step is\n * picked the way {@link pickOnSoftStep} picks text on a tint: walk away from the\n * surfaces until the ring clears the floor against *all* of them.\n *\n * The fallback is the candidate with the best worst case rather than the last\n * one, because \"the most extreme step\" is not always the most contrasting one\n * once a brand is near-neutral.\n *\n * @param scale - The generated ramp.\n * @param surfaces - The four surfaces this theme will paint, opaque hex.\n * @returns The first candidate clearing {@link INDICATOR_CONTRAST} on every\n * surface, else the candidate whose weakest pairing is strongest.\n */\nfunction pickFocusRingStep(scale: ColorScale, surfaces: readonly string[]): ScaleStep {\n const worstCase = (step: ScaleStep): number =>\n Math.min(...surfaces.map((surface) => contrastRatio(scale[step], surface)));\n let best: ScaleStep = FOCUS_RING_CANDIDATES[0] as ScaleStep;\n let bestRatio = -1;\n for (const step of FOCUS_RING_CANDIDATES) {\n const measured = worstCase(step);\n if (measured >= INDICATOR_CONTRAST) return step;\n if (measured > bestRatio) {\n best = step;\n bestRatio = measured;\n }\n }\n return best;\n}\n\n/**\n * The surfaces this theme will actually paint, per scheme.\n *\n * A theme that names `gray` repaints all four, so measuring the ring against the\n * SDK's would be measuring the wrong backdrop — and a theme that does not keeps\n * the SDK's, which is the common case. The order and the ramp steps mirror\n * {@link writeNeutralAliases}, which is what emits them.\n *\n * @param scheme - Which scheme is being emitted.\n * @param grayScales - The generated neutral ramps, when the theme names one.\n * @returns Four opaque hex colors: bg, surface, surface-2, surface-3.\n */\nfunction surfacesOf(\n scheme: \"light\" | \"dark\",\n grayScales: { light: ColorScale; dark: ColorScale } | undefined,\n): readonly string[] {\n if (!grayScales) return SDK_SURFACES[scheme];\n const scale = grayScales[scheme];\n return scheme === \"light\"\n ? [\"#ffffff\", scale[50], scale[100], scale[200]]\n : [scale[50], scale[100], scale[200], scale[300]];\n}\n\n/**\n * Warn, in a development build, that an alpha was asked for the focus ring.\n *\n * A translucent ring has no contrast of its own — it has the contrast of\n * whatever it composites over — and that is the failure this option used to\n * ship by default: measured at alpha `0.35`, twelve brands across four surfaces\n * and both schemes produced 96 pairings, of which 3 cleared the 3:1 of WCAG 2.2\n * SC 1.4.11. It stays available, because a translucent ring over a\n * known background is a legitimate choice, but it is a choice a theme now has to\n * make on purpose.\n *\n * The warning is the difficult part to notice otherwise: a failing focus ring is\n * visible, it just does not separate.\n *\n * @param focusRingAlpha - The opacity the caller asked for, if any.\n */\nfunction warnOnTranslucentFocusRing(focusRingAlpha: number | undefined): void {\n if (focusRingAlpha === undefined || focusRingAlpha >= 1) return;\n if (!isDevBuild()) return;\n console.warn(\n `[tempest-react-sdk] createTheme({ focusRingAlpha: ${focusRingAlpha} }) makes the focus ring ` +\n \"translucent, so its contrast becomes whatever it composites over — measured below the 3:1 \" +\n \"of WCAG 2.2 SC 1.4.11 on every surface the SDK paints. Drop the option to get the opaque \" +\n \"ring, which is derived from your brand ramp and measured against those surfaces.\",\n );\n}\n\n/**\n * Emit the primary aliases for one scheme.\n *\n * The dark scheme walks the ramp the other way (hover is *lighter*, the soft\n * tint is a dark shade, and readable text on that tint is a light shade) — the\n * same inversion the built-in `colors.css` dark block does by hand.\n *\n * The selected-state indicator is derived from the same measurement as the focus\n * ring and emitted as its own token. It never takes `focusRingAlpha`: a\n * translucent ring over a known background is a legitimate choice a theme can\n * make, while a translucent *state* indicator has the contrast of whatever it\n * composites over, which is the defect this token exists to end.\n *\n * @param tokens - The token map for this scheme, written in place.\n * @param scale - The generated primary ramp.\n * @param scheme - Which scheme is being emitted.\n * @param surfaces - The surfaces this theme paints, for the focus-ring measurement.\n * @param focusRingAlpha - Opacity asked for the ring, or `undefined` for opaque.\n */\nfunction writePrimaryAliases(\n tokens: Record<string, string>,\n scale: ColorScale,\n scheme: \"light\" | \"dark\",\n surfaces: readonly string[],\n focusRingAlpha: number | undefined,\n): void {\n tokens[\"--tempest-primary\"] = \"var(--tempest-primary-500)\";\n if (scheme === \"light\") {\n tokens[\"--tempest-primary-hover\"] = \"var(--tempest-primary-600)\";\n tokens[\"--tempest-primary-active\"] = \"var(--tempest-primary-700)\";\n tokens[\"--tempest-primary-soft\"] = \"var(--tempest-primary-50)\";\n tokens[\"--tempest-primary-soft-hover\"] = \"var(--tempest-primary-100)\";\n tokens[\"--tempest-primary-on-soft\"] =\n `var(--tempest-primary-${pickOnSoftStep(scale, 50, [600, 700, 800, 900])})`;\n } else {\n tokens[\"--tempest-primary-hover\"] = \"var(--tempest-primary-400)\";\n tokens[\"--tempest-primary-active\"] = \"var(--tempest-primary-300)\";\n tokens[\"--tempest-primary-soft\"] = \"var(--tempest-primary-100)\";\n tokens[\"--tempest-primary-soft-hover\"] = \"var(--tempest-primary-200)\";\n tokens[\"--tempest-primary-on-soft\"] =\n `var(--tempest-primary-${pickOnSoftStep(scale, 100, [700, 800, 900])})`;\n }\n\n const foreground = readableForeground(scale[500]);\n tokens[\"--tempest-primary-foreground\"] = foreground;\n tokens[\"--tempest-text-on-primary\"] = foreground;\n const ring = scale[pickFocusRingStep(scale, surfaces)];\n tokens[\"--tempest-focus-ring-color\"] =\n focusRingAlpha === undefined || focusRingAlpha >= 1\n ? ring\n : hexToRgbaString(ring, focusRingAlpha);\n tokens[\"--tempest-selected-indicator\"] = ring;\n}\n\n/**\n * Emit the neutral surface/border/text aliases from a generated gray scale.\n *\n * `--tempest-bg` is pushed past the ramp on purpose: pure white in light mode and\n * a shade darker than `gray-50` in dark mode, so a raised surface still reads as\n * raised against the page.\n */\nfunction writeNeutralAliases(\n tokens: Record<string, string>,\n scale: ColorScale,\n scheme: \"light\" | \"dark\",\n): void {\n if (scheme === \"light\") {\n tokens[\"--tempest-bg\"] = \"#ffffff\";\n tokens[\"--tempest-surface\"] = \"var(--tempest-gray-50)\";\n tokens[\"--tempest-surface-2\"] = \"var(--tempest-gray-100)\";\n tokens[\"--tempest-surface-3\"] = \"var(--tempest-gray-200)\";\n tokens[\"--tempest-border\"] = \"var(--tempest-gray-200)\";\n tokens[\"--tempest-border-strong\"] = \"var(--tempest-gray-300)\";\n tokens[\"--tempest-text\"] = \"var(--tempest-gray-900)\";\n tokens[\"--tempest-text-muted\"] = \"var(--tempest-gray-600)\";\n tokens[\"--tempest-text-subtle\"] = \"var(--tempest-gray-500)\";\n } else {\n tokens[\"--tempest-bg\"] = scale[50];\n tokens[\"--tempest-surface\"] = \"var(--tempest-gray-100)\";\n tokens[\"--tempest-surface-2\"] = \"var(--tempest-gray-200)\";\n tokens[\"--tempest-surface-3\"] = \"var(--tempest-gray-300)\";\n tokens[\"--tempest-border\"] = \"var(--tempest-gray-300)\";\n tokens[\"--tempest-border-strong\"] = \"var(--tempest-gray-400)\";\n tokens[\"--tempest-text\"] = \"var(--tempest-gray-900)\";\n tokens[\"--tempest-text-muted\"] = \"var(--tempest-gray-700)\";\n tokens[\"--tempest-text-subtle\"] = \"var(--tempest-gray-600)\";\n }\n tokens[\"--tempest-neutral-on-solid\"] = readableForeground(scale[700], \"#ffffff\", ON_SOLID_INK);\n}\n\nfunction renderBlock(selector: string, tokens: Record<string, string>): string {\n const entries = Object.entries(tokens);\n if (entries.length === 0) return \"\";\n const body = entries.map(([name, value]) => ` ${name}: ${value};`).join(\"\\n\");\n return `${selector} {\\n${body}\\n}`;\n}\n\n/**\n * Generate `--tempest-*` overrides from a small brand description.\n *\n * Only the families you pass are generated; everything else falls through to the\n * SDK's own tokens, so a theme stays a patch and not a fork of `colors.css`.\n *\n * @example\n * ```ts\n * import { applyTheme, createTheme } from \"tempest-react-sdk\";\n *\n * const theme = createTheme({\n * primary: \"#7c3aed\",\n * radius: \"lg\",\n * chart: [\"#7c3aed\", \"#0ea5e9\", \"#22c55e\", \"#f59e0b\"],\n * });\n *\n * applyTheme(theme);\n * ```\n *\n * @param options - Brand colors plus optional radius / chart / focus-ring tuning.\n * @returns The light and dark token maps and the CSS text that carries them.\n */\nexport function createTheme(options: CreateThemeOptions = {}): GeneratedTheme {\n const {\n primary,\n gray,\n chart,\n radius,\n focusRingAlpha,\n selector = \":root\",\n darkSelector = '[data-tempest-theme=\"dark\"]',\n inverse,\n inverseSelector = INVERSE_SELECTOR,\n } = options;\n\n warnOnTranslucentFocusRing(focusRingAlpha);\n\n const light: Record<string, string> = {};\n const dark: Record<string, string> = {};\n\n // `anchor: false`: a neutral ramp has to keep its tuned lightness curve.\n // Anchoring it at the input compressed both halves and dropped\n // text-muted-on-surface-3 to ~4.2:1 — below AA — which the browser axe\n // sweep caught on the generated themes.\n const grayScales = gray\n ? {\n light: createColorScale(gray, \"light\", { anchor: false, neutral: true }),\n dark: createColorScale(gray, \"dark\", { anchor: false, neutral: true }),\n }\n : undefined;\n\n if (primary) {\n const lightScale = createColorScale(primary, \"light\");\n const darkScale = createColorScale(primary, \"dark\");\n writeScale(light, \"primary\", lightScale);\n writeScale(dark, \"primary\", darkScale);\n writePrimaryAliases(\n light,\n lightScale,\n \"light\",\n surfacesOf(\"light\", grayScales),\n focusRingAlpha,\n );\n writePrimaryAliases(\n dark,\n darkScale,\n \"dark\",\n surfacesOf(\"dark\", grayScales),\n focusRingAlpha,\n );\n }\n\n if (grayScales) {\n writeScale(light, \"gray\", grayScales.light);\n writeScale(dark, \"gray\", grayScales.dark);\n writeNeutralAliases(light, grayScales.light, \"light\");\n writeNeutralAliases(dark, grayScales.dark, \"dark\");\n }\n\n for (const status of THEME_STATUSES) {\n const value = options[status];\n if (!value) continue;\n writeStatus(light, status, createColorScale(value, \"light\"), \"light\");\n writeStatus(dark, status, createColorScale(value, \"dark\"), \"dark\");\n }\n\n if (chart?.length) {\n chart.forEach((color, index) => {\n light[`--tempest-chart-${index + 1}`] = color;\n });\n // Declares how many series colors this theme owns. Without it the reader\n // keeps walking into the SDK's built-in `--tempest-chart-7`/`-8`, so a\n // 6-color brand palette silently mixes with two leftover defaults — which\n // is exactly what a 7-series chart would show.\n light[\"--tempest-chart-count\"] = String(chart.length);\n }\n\n /*\n * Continuous scales follow the brand too.\n *\n * Without this a rebranded app gets its categorical slots recoloured and then a\n * heatmap still painted in the SDK's blue — the one chart on the page that\n * silently ignores the brand. Both scales derive from the first series hue (or\n * the primary), stepped by perceptual lightness for each mode separately.\n *\n * Skipped entirely when the theme names no colour to derive from, so the\n * built-in tokens survive instead of being overwritten with a guess.\n */\n const scaleSource = chart?.[0] ?? primary;\n if (scaleSource) {\n const coolHue = hueOf(scaleSource);\n // The opposite pole: the brand's own danger colour when it has one, since a\n // diverging scale's warm arm and \"bad\" should not disagree on screen.\n const warmHue = options.danger ? hueOf(options.danger) : (coolHue + 180) % 360;\n for (const [target, mode] of [\n [light, \"light\"],\n [dark, \"dark\"],\n ] as const) {\n buildRamp(coolHue, mode).forEach((stepColor, index) => {\n target[`--tempest-chart-sequential-${index + 1}`] = stepColor;\n });\n const diverging = buildDivergingRamp({\n coolHue,\n warmHue,\n mid: mode === \"light\" ? \"#e4e7ec\" : \"#262d3f\",\n mode,\n });\n [...diverging.cool, diverging.mid, ...diverging.warm].forEach((stepColor, index) => {\n target[`--tempest-chart-diverging-${index + 1}`] = stepColor;\n });\n }\n }\n\n if (radius) {\n const steps = typeof radius === \"string\" ? RADIUS_PRESETS[radius] : radius;\n for (const [step, value] of Object.entries(steps)) {\n light[`--tempest-radius-${step}`] = value;\n }\n }\n\n const inverseSurface =\n primary && inverse\n ? inverse({\n primary,\n statuses: {\n success: options.success,\n warning: options.warning,\n danger: options.danger,\n info: options.info,\n },\n chart,\n })\n : null;\n\n const css = [\n renderBlock(selector, light),\n renderBlock(darkSelector, dark),\n inverseSurface ? renderInverseSurface(inverseSelector, inverseSurface) : \"\",\n ]\n .filter(Boolean)\n .join(\"\\n\\n\");\n\n return inverseSurface\n ? { light, dark, inverse: inverseSurface.tokens, css }\n : { light, dark, css };\n}\n\n/**\n * Contrast ratio of `--tempest-primary-foreground` over the brand color.\n *\n * Exposed so an app (or a test) can assert its own brand clears WCAG AA (4.5) for\n * body text or AA-large (3.0) for button labels, instead of trusting the pick.\n *\n * @param options - The same input given to {@link createTheme}.\n * @returns The ratio, or `null` when no `primary` was provided.\n */\nexport function themeContrast(options: CreateThemeOptions): number | null {\n if (!options.primary) return null;\n const scale = createColorScale(options.primary, \"light\");\n return contrastRatio(scale[500], readableForeground(scale[500]));\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"create-theme.cjs","names":[],"sources":["../../src/theme/create-theme.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — one pass that emits the whole token set — surfaces,\n * text, borders, states, the chart ramps — for both modes, with every value checked\n * against the surface it will sit on. The checks are what make the length: each\n * token is derived and then verified against the same background table.\n */\n/**\n * Theme factory: turns a handful of brand colors into the full set of\n * `--tempest-*` token overrides, for both light and dark.\n *\n * Rebranding used to mean hand-writing ~167 custom properties (and getting the\n * dark ramp inversion right by hand). `createTheme({ primary: \"#7c3aed\" })`\n * emits the same thing, derived in OKLCH so the ramp stays perceptually even.\n */\nimport {\n contrastRatio,\n createColorScale,\n hexToRgbaString,\n readableForeground,\n type ColorScale,\n type ScaleStep,\n} from \"./color\";\nimport { buildDivergingRamp, buildRamp, hueOf } from \"./data-viz-ramps\";\nimport { isDevBuild } from \"../utils/dev-mode\";\nimport {\n INVERSE_SELECTOR,\n renderInverseSurface,\n type InverseSurfaceGenerator,\n} from \"./inverse-render\";\nimport { ON_SOLID_INK, THEME_STATUSES, writeStatus } from \"./status-tokens\";\n\n/** Radius presets, applied to the whole `--tempest-radius-*` family at once. */\nexport type ThemeRadius = \"none\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"full\";\n\nexport type { ThemeStatus } from \"./status-tokens\";\n\n/** Input for {@link createTheme}. Every field is optional — omitted families keep the built-in tokens. */\nexport interface CreateThemeOptions {\n /** Brand color, used as the `500` step of the primary scale. */\n primary?: string;\n /** Neutral color, used as the `500` step of the gray scale (surfaces, borders, text). */\n gray?: string;\n /** Success color (`--tempest-success*`). */\n success?: string;\n /** Warning color (`--tempest-warning*`). */\n warning?: string;\n /** Danger color (`--tempest-danger*`). */\n danger?: string;\n /** Info color (`--tempest-info*`). */\n info?: string;\n /**\n * Categorical series colors for `tempest-react-sdk/charts`, in cycle order.\n * Written to `--tempest-chart-1` … `--tempest-chart-N`, so charts follow the\n * theme instead of a hardcoded palette.\n */\n chart?: string[];\n /** Corner radius scale. A preset name, or explicit per-step values. */\n radius?: ThemeRadius | Partial<Record<\"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\", string>>;\n /**\n * Opacity of `--tempest-focus-ring-color`. Omit it — the default ring is opaque.\n *\n * A translucent ring has no contrast of its own; it has the contrast of\n * whatever it composites over. Passing a value below `1` reintroduces the\n * failure 0.61.0 fixed in the built-in tokens, and logs a warning in a\n * development build. Kept because a translucent ring over a background you\n * control is a legitimate choice.\n */\n focusRingAlpha?: number;\n /** Selector the light tokens are written under. Default `\":root\"`. */\n selector?: string;\n /** Selector the dark tokens are written under. Default `'[data-tempest-theme=\"dark\"]'`. */\n darkSelector?: string;\n /**\n * Also emit the inverse surface — the token set for a section painted in the\n * brand color — by passing its generator, `createInverseSurface`. Off by\n * default; ignored without `primary`.\n *\n * The generator is passed in rather than switched on with `true` so that\n * `createTheme` never imports it: an app that does not paint a section in\n * its brand does not ship the 1.68 kB brotli it costs.\n *\n * @example\n * ```ts\n * import { createInverseSurface, createTheme } from \"tempest-react-sdk\";\n *\n * createTheme({ primary: \"#03184b\", inverse: createInverseSurface });\n * ```\n */\n inverse?: InverseSurfaceGenerator;\n /** Selector the inverse tokens are written under. Default `'[data-tempest-tone=\"inverse\"]'`. */\n inverseSelector?: string;\n}\n\n/** A generated theme: token maps per color scheme, plus the CSS text that carries them. */\nexport interface GeneratedTheme {\n /** Light-scheme custom properties, without the leading `--`-less names (keys include `--`). */\n light: Record<string, string>;\n /** Dark-scheme custom properties. */\n dark: Record<string, string>;\n /**\n * Inverse-surface custom properties — literal colors, identical under the\n * light and the dark page scheme. Present only when the theme names a\n * `primary` and passes `inverse`.\n */\n inverse?: Record<string, string>;\n /** Both blocks rendered as CSS, ready for {@link applyTheme} or a stylesheet. */\n css: string;\n}\n\nconst RADIUS_PRESETS: Record<ThemeRadius, Record<string, string>> = {\n none: { xs: \"0\", sm: \"0\", md: \"0\", lg: \"0\", xl: \"0\", \"2xl\": \"0\" },\n sm: { xs: \"1px\", sm: \"2px\", md: \"4px\", lg: \"6px\", xl: \"8px\", \"2xl\": \"12px\" },\n md: { xs: \"2px\", sm: \"4px\", md: \"8px\", lg: \"12px\", xl: \"16px\", \"2xl\": \"24px\" },\n lg: { xs: \"4px\", sm: \"6px\", md: \"12px\", lg: \"16px\", xl: \"22px\", \"2xl\": \"32px\" },\n xl: { xs: \"6px\", sm: \"10px\", md: \"16px\", lg: \"24px\", xl: \"32px\", \"2xl\": \"44px\" },\n full: { xs: \"4px\", sm: \"8px\", md: \"9999px\", lg: \"9999px\", xl: \"9999px\", \"2xl\": \"9999px\" },\n};\n\nconst SCALE_STEPS: ScaleStep[] = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900];\n\nfunction writeScale(tokens: Record<string, string>, name: string, scale: ColorScale): void {\n for (const step of SCALE_STEPS) {\n tokens[`--tempest-${name}-${step}`] = scale[step];\n }\n}\n\n/** Minimum contrast for body text, WCAG 2.x AA. */\nconst AA_TEXT_CONTRAST = 4.5;\n\n/**\n * Choose the ramp step for text sitting on the soft tint.\n *\n * A fixed step does not survive an arbitrary brand color: the built-in blue only\n * reaches 4.37:1 at `500` over its own `50` tint, and a generated emerald lands\n * at 4.41:1 at `600` — both fail AA for body text by a hair. So the step is\n * picked by measuring, walking away from the tint until it clears AA, and\n * falling back to the most extreme candidate when even that cannot (a very light\n * or very desaturated brand).\n *\n * @param scale - The generated ramp.\n * @param softStep - Step used as `--tempest-primary-soft`.\n * @param candidates - Steps to try, in order of preference.\n * @returns The first candidate clearing {@link AA_TEXT_CONTRAST}, else the last.\n */\nfunction pickOnSoftStep(\n scale: ColorScale,\n softStep: ScaleStep,\n candidates: ScaleStep[],\n): ScaleStep {\n for (const step of candidates) {\n if (contrastRatio(scale[softStep], scale[step]) >= AA_TEXT_CONTRAST) return step;\n }\n return candidates[candidates.length - 1];\n}\n\n/** Minimum contrast for a non-text indicator, WCAG 2.2 SC 1.4.11. */\nconst INDICATOR_CONTRAST = 3;\n\n/**\n * The four surfaces a focus ring can land on, as the SDK's own `colors.css`\n * paints them.\n *\n * Ported from `src/styles/colors.css` (`--tempest-bg`, `--tempest-surface`,\n * `--tempest-surface-2`, `--tempest-surface-3`, resolved through the gray ramp),\n * and pinned by a test that reads that file — a theme that only names `primary`\n * keeps these, so the ring has to be measured against them and not against a\n * guess. The light values resolve through `--tempest-gray-50/100/200`; the dark\n * ones are literals in the dark block.\n */\nconst SDK_SURFACES: Record<\"light\" | \"dark\", readonly string[]> = {\n light: [\"#ffffff\", \"#f8f9fb\", \"#f1f3f6\", \"#e4e7ec\"],\n dark: [\"#0b0d12\", \"#14171f\", \"#1d2230\", \"#262d3f\"],\n};\n\n/**\n * Ramp steps to try for the focus ring, in order.\n *\n * `500` first, because the ring should read as the brand when it can, then up\n * the ramp — which moves away from the surfaces in **both** schemes, because\n * `createColorScale` inverts the dark ramp: there `50` is the darkest step (it\n * is what `--tempest-bg` is made of) and `900` the lightest. Walking down in\n * dark mode looks like the right inversion and is the wrong direction; measured,\n * it left a near-black brand's ring at 1.04:1 over its own background.\n */\nconst FOCUS_RING_CANDIDATES: readonly ScaleStep[] = [500, 600, 700, 800, 900];\n\n/**\n * Choose the ramp step for the focus ring, by measuring it against the surfaces\n * it will actually be drawn on.\n *\n * A fixed step does not survive an arbitrary brand, and the measurement says so\n * loudly: of twelve brands checked against the four surfaces in both schemes —\n * 96 pairings — `500` opaque clears 3:1 in 58 of them, and `500` at the old\n * default alpha of 0.35 in 3. Picking the step by measurement clears all 96. The\n * brand from the report that opened this\n * (`#8100D7`) passes in light at 7.20:1 and fails in **dark** at 2.70:1 down to\n * 1.91:1 on `surface-3`; a yellow fails in light at 1.43:1. So the step is\n * picked the way {@link pickOnSoftStep} picks text on a tint: walk away from the\n * surfaces until the ring clears the floor against *all* of them.\n *\n * The fallback is the candidate with the best worst case rather than the last\n * one, because \"the most extreme step\" is not always the most contrasting one\n * once a brand is near-neutral.\n *\n * @param scale - The generated ramp.\n * @param surfaces - The four surfaces this theme will paint, opaque hex.\n * @returns The first candidate clearing {@link INDICATOR_CONTRAST} on every\n * surface, else the candidate whose weakest pairing is strongest.\n */\nfunction pickFocusRingStep(scale: ColorScale, surfaces: readonly string[]): ScaleStep {\n const worstCase = (step: ScaleStep): number =>\n Math.min(...surfaces.map((surface) => contrastRatio(scale[step], surface)));\n let best: ScaleStep = FOCUS_RING_CANDIDATES[0] as ScaleStep;\n let bestRatio = -1;\n for (const step of FOCUS_RING_CANDIDATES) {\n const measured = worstCase(step);\n if (measured >= INDICATOR_CONTRAST) return step;\n if (measured > bestRatio) {\n best = step;\n bestRatio = measured;\n }\n }\n return best;\n}\n\n/**\n * The surfaces this theme will actually paint, per scheme.\n *\n * A theme that names `gray` repaints all four, so measuring the ring against the\n * SDK's would be measuring the wrong backdrop — and a theme that does not keeps\n * the SDK's, which is the common case. The order and the ramp steps mirror\n * {@link writeNeutralAliases}, which is what emits them.\n *\n * @param scheme - Which scheme is being emitted.\n * @param grayScales - The generated neutral ramps, when the theme names one.\n * @returns Four opaque hex colors: bg, surface, surface-2, surface-3.\n */\nfunction surfacesOf(\n scheme: \"light\" | \"dark\",\n grayScales: { light: ColorScale; dark: ColorScale } | undefined,\n): readonly string[] {\n if (!grayScales) return SDK_SURFACES[scheme];\n const scale = grayScales[scheme];\n return scheme === \"light\"\n ? [\"#ffffff\", scale[50], scale[100], scale[200]]\n : [scale[50], scale[100], scale[200], scale[300]];\n}\n\n/**\n * Warn, in a development build, that an alpha was asked for the focus ring.\n *\n * A translucent ring has no contrast of its own — it has the contrast of\n * whatever it composites over — and that is the failure this option used to\n * ship by default: measured at alpha `0.35`, twelve brands across four surfaces\n * and both schemes produced 96 pairings, of which 3 cleared the 3:1 of WCAG 2.2\n * SC 1.4.11. It stays available, because a translucent ring over a\n * known background is a legitimate choice, but it is a choice a theme now has to\n * make on purpose.\n *\n * The warning is the difficult part to notice otherwise: a failing focus ring is\n * visible, it just does not separate.\n *\n * @param focusRingAlpha - The opacity the caller asked for, if any.\n */\nfunction warnOnTranslucentFocusRing(focusRingAlpha: number | undefined): void {\n if (focusRingAlpha === undefined || focusRingAlpha >= 1) return;\n if (!isDevBuild()) return;\n console.warn(\n `[tempest-react-sdk] createTheme({ focusRingAlpha: ${focusRingAlpha} }) makes the focus ring ` +\n \"translucent, so its contrast becomes whatever it composites over — measured below the 3:1 \" +\n \"of WCAG 2.2 SC 1.4.11 on every surface the SDK paints. Drop the option to get the opaque \" +\n \"ring, which is derived from your brand ramp and measured against those surfaces.\",\n );\n}\n\n/**\n * Emit the primary aliases for one scheme.\n *\n * The dark scheme walks the ramp the other way (hover is *lighter*, the soft\n * tint is a dark shade, and readable text on that tint is a light shade) — the\n * same inversion the built-in `colors.css` dark block does by hand.\n *\n * The selected-state indicator is derived from the same measurement as the focus\n * ring and emitted as its own token. It never takes `focusRingAlpha`: a\n * translucent ring over a known background is a legitimate choice a theme can\n * make, while a translucent *state* indicator has the contrast of whatever it\n * composites over, which is the defect this token exists to end.\n *\n * @param tokens - The token map for this scheme, written in place.\n * @param scale - The generated primary ramp.\n * @param scheme - Which scheme is being emitted.\n * @param surfaces - The surfaces this theme paints, for the focus-ring measurement.\n * @param focusRingAlpha - Opacity asked for the ring, or `undefined` for opaque.\n */\nfunction writePrimaryAliases(\n tokens: Record<string, string>,\n scale: ColorScale,\n scheme: \"light\" | \"dark\",\n surfaces: readonly string[],\n focusRingAlpha: number | undefined,\n): void {\n tokens[\"--tempest-primary\"] = \"var(--tempest-primary-500)\";\n if (scheme === \"light\") {\n tokens[\"--tempest-primary-hover\"] = \"var(--tempest-primary-600)\";\n tokens[\"--tempest-primary-active\"] = \"var(--tempest-primary-700)\";\n tokens[\"--tempest-primary-soft\"] = \"var(--tempest-primary-50)\";\n tokens[\"--tempest-primary-soft-hover\"] = \"var(--tempest-primary-100)\";\n tokens[\"--tempest-primary-on-soft\"] =\n `var(--tempest-primary-${pickOnSoftStep(scale, 50, [600, 700, 800, 900])})`;\n } else {\n tokens[\"--tempest-primary-hover\"] = \"var(--tempest-primary-400)\";\n tokens[\"--tempest-primary-active\"] = \"var(--tempest-primary-300)\";\n tokens[\"--tempest-primary-soft\"] = \"var(--tempest-primary-100)\";\n tokens[\"--tempest-primary-soft-hover\"] = \"var(--tempest-primary-200)\";\n tokens[\"--tempest-primary-on-soft\"] =\n `var(--tempest-primary-${pickOnSoftStep(scale, 100, [700, 800, 900])})`;\n }\n\n const foreground = readableForeground(scale[500]);\n tokens[\"--tempest-primary-foreground\"] = foreground;\n tokens[\"--tempest-text-on-primary\"] = foreground;\n const ring = scale[pickFocusRingStep(scale, surfaces)];\n tokens[\"--tempest-focus-ring-color\"] =\n focusRingAlpha === undefined || focusRingAlpha >= 1\n ? ring\n : hexToRgbaString(ring, focusRingAlpha);\n tokens[\"--tempest-selected-indicator\"] = ring;\n}\n\n/**\n * Emit the neutral surface/border/text aliases from a generated gray scale.\n *\n * `--tempest-bg` is pushed past the ramp on purpose: pure white in light mode and\n * a shade darker than `gray-50` in dark mode, so a raised surface still reads as\n * raised against the page.\n */\nfunction writeNeutralAliases(\n tokens: Record<string, string>,\n scale: ColorScale,\n scheme: \"light\" | \"dark\",\n): void {\n if (scheme === \"light\") {\n tokens[\"--tempest-bg\"] = \"#ffffff\";\n tokens[\"--tempest-surface\"] = \"var(--tempest-gray-50)\";\n tokens[\"--tempest-surface-2\"] = \"var(--tempest-gray-100)\";\n tokens[\"--tempest-surface-3\"] = \"var(--tempest-gray-200)\";\n tokens[\"--tempest-border\"] = \"var(--tempest-gray-200)\";\n tokens[\"--tempest-border-strong\"] = \"var(--tempest-gray-300)\";\n tokens[\"--tempest-text\"] = \"var(--tempest-gray-900)\";\n tokens[\"--tempest-text-muted\"] = \"var(--tempest-gray-600)\";\n tokens[\"--tempest-text-subtle\"] = \"var(--tempest-gray-500)\";\n } else {\n tokens[\"--tempest-bg\"] = scale[50];\n tokens[\"--tempest-surface\"] = \"var(--tempest-gray-100)\";\n tokens[\"--tempest-surface-2\"] = \"var(--tempest-gray-200)\";\n tokens[\"--tempest-surface-3\"] = \"var(--tempest-gray-300)\";\n tokens[\"--tempest-border\"] = \"var(--tempest-gray-300)\";\n tokens[\"--tempest-border-strong\"] = \"var(--tempest-gray-400)\";\n tokens[\"--tempest-text\"] = \"var(--tempest-gray-900)\";\n tokens[\"--tempest-text-muted\"] = \"var(--tempest-gray-700)\";\n tokens[\"--tempest-text-subtle\"] = \"var(--tempest-gray-600)\";\n }\n tokens[\"--tempest-neutral-on-solid\"] = readableForeground(scale[700], \"#ffffff\", ON_SOLID_INK);\n}\n\nfunction renderBlock(selector: string, tokens: Record<string, string>): string {\n const entries = Object.entries(tokens);\n if (entries.length === 0) return \"\";\n const body = entries.map(([name, value]) => ` ${name}: ${value};`).join(\"\\n\");\n return `${selector} {\\n${body}\\n}`;\n}\n\n/**\n * Generate `--tempest-*` overrides from a small brand description.\n *\n * Only the families you pass are generated; everything else falls through to the\n * SDK's own tokens, so a theme stays a patch and not a fork of `colors.css`.\n *\n * @example\n * ```ts\n * import { applyTheme, createTheme } from \"tempest-react-sdk\";\n *\n * const theme = createTheme({\n * primary: \"#7c3aed\",\n * radius: \"lg\",\n * chart: [\"#7c3aed\", \"#0ea5e9\", \"#22c55e\", \"#f59e0b\"],\n * });\n *\n * applyTheme(theme);\n * ```\n *\n * @param options - Brand colors plus optional radius / chart / focus-ring tuning.\n * @returns The light and dark token maps and the CSS text that carries them.\n */\nexport function createTheme(options: CreateThemeOptions = {}): GeneratedTheme {\n const {\n primary,\n gray,\n chart,\n radius,\n focusRingAlpha,\n selector = \":root\",\n darkSelector = '[data-tempest-theme=\"dark\"]',\n inverse,\n inverseSelector = INVERSE_SELECTOR,\n } = options;\n\n warnOnTranslucentFocusRing(focusRingAlpha);\n\n const light: Record<string, string> = {};\n const dark: Record<string, string> = {};\n\n // `anchor: false`: a neutral ramp has to keep its tuned lightness curve.\n // Anchoring it at the input compressed both halves and dropped\n // text-muted-on-surface-3 to ~4.2:1 — below AA — which the browser axe\n // sweep caught on the generated themes.\n const grayScales = gray\n ? {\n light: createColorScale(gray, \"light\", { anchor: false, neutral: true }),\n dark: createColorScale(gray, \"dark\", { anchor: false, neutral: true }),\n }\n : undefined;\n\n if (primary) {\n const lightScale = createColorScale(primary, \"light\");\n const darkScale = createColorScale(primary, \"dark\");\n writeScale(light, \"primary\", lightScale);\n writeScale(dark, \"primary\", darkScale);\n writePrimaryAliases(\n light,\n lightScale,\n \"light\",\n surfacesOf(\"light\", grayScales),\n focusRingAlpha,\n );\n writePrimaryAliases(\n dark,\n darkScale,\n \"dark\",\n surfacesOf(\"dark\", grayScales),\n focusRingAlpha,\n );\n }\n\n if (grayScales) {\n writeScale(light, \"gray\", grayScales.light);\n writeScale(dark, \"gray\", grayScales.dark);\n writeNeutralAliases(light, grayScales.light, \"light\");\n writeNeutralAliases(dark, grayScales.dark, \"dark\");\n }\n\n for (const status of THEME_STATUSES) {\n const value = options[status];\n if (!value) continue;\n writeStatus(light, status, createColorScale(value, \"light\"), \"light\");\n writeStatus(dark, status, createColorScale(value, \"dark\"), \"dark\");\n }\n\n if (chart?.length) {\n chart.forEach((color, index) => {\n light[`--tempest-chart-${index + 1}`] = color;\n });\n // Declares how many series colors this theme owns. Without it the reader\n // keeps walking into the SDK's built-in `--tempest-chart-7`/`-8`, so a\n // 6-color brand palette silently mixes with two leftover defaults — which\n // is exactly what a 7-series chart would show.\n light[\"--tempest-chart-count\"] = String(chart.length);\n }\n\n /*\n * Continuous scales follow the brand too.\n *\n * Without this a rebranded app gets its categorical slots recoloured and then a\n * heatmap still painted in the SDK's blue — the one chart on the page that\n * silently ignores the brand. Both scales derive from the first series hue (or\n * the primary), stepped by perceptual lightness for each mode separately.\n *\n * Skipped entirely when the theme names no colour to derive from, so the\n * built-in tokens survive instead of being overwritten with a guess.\n */\n const scaleSource = chart?.[0] ?? primary;\n if (scaleSource) {\n const coolHue = hueOf(scaleSource);\n // The opposite pole: the brand's own danger colour when it has one, since a\n // diverging scale's warm arm and \"bad\" should not disagree on screen.\n const warmHue = options.danger ? hueOf(options.danger) : (coolHue + 180) % 360;\n for (const [target, mode] of [\n [light, \"light\"],\n [dark, \"dark\"],\n ] as const) {\n buildRamp(coolHue, mode).forEach((stepColor, index) => {\n target[`--tempest-chart-sequential-${index + 1}`] = stepColor;\n });\n const diverging = buildDivergingRamp({\n coolHue,\n warmHue,\n mid: mode === \"light\" ? \"#e4e7ec\" : \"#262d3f\",\n mode,\n });\n [...diverging.cool, diverging.mid, ...diverging.warm].forEach((stepColor, index) => {\n target[`--tempest-chart-diverging-${index + 1}`] = stepColor;\n });\n }\n }\n\n if (radius) {\n const steps = typeof radius === \"string\" ? RADIUS_PRESETS[radius] : radius;\n for (const [step, value] of Object.entries(steps)) {\n light[`--tempest-radius-${step}`] = value;\n }\n }\n\n const inverseSurface =\n primary && inverse\n ? inverse({\n primary,\n statuses: {\n success: options.success,\n warning: options.warning,\n danger: options.danger,\n info: options.info,\n },\n chart,\n selector: inverseSelector,\n page: { selector, darkSelector },\n })\n : null;\n\n const css = [\n renderBlock(selector, light),\n renderBlock(darkSelector, dark),\n inverseSurface ? renderInverseSurface(inverseSelector, inverseSurface) : \"\",\n ]\n .filter(Boolean)\n .join(\"\\n\\n\");\n\n return inverseSurface\n ? { light, dark, inverse: inverseSurface.tokens, css }\n : { light, dark, css };\n}\n\n/**\n * Contrast ratio of `--tempest-primary-foreground` over the brand color.\n *\n * Exposed so an app (or a test) can assert its own brand clears WCAG AA (4.5) for\n * body text or AA-large (3.0) for button labels, instead of trusting the pick.\n *\n * @param options - The same input given to {@link createTheme}.\n * @returns The ratio, or `null` when no `primary` was provided.\n */\nexport function themeContrast(options: CreateThemeOptions): number | null {\n if (!options.primary) return null;\n const scale = createColorScale(options.primary, \"light\");\n return contrastRatio(scale[500], readableForeground(scale[500]));\n}\n"],"mappings":"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"}
@@ -202,7 +202,12 @@ function D(e = {}) {
202
202
  danger: e.danger,
203
203
  info: e.info
204
204
  },
205
- chart: i
205
+ chart: i,
206
+ selector: y,
207
+ page: {
208
+ selector: g,
209
+ darkSelector: _
210
+ }
206
211
  }) : null, A = [
207
212
  E(g, b),
208
213
  E(_, x),
@@ -1 +1 @@
1
- {"version":3,"file":"create-theme.js","names":[],"sources":["../../src/theme/create-theme.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — one pass that emits the whole token set — surfaces,\n * text, borders, states, the chart ramps — for both modes, with every value checked\n * against the surface it will sit on. The checks are what make the length: each\n * token is derived and then verified against the same background table.\n */\n/**\n * Theme factory: turns a handful of brand colors into the full set of\n * `--tempest-*` token overrides, for both light and dark.\n *\n * Rebranding used to mean hand-writing ~167 custom properties (and getting the\n * dark ramp inversion right by hand). `createTheme({ primary: \"#7c3aed\" })`\n * emits the same thing, derived in OKLCH so the ramp stays perceptually even.\n */\nimport {\n contrastRatio,\n createColorScale,\n hexToRgbaString,\n readableForeground,\n type ColorScale,\n type ScaleStep,\n} from \"./color\";\nimport { buildDivergingRamp, buildRamp, hueOf } from \"./data-viz-ramps\";\nimport { isDevBuild } from \"../utils/dev-mode\";\nimport {\n INVERSE_SELECTOR,\n renderInverseSurface,\n type InverseSurfaceGenerator,\n} from \"./inverse-render\";\nimport { ON_SOLID_INK, THEME_STATUSES, writeStatus } from \"./status-tokens\";\n\n/** Radius presets, applied to the whole `--tempest-radius-*` family at once. */\nexport type ThemeRadius = \"none\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"full\";\n\nexport type { ThemeStatus } from \"./status-tokens\";\n\n/** Input for {@link createTheme}. Every field is optional — omitted families keep the built-in tokens. */\nexport interface CreateThemeOptions {\n /** Brand color, used as the `500` step of the primary scale. */\n primary?: string;\n /** Neutral color, used as the `500` step of the gray scale (surfaces, borders, text). */\n gray?: string;\n /** Success color (`--tempest-success*`). */\n success?: string;\n /** Warning color (`--tempest-warning*`). */\n warning?: string;\n /** Danger color (`--tempest-danger*`). */\n danger?: string;\n /** Info color (`--tempest-info*`). */\n info?: string;\n /**\n * Categorical series colors for `tempest-react-sdk/charts`, in cycle order.\n * Written to `--tempest-chart-1` … `--tempest-chart-N`, so charts follow the\n * theme instead of a hardcoded palette.\n */\n chart?: string[];\n /** Corner radius scale. A preset name, or explicit per-step values. */\n radius?: ThemeRadius | Partial<Record<\"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\", string>>;\n /**\n * Opacity of `--tempest-focus-ring-color`. Omit it — the default ring is opaque.\n *\n * A translucent ring has no contrast of its own; it has the contrast of\n * whatever it composites over. Passing a value below `1` reintroduces the\n * failure 0.61.0 fixed in the built-in tokens, and logs a warning in a\n * development build. Kept because a translucent ring over a background you\n * control is a legitimate choice.\n */\n focusRingAlpha?: number;\n /** Selector the light tokens are written under. Default `\":root\"`. */\n selector?: string;\n /** Selector the dark tokens are written under. Default `'[data-tempest-theme=\"dark\"]'`. */\n darkSelector?: string;\n /**\n * Also emit the inverse surface — the token set for a section painted in the\n * brand color — by passing its generator, `createInverseSurface`. Off by\n * default; ignored without `primary`.\n *\n * The generator is passed in rather than switched on with `true` so that\n * `createTheme` never imports it: an app that does not paint a section in\n * its brand does not ship the 1.68 kB brotli it costs.\n *\n * @example\n * ```ts\n * import { createInverseSurface, createTheme } from \"tempest-react-sdk\";\n *\n * createTheme({ primary: \"#03184b\", inverse: createInverseSurface });\n * ```\n */\n inverse?: InverseSurfaceGenerator;\n /** Selector the inverse tokens are written under. Default `'[data-tempest-tone=\"inverse\"]'`. */\n inverseSelector?: string;\n}\n\n/** A generated theme: token maps per color scheme, plus the CSS text that carries them. */\nexport interface GeneratedTheme {\n /** Light-scheme custom properties, without the leading `--`-less names (keys include `--`). */\n light: Record<string, string>;\n /** Dark-scheme custom properties. */\n dark: Record<string, string>;\n /**\n * Inverse-surface custom properties — literal colors, identical under the\n * light and the dark page scheme. Present only when the theme names a\n * `primary` and passes `inverse`.\n */\n inverse?: Record<string, string>;\n /** Both blocks rendered as CSS, ready for {@link applyTheme} or a stylesheet. */\n css: string;\n}\n\nconst RADIUS_PRESETS: Record<ThemeRadius, Record<string, string>> = {\n none: { xs: \"0\", sm: \"0\", md: \"0\", lg: \"0\", xl: \"0\", \"2xl\": \"0\" },\n sm: { xs: \"1px\", sm: \"2px\", md: \"4px\", lg: \"6px\", xl: \"8px\", \"2xl\": \"12px\" },\n md: { xs: \"2px\", sm: \"4px\", md: \"8px\", lg: \"12px\", xl: \"16px\", \"2xl\": \"24px\" },\n lg: { xs: \"4px\", sm: \"6px\", md: \"12px\", lg: \"16px\", xl: \"22px\", \"2xl\": \"32px\" },\n xl: { xs: \"6px\", sm: \"10px\", md: \"16px\", lg: \"24px\", xl: \"32px\", \"2xl\": \"44px\" },\n full: { xs: \"4px\", sm: \"8px\", md: \"9999px\", lg: \"9999px\", xl: \"9999px\", \"2xl\": \"9999px\" },\n};\n\nconst SCALE_STEPS: ScaleStep[] = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900];\n\nfunction writeScale(tokens: Record<string, string>, name: string, scale: ColorScale): void {\n for (const step of SCALE_STEPS) {\n tokens[`--tempest-${name}-${step}`] = scale[step];\n }\n}\n\n/** Minimum contrast for body text, WCAG 2.x AA. */\nconst AA_TEXT_CONTRAST = 4.5;\n\n/**\n * Choose the ramp step for text sitting on the soft tint.\n *\n * A fixed step does not survive an arbitrary brand color: the built-in blue only\n * reaches 4.37:1 at `500` over its own `50` tint, and a generated emerald lands\n * at 4.41:1 at `600` — both fail AA for body text by a hair. So the step is\n * picked by measuring, walking away from the tint until it clears AA, and\n * falling back to the most extreme candidate when even that cannot (a very light\n * or very desaturated brand).\n *\n * @param scale - The generated ramp.\n * @param softStep - Step used as `--tempest-primary-soft`.\n * @param candidates - Steps to try, in order of preference.\n * @returns The first candidate clearing {@link AA_TEXT_CONTRAST}, else the last.\n */\nfunction pickOnSoftStep(\n scale: ColorScale,\n softStep: ScaleStep,\n candidates: ScaleStep[],\n): ScaleStep {\n for (const step of candidates) {\n if (contrastRatio(scale[softStep], scale[step]) >= AA_TEXT_CONTRAST) return step;\n }\n return candidates[candidates.length - 1];\n}\n\n/** Minimum contrast for a non-text indicator, WCAG 2.2 SC 1.4.11. */\nconst INDICATOR_CONTRAST = 3;\n\n/**\n * The four surfaces a focus ring can land on, as the SDK's own `colors.css`\n * paints them.\n *\n * Ported from `src/styles/colors.css` (`--tempest-bg`, `--tempest-surface`,\n * `--tempest-surface-2`, `--tempest-surface-3`, resolved through the gray ramp),\n * and pinned by a test that reads that file — a theme that only names `primary`\n * keeps these, so the ring has to be measured against them and not against a\n * guess. The light values resolve through `--tempest-gray-50/100/200`; the dark\n * ones are literals in the dark block.\n */\nconst SDK_SURFACES: Record<\"light\" | \"dark\", readonly string[]> = {\n light: [\"#ffffff\", \"#f8f9fb\", \"#f1f3f6\", \"#e4e7ec\"],\n dark: [\"#0b0d12\", \"#14171f\", \"#1d2230\", \"#262d3f\"],\n};\n\n/**\n * Ramp steps to try for the focus ring, in order.\n *\n * `500` first, because the ring should read as the brand when it can, then up\n * the ramp — which moves away from the surfaces in **both** schemes, because\n * `createColorScale` inverts the dark ramp: there `50` is the darkest step (it\n * is what `--tempest-bg` is made of) and `900` the lightest. Walking down in\n * dark mode looks like the right inversion and is the wrong direction; measured,\n * it left a near-black brand's ring at 1.04:1 over its own background.\n */\nconst FOCUS_RING_CANDIDATES: readonly ScaleStep[] = [500, 600, 700, 800, 900];\n\n/**\n * Choose the ramp step for the focus ring, by measuring it against the surfaces\n * it will actually be drawn on.\n *\n * A fixed step does not survive an arbitrary brand, and the measurement says so\n * loudly: of twelve brands checked against the four surfaces in both schemes —\n * 96 pairings — `500` opaque clears 3:1 in 58 of them, and `500` at the old\n * default alpha of 0.35 in 3. Picking the step by measurement clears all 96. The\n * brand from the report that opened this\n * (`#8100D7`) passes in light at 7.20:1 and fails in **dark** at 2.70:1 down to\n * 1.91:1 on `surface-3`; a yellow fails in light at 1.43:1. So the step is\n * picked the way {@link pickOnSoftStep} picks text on a tint: walk away from the\n * surfaces until the ring clears the floor against *all* of them.\n *\n * The fallback is the candidate with the best worst case rather than the last\n * one, because \"the most extreme step\" is not always the most contrasting one\n * once a brand is near-neutral.\n *\n * @param scale - The generated ramp.\n * @param surfaces - The four surfaces this theme will paint, opaque hex.\n * @returns The first candidate clearing {@link INDICATOR_CONTRAST} on every\n * surface, else the candidate whose weakest pairing is strongest.\n */\nfunction pickFocusRingStep(scale: ColorScale, surfaces: readonly string[]): ScaleStep {\n const worstCase = (step: ScaleStep): number =>\n Math.min(...surfaces.map((surface) => contrastRatio(scale[step], surface)));\n let best: ScaleStep = FOCUS_RING_CANDIDATES[0] as ScaleStep;\n let bestRatio = -1;\n for (const step of FOCUS_RING_CANDIDATES) {\n const measured = worstCase(step);\n if (measured >= INDICATOR_CONTRAST) return step;\n if (measured > bestRatio) {\n best = step;\n bestRatio = measured;\n }\n }\n return best;\n}\n\n/**\n * The surfaces this theme will actually paint, per scheme.\n *\n * A theme that names `gray` repaints all four, so measuring the ring against the\n * SDK's would be measuring the wrong backdrop — and a theme that does not keeps\n * the SDK's, which is the common case. The order and the ramp steps mirror\n * {@link writeNeutralAliases}, which is what emits them.\n *\n * @param scheme - Which scheme is being emitted.\n * @param grayScales - The generated neutral ramps, when the theme names one.\n * @returns Four opaque hex colors: bg, surface, surface-2, surface-3.\n */\nfunction surfacesOf(\n scheme: \"light\" | \"dark\",\n grayScales: { light: ColorScale; dark: ColorScale } | undefined,\n): readonly string[] {\n if (!grayScales) return SDK_SURFACES[scheme];\n const scale = grayScales[scheme];\n return scheme === \"light\"\n ? [\"#ffffff\", scale[50], scale[100], scale[200]]\n : [scale[50], scale[100], scale[200], scale[300]];\n}\n\n/**\n * Warn, in a development build, that an alpha was asked for the focus ring.\n *\n * A translucent ring has no contrast of its own — it has the contrast of\n * whatever it composites over — and that is the failure this option used to\n * ship by default: measured at alpha `0.35`, twelve brands across four surfaces\n * and both schemes produced 96 pairings, of which 3 cleared the 3:1 of WCAG 2.2\n * SC 1.4.11. It stays available, because a translucent ring over a\n * known background is a legitimate choice, but it is a choice a theme now has to\n * make on purpose.\n *\n * The warning is the difficult part to notice otherwise: a failing focus ring is\n * visible, it just does not separate.\n *\n * @param focusRingAlpha - The opacity the caller asked for, if any.\n */\nfunction warnOnTranslucentFocusRing(focusRingAlpha: number | undefined): void {\n if (focusRingAlpha === undefined || focusRingAlpha >= 1) return;\n if (!isDevBuild()) return;\n console.warn(\n `[tempest-react-sdk] createTheme({ focusRingAlpha: ${focusRingAlpha} }) makes the focus ring ` +\n \"translucent, so its contrast becomes whatever it composites over — measured below the 3:1 \" +\n \"of WCAG 2.2 SC 1.4.11 on every surface the SDK paints. Drop the option to get the opaque \" +\n \"ring, which is derived from your brand ramp and measured against those surfaces.\",\n );\n}\n\n/**\n * Emit the primary aliases for one scheme.\n *\n * The dark scheme walks the ramp the other way (hover is *lighter*, the soft\n * tint is a dark shade, and readable text on that tint is a light shade) — the\n * same inversion the built-in `colors.css` dark block does by hand.\n *\n * The selected-state indicator is derived from the same measurement as the focus\n * ring and emitted as its own token. It never takes `focusRingAlpha`: a\n * translucent ring over a known background is a legitimate choice a theme can\n * make, while a translucent *state* indicator has the contrast of whatever it\n * composites over, which is the defect this token exists to end.\n *\n * @param tokens - The token map for this scheme, written in place.\n * @param scale - The generated primary ramp.\n * @param scheme - Which scheme is being emitted.\n * @param surfaces - The surfaces this theme paints, for the focus-ring measurement.\n * @param focusRingAlpha - Opacity asked for the ring, or `undefined` for opaque.\n */\nfunction writePrimaryAliases(\n tokens: Record<string, string>,\n scale: ColorScale,\n scheme: \"light\" | \"dark\",\n surfaces: readonly string[],\n focusRingAlpha: number | undefined,\n): void {\n tokens[\"--tempest-primary\"] = \"var(--tempest-primary-500)\";\n if (scheme === \"light\") {\n tokens[\"--tempest-primary-hover\"] = \"var(--tempest-primary-600)\";\n tokens[\"--tempest-primary-active\"] = \"var(--tempest-primary-700)\";\n tokens[\"--tempest-primary-soft\"] = \"var(--tempest-primary-50)\";\n tokens[\"--tempest-primary-soft-hover\"] = \"var(--tempest-primary-100)\";\n tokens[\"--tempest-primary-on-soft\"] =\n `var(--tempest-primary-${pickOnSoftStep(scale, 50, [600, 700, 800, 900])})`;\n } else {\n tokens[\"--tempest-primary-hover\"] = \"var(--tempest-primary-400)\";\n tokens[\"--tempest-primary-active\"] = \"var(--tempest-primary-300)\";\n tokens[\"--tempest-primary-soft\"] = \"var(--tempest-primary-100)\";\n tokens[\"--tempest-primary-soft-hover\"] = \"var(--tempest-primary-200)\";\n tokens[\"--tempest-primary-on-soft\"] =\n `var(--tempest-primary-${pickOnSoftStep(scale, 100, [700, 800, 900])})`;\n }\n\n const foreground = readableForeground(scale[500]);\n tokens[\"--tempest-primary-foreground\"] = foreground;\n tokens[\"--tempest-text-on-primary\"] = foreground;\n const ring = scale[pickFocusRingStep(scale, surfaces)];\n tokens[\"--tempest-focus-ring-color\"] =\n focusRingAlpha === undefined || focusRingAlpha >= 1\n ? ring\n : hexToRgbaString(ring, focusRingAlpha);\n tokens[\"--tempest-selected-indicator\"] = ring;\n}\n\n/**\n * Emit the neutral surface/border/text aliases from a generated gray scale.\n *\n * `--tempest-bg` is pushed past the ramp on purpose: pure white in light mode and\n * a shade darker than `gray-50` in dark mode, so a raised surface still reads as\n * raised against the page.\n */\nfunction writeNeutralAliases(\n tokens: Record<string, string>,\n scale: ColorScale,\n scheme: \"light\" | \"dark\",\n): void {\n if (scheme === \"light\") {\n tokens[\"--tempest-bg\"] = \"#ffffff\";\n tokens[\"--tempest-surface\"] = \"var(--tempest-gray-50)\";\n tokens[\"--tempest-surface-2\"] = \"var(--tempest-gray-100)\";\n tokens[\"--tempest-surface-3\"] = \"var(--tempest-gray-200)\";\n tokens[\"--tempest-border\"] = \"var(--tempest-gray-200)\";\n tokens[\"--tempest-border-strong\"] = \"var(--tempest-gray-300)\";\n tokens[\"--tempest-text\"] = \"var(--tempest-gray-900)\";\n tokens[\"--tempest-text-muted\"] = \"var(--tempest-gray-600)\";\n tokens[\"--tempest-text-subtle\"] = \"var(--tempest-gray-500)\";\n } else {\n tokens[\"--tempest-bg\"] = scale[50];\n tokens[\"--tempest-surface\"] = \"var(--tempest-gray-100)\";\n tokens[\"--tempest-surface-2\"] = \"var(--tempest-gray-200)\";\n tokens[\"--tempest-surface-3\"] = \"var(--tempest-gray-300)\";\n tokens[\"--tempest-border\"] = \"var(--tempest-gray-300)\";\n tokens[\"--tempest-border-strong\"] = \"var(--tempest-gray-400)\";\n tokens[\"--tempest-text\"] = \"var(--tempest-gray-900)\";\n tokens[\"--tempest-text-muted\"] = \"var(--tempest-gray-700)\";\n tokens[\"--tempest-text-subtle\"] = \"var(--tempest-gray-600)\";\n }\n tokens[\"--tempest-neutral-on-solid\"] = readableForeground(scale[700], \"#ffffff\", ON_SOLID_INK);\n}\n\nfunction renderBlock(selector: string, tokens: Record<string, string>): string {\n const entries = Object.entries(tokens);\n if (entries.length === 0) return \"\";\n const body = entries.map(([name, value]) => ` ${name}: ${value};`).join(\"\\n\");\n return `${selector} {\\n${body}\\n}`;\n}\n\n/**\n * Generate `--tempest-*` overrides from a small brand description.\n *\n * Only the families you pass are generated; everything else falls through to the\n * SDK's own tokens, so a theme stays a patch and not a fork of `colors.css`.\n *\n * @example\n * ```ts\n * import { applyTheme, createTheme } from \"tempest-react-sdk\";\n *\n * const theme = createTheme({\n * primary: \"#7c3aed\",\n * radius: \"lg\",\n * chart: [\"#7c3aed\", \"#0ea5e9\", \"#22c55e\", \"#f59e0b\"],\n * });\n *\n * applyTheme(theme);\n * ```\n *\n * @param options - Brand colors plus optional radius / chart / focus-ring tuning.\n * @returns The light and dark token maps and the CSS text that carries them.\n */\nexport function createTheme(options: CreateThemeOptions = {}): GeneratedTheme {\n const {\n primary,\n gray,\n chart,\n radius,\n focusRingAlpha,\n selector = \":root\",\n darkSelector = '[data-tempest-theme=\"dark\"]',\n inverse,\n inverseSelector = INVERSE_SELECTOR,\n } = options;\n\n warnOnTranslucentFocusRing(focusRingAlpha);\n\n const light: Record<string, string> = {};\n const dark: Record<string, string> = {};\n\n // `anchor: false`: a neutral ramp has to keep its tuned lightness curve.\n // Anchoring it at the input compressed both halves and dropped\n // text-muted-on-surface-3 to ~4.2:1 — below AA — which the browser axe\n // sweep caught on the generated themes.\n const grayScales = gray\n ? {\n light: createColorScale(gray, \"light\", { anchor: false, neutral: true }),\n dark: createColorScale(gray, \"dark\", { anchor: false, neutral: true }),\n }\n : undefined;\n\n if (primary) {\n const lightScale = createColorScale(primary, \"light\");\n const darkScale = createColorScale(primary, \"dark\");\n writeScale(light, \"primary\", lightScale);\n writeScale(dark, \"primary\", darkScale);\n writePrimaryAliases(\n light,\n lightScale,\n \"light\",\n surfacesOf(\"light\", grayScales),\n focusRingAlpha,\n );\n writePrimaryAliases(\n dark,\n darkScale,\n \"dark\",\n surfacesOf(\"dark\", grayScales),\n focusRingAlpha,\n );\n }\n\n if (grayScales) {\n writeScale(light, \"gray\", grayScales.light);\n writeScale(dark, \"gray\", grayScales.dark);\n writeNeutralAliases(light, grayScales.light, \"light\");\n writeNeutralAliases(dark, grayScales.dark, \"dark\");\n }\n\n for (const status of THEME_STATUSES) {\n const value = options[status];\n if (!value) continue;\n writeStatus(light, status, createColorScale(value, \"light\"), \"light\");\n writeStatus(dark, status, createColorScale(value, \"dark\"), \"dark\");\n }\n\n if (chart?.length) {\n chart.forEach((color, index) => {\n light[`--tempest-chart-${index + 1}`] = color;\n });\n // Declares how many series colors this theme owns. Without it the reader\n // keeps walking into the SDK's built-in `--tempest-chart-7`/`-8`, so a\n // 6-color brand palette silently mixes with two leftover defaults — which\n // is exactly what a 7-series chart would show.\n light[\"--tempest-chart-count\"] = String(chart.length);\n }\n\n /*\n * Continuous scales follow the brand too.\n *\n * Without this a rebranded app gets its categorical slots recoloured and then a\n * heatmap still painted in the SDK's blue — the one chart on the page that\n * silently ignores the brand. Both scales derive from the first series hue (or\n * the primary), stepped by perceptual lightness for each mode separately.\n *\n * Skipped entirely when the theme names no colour to derive from, so the\n * built-in tokens survive instead of being overwritten with a guess.\n */\n const scaleSource = chart?.[0] ?? primary;\n if (scaleSource) {\n const coolHue = hueOf(scaleSource);\n // The opposite pole: the brand's own danger colour when it has one, since a\n // diverging scale's warm arm and \"bad\" should not disagree on screen.\n const warmHue = options.danger ? hueOf(options.danger) : (coolHue + 180) % 360;\n for (const [target, mode] of [\n [light, \"light\"],\n [dark, \"dark\"],\n ] as const) {\n buildRamp(coolHue, mode).forEach((stepColor, index) => {\n target[`--tempest-chart-sequential-${index + 1}`] = stepColor;\n });\n const diverging = buildDivergingRamp({\n coolHue,\n warmHue,\n mid: mode === \"light\" ? \"#e4e7ec\" : \"#262d3f\",\n mode,\n });\n [...diverging.cool, diverging.mid, ...diverging.warm].forEach((stepColor, index) => {\n target[`--tempest-chart-diverging-${index + 1}`] = stepColor;\n });\n }\n }\n\n if (radius) {\n const steps = typeof radius === \"string\" ? RADIUS_PRESETS[radius] : radius;\n for (const [step, value] of Object.entries(steps)) {\n light[`--tempest-radius-${step}`] = value;\n }\n }\n\n const inverseSurface =\n primary && inverse\n ? inverse({\n primary,\n statuses: {\n success: options.success,\n warning: options.warning,\n danger: options.danger,\n info: options.info,\n },\n chart,\n })\n : null;\n\n const css = [\n renderBlock(selector, light),\n renderBlock(darkSelector, dark),\n inverseSurface ? renderInverseSurface(inverseSelector, inverseSurface) : \"\",\n ]\n .filter(Boolean)\n .join(\"\\n\\n\");\n\n return inverseSurface\n ? { light, dark, inverse: inverseSurface.tokens, css }\n : { light, dark, css };\n}\n\n/**\n * Contrast ratio of `--tempest-primary-foreground` over the brand color.\n *\n * Exposed so an app (or a test) can assert its own brand clears WCAG AA (4.5) for\n * body text or AA-large (3.0) for button labels, instead of trusting the pick.\n *\n * @param options - The same input given to {@link createTheme}.\n * @returns The ratio, or `null` when no `primary` was provided.\n */\nexport function themeContrast(options: CreateThemeOptions): number | null {\n if (!options.primary) return null;\n const scale = createColorScale(options.primary, \"light\");\n return contrastRatio(scale[500], readableForeground(scale[500]));\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"create-theme.js","names":[],"sources":["../../src/theme/create-theme.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — one pass that emits the whole token set — surfaces,\n * text, borders, states, the chart ramps — for both modes, with every value checked\n * against the surface it will sit on. The checks are what make the length: each\n * token is derived and then verified against the same background table.\n */\n/**\n * Theme factory: turns a handful of brand colors into the full set of\n * `--tempest-*` token overrides, for both light and dark.\n *\n * Rebranding used to mean hand-writing ~167 custom properties (and getting the\n * dark ramp inversion right by hand). `createTheme({ primary: \"#7c3aed\" })`\n * emits the same thing, derived in OKLCH so the ramp stays perceptually even.\n */\nimport {\n contrastRatio,\n createColorScale,\n hexToRgbaString,\n readableForeground,\n type ColorScale,\n type ScaleStep,\n} from \"./color\";\nimport { buildDivergingRamp, buildRamp, hueOf } from \"./data-viz-ramps\";\nimport { isDevBuild } from \"../utils/dev-mode\";\nimport {\n INVERSE_SELECTOR,\n renderInverseSurface,\n type InverseSurfaceGenerator,\n} from \"./inverse-render\";\nimport { ON_SOLID_INK, THEME_STATUSES, writeStatus } from \"./status-tokens\";\n\n/** Radius presets, applied to the whole `--tempest-radius-*` family at once. */\nexport type ThemeRadius = \"none\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"full\";\n\nexport type { ThemeStatus } from \"./status-tokens\";\n\n/** Input for {@link createTheme}. Every field is optional — omitted families keep the built-in tokens. */\nexport interface CreateThemeOptions {\n /** Brand color, used as the `500` step of the primary scale. */\n primary?: string;\n /** Neutral color, used as the `500` step of the gray scale (surfaces, borders, text). */\n gray?: string;\n /** Success color (`--tempest-success*`). */\n success?: string;\n /** Warning color (`--tempest-warning*`). */\n warning?: string;\n /** Danger color (`--tempest-danger*`). */\n danger?: string;\n /** Info color (`--tempest-info*`). */\n info?: string;\n /**\n * Categorical series colors for `tempest-react-sdk/charts`, in cycle order.\n * Written to `--tempest-chart-1` … `--tempest-chart-N`, so charts follow the\n * theme instead of a hardcoded palette.\n */\n chart?: string[];\n /** Corner radius scale. A preset name, or explicit per-step values. */\n radius?: ThemeRadius | Partial<Record<\"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\", string>>;\n /**\n * Opacity of `--tempest-focus-ring-color`. Omit it — the default ring is opaque.\n *\n * A translucent ring has no contrast of its own; it has the contrast of\n * whatever it composites over. Passing a value below `1` reintroduces the\n * failure 0.61.0 fixed in the built-in tokens, and logs a warning in a\n * development build. Kept because a translucent ring over a background you\n * control is a legitimate choice.\n */\n focusRingAlpha?: number;\n /** Selector the light tokens are written under. Default `\":root\"`. */\n selector?: string;\n /** Selector the dark tokens are written under. Default `'[data-tempest-theme=\"dark\"]'`. */\n darkSelector?: string;\n /**\n * Also emit the inverse surface — the token set for a section painted in the\n * brand color — by passing its generator, `createInverseSurface`. Off by\n * default; ignored without `primary`.\n *\n * The generator is passed in rather than switched on with `true` so that\n * `createTheme` never imports it: an app that does not paint a section in\n * its brand does not ship the 1.68 kB brotli it costs.\n *\n * @example\n * ```ts\n * import { createInverseSurface, createTheme } from \"tempest-react-sdk\";\n *\n * createTheme({ primary: \"#03184b\", inverse: createInverseSurface });\n * ```\n */\n inverse?: InverseSurfaceGenerator;\n /** Selector the inverse tokens are written under. Default `'[data-tempest-tone=\"inverse\"]'`. */\n inverseSelector?: string;\n}\n\n/** A generated theme: token maps per color scheme, plus the CSS text that carries them. */\nexport interface GeneratedTheme {\n /** Light-scheme custom properties, without the leading `--`-less names (keys include `--`). */\n light: Record<string, string>;\n /** Dark-scheme custom properties. */\n dark: Record<string, string>;\n /**\n * Inverse-surface custom properties — literal colors, identical under the\n * light and the dark page scheme. Present only when the theme names a\n * `primary` and passes `inverse`.\n */\n inverse?: Record<string, string>;\n /** Both blocks rendered as CSS, ready for {@link applyTheme} or a stylesheet. */\n css: string;\n}\n\nconst RADIUS_PRESETS: Record<ThemeRadius, Record<string, string>> = {\n none: { xs: \"0\", sm: \"0\", md: \"0\", lg: \"0\", xl: \"0\", \"2xl\": \"0\" },\n sm: { xs: \"1px\", sm: \"2px\", md: \"4px\", lg: \"6px\", xl: \"8px\", \"2xl\": \"12px\" },\n md: { xs: \"2px\", sm: \"4px\", md: \"8px\", lg: \"12px\", xl: \"16px\", \"2xl\": \"24px\" },\n lg: { xs: \"4px\", sm: \"6px\", md: \"12px\", lg: \"16px\", xl: \"22px\", \"2xl\": \"32px\" },\n xl: { xs: \"6px\", sm: \"10px\", md: \"16px\", lg: \"24px\", xl: \"32px\", \"2xl\": \"44px\" },\n full: { xs: \"4px\", sm: \"8px\", md: \"9999px\", lg: \"9999px\", xl: \"9999px\", \"2xl\": \"9999px\" },\n};\n\nconst SCALE_STEPS: ScaleStep[] = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900];\n\nfunction writeScale(tokens: Record<string, string>, name: string, scale: ColorScale): void {\n for (const step of SCALE_STEPS) {\n tokens[`--tempest-${name}-${step}`] = scale[step];\n }\n}\n\n/** Minimum contrast for body text, WCAG 2.x AA. */\nconst AA_TEXT_CONTRAST = 4.5;\n\n/**\n * Choose the ramp step for text sitting on the soft tint.\n *\n * A fixed step does not survive an arbitrary brand color: the built-in blue only\n * reaches 4.37:1 at `500` over its own `50` tint, and a generated emerald lands\n * at 4.41:1 at `600` — both fail AA for body text by a hair. So the step is\n * picked by measuring, walking away from the tint until it clears AA, and\n * falling back to the most extreme candidate when even that cannot (a very light\n * or very desaturated brand).\n *\n * @param scale - The generated ramp.\n * @param softStep - Step used as `--tempest-primary-soft`.\n * @param candidates - Steps to try, in order of preference.\n * @returns The first candidate clearing {@link AA_TEXT_CONTRAST}, else the last.\n */\nfunction pickOnSoftStep(\n scale: ColorScale,\n softStep: ScaleStep,\n candidates: ScaleStep[],\n): ScaleStep {\n for (const step of candidates) {\n if (contrastRatio(scale[softStep], scale[step]) >= AA_TEXT_CONTRAST) return step;\n }\n return candidates[candidates.length - 1];\n}\n\n/** Minimum contrast for a non-text indicator, WCAG 2.2 SC 1.4.11. */\nconst INDICATOR_CONTRAST = 3;\n\n/**\n * The four surfaces a focus ring can land on, as the SDK's own `colors.css`\n * paints them.\n *\n * Ported from `src/styles/colors.css` (`--tempest-bg`, `--tempest-surface`,\n * `--tempest-surface-2`, `--tempest-surface-3`, resolved through the gray ramp),\n * and pinned by a test that reads that file — a theme that only names `primary`\n * keeps these, so the ring has to be measured against them and not against a\n * guess. The light values resolve through `--tempest-gray-50/100/200`; the dark\n * ones are literals in the dark block.\n */\nconst SDK_SURFACES: Record<\"light\" | \"dark\", readonly string[]> = {\n light: [\"#ffffff\", \"#f8f9fb\", \"#f1f3f6\", \"#e4e7ec\"],\n dark: [\"#0b0d12\", \"#14171f\", \"#1d2230\", \"#262d3f\"],\n};\n\n/**\n * Ramp steps to try for the focus ring, in order.\n *\n * `500` first, because the ring should read as the brand when it can, then up\n * the ramp — which moves away from the surfaces in **both** schemes, because\n * `createColorScale` inverts the dark ramp: there `50` is the darkest step (it\n * is what `--tempest-bg` is made of) and `900` the lightest. Walking down in\n * dark mode looks like the right inversion and is the wrong direction; measured,\n * it left a near-black brand's ring at 1.04:1 over its own background.\n */\nconst FOCUS_RING_CANDIDATES: readonly ScaleStep[] = [500, 600, 700, 800, 900];\n\n/**\n * Choose the ramp step for the focus ring, by measuring it against the surfaces\n * it will actually be drawn on.\n *\n * A fixed step does not survive an arbitrary brand, and the measurement says so\n * loudly: of twelve brands checked against the four surfaces in both schemes —\n * 96 pairings — `500` opaque clears 3:1 in 58 of them, and `500` at the old\n * default alpha of 0.35 in 3. Picking the step by measurement clears all 96. The\n * brand from the report that opened this\n * (`#8100D7`) passes in light at 7.20:1 and fails in **dark** at 2.70:1 down to\n * 1.91:1 on `surface-3`; a yellow fails in light at 1.43:1. So the step is\n * picked the way {@link pickOnSoftStep} picks text on a tint: walk away from the\n * surfaces until the ring clears the floor against *all* of them.\n *\n * The fallback is the candidate with the best worst case rather than the last\n * one, because \"the most extreme step\" is not always the most contrasting one\n * once a brand is near-neutral.\n *\n * @param scale - The generated ramp.\n * @param surfaces - The four surfaces this theme will paint, opaque hex.\n * @returns The first candidate clearing {@link INDICATOR_CONTRAST} on every\n * surface, else the candidate whose weakest pairing is strongest.\n */\nfunction pickFocusRingStep(scale: ColorScale, surfaces: readonly string[]): ScaleStep {\n const worstCase = (step: ScaleStep): number =>\n Math.min(...surfaces.map((surface) => contrastRatio(scale[step], surface)));\n let best: ScaleStep = FOCUS_RING_CANDIDATES[0] as ScaleStep;\n let bestRatio = -1;\n for (const step of FOCUS_RING_CANDIDATES) {\n const measured = worstCase(step);\n if (measured >= INDICATOR_CONTRAST) return step;\n if (measured > bestRatio) {\n best = step;\n bestRatio = measured;\n }\n }\n return best;\n}\n\n/**\n * The surfaces this theme will actually paint, per scheme.\n *\n * A theme that names `gray` repaints all four, so measuring the ring against the\n * SDK's would be measuring the wrong backdrop — and a theme that does not keeps\n * the SDK's, which is the common case. The order and the ramp steps mirror\n * {@link writeNeutralAliases}, which is what emits them.\n *\n * @param scheme - Which scheme is being emitted.\n * @param grayScales - The generated neutral ramps, when the theme names one.\n * @returns Four opaque hex colors: bg, surface, surface-2, surface-3.\n */\nfunction surfacesOf(\n scheme: \"light\" | \"dark\",\n grayScales: { light: ColorScale; dark: ColorScale } | undefined,\n): readonly string[] {\n if (!grayScales) return SDK_SURFACES[scheme];\n const scale = grayScales[scheme];\n return scheme === \"light\"\n ? [\"#ffffff\", scale[50], scale[100], scale[200]]\n : [scale[50], scale[100], scale[200], scale[300]];\n}\n\n/**\n * Warn, in a development build, that an alpha was asked for the focus ring.\n *\n * A translucent ring has no contrast of its own — it has the contrast of\n * whatever it composites over — and that is the failure this option used to\n * ship by default: measured at alpha `0.35`, twelve brands across four surfaces\n * and both schemes produced 96 pairings, of which 3 cleared the 3:1 of WCAG 2.2\n * SC 1.4.11. It stays available, because a translucent ring over a\n * known background is a legitimate choice, but it is a choice a theme now has to\n * make on purpose.\n *\n * The warning is the difficult part to notice otherwise: a failing focus ring is\n * visible, it just does not separate.\n *\n * @param focusRingAlpha - The opacity the caller asked for, if any.\n */\nfunction warnOnTranslucentFocusRing(focusRingAlpha: number | undefined): void {\n if (focusRingAlpha === undefined || focusRingAlpha >= 1) return;\n if (!isDevBuild()) return;\n console.warn(\n `[tempest-react-sdk] createTheme({ focusRingAlpha: ${focusRingAlpha} }) makes the focus ring ` +\n \"translucent, so its contrast becomes whatever it composites over — measured below the 3:1 \" +\n \"of WCAG 2.2 SC 1.4.11 on every surface the SDK paints. Drop the option to get the opaque \" +\n \"ring, which is derived from your brand ramp and measured against those surfaces.\",\n );\n}\n\n/**\n * Emit the primary aliases for one scheme.\n *\n * The dark scheme walks the ramp the other way (hover is *lighter*, the soft\n * tint is a dark shade, and readable text on that tint is a light shade) — the\n * same inversion the built-in `colors.css` dark block does by hand.\n *\n * The selected-state indicator is derived from the same measurement as the focus\n * ring and emitted as its own token. It never takes `focusRingAlpha`: a\n * translucent ring over a known background is a legitimate choice a theme can\n * make, while a translucent *state* indicator has the contrast of whatever it\n * composites over, which is the defect this token exists to end.\n *\n * @param tokens - The token map for this scheme, written in place.\n * @param scale - The generated primary ramp.\n * @param scheme - Which scheme is being emitted.\n * @param surfaces - The surfaces this theme paints, for the focus-ring measurement.\n * @param focusRingAlpha - Opacity asked for the ring, or `undefined` for opaque.\n */\nfunction writePrimaryAliases(\n tokens: Record<string, string>,\n scale: ColorScale,\n scheme: \"light\" | \"dark\",\n surfaces: readonly string[],\n focusRingAlpha: number | undefined,\n): void {\n tokens[\"--tempest-primary\"] = \"var(--tempest-primary-500)\";\n if (scheme === \"light\") {\n tokens[\"--tempest-primary-hover\"] = \"var(--tempest-primary-600)\";\n tokens[\"--tempest-primary-active\"] = \"var(--tempest-primary-700)\";\n tokens[\"--tempest-primary-soft\"] = \"var(--tempest-primary-50)\";\n tokens[\"--tempest-primary-soft-hover\"] = \"var(--tempest-primary-100)\";\n tokens[\"--tempest-primary-on-soft\"] =\n `var(--tempest-primary-${pickOnSoftStep(scale, 50, [600, 700, 800, 900])})`;\n } else {\n tokens[\"--tempest-primary-hover\"] = \"var(--tempest-primary-400)\";\n tokens[\"--tempest-primary-active\"] = \"var(--tempest-primary-300)\";\n tokens[\"--tempest-primary-soft\"] = \"var(--tempest-primary-100)\";\n tokens[\"--tempest-primary-soft-hover\"] = \"var(--tempest-primary-200)\";\n tokens[\"--tempest-primary-on-soft\"] =\n `var(--tempest-primary-${pickOnSoftStep(scale, 100, [700, 800, 900])})`;\n }\n\n const foreground = readableForeground(scale[500]);\n tokens[\"--tempest-primary-foreground\"] = foreground;\n tokens[\"--tempest-text-on-primary\"] = foreground;\n const ring = scale[pickFocusRingStep(scale, surfaces)];\n tokens[\"--tempest-focus-ring-color\"] =\n focusRingAlpha === undefined || focusRingAlpha >= 1\n ? ring\n : hexToRgbaString(ring, focusRingAlpha);\n tokens[\"--tempest-selected-indicator\"] = ring;\n}\n\n/**\n * Emit the neutral surface/border/text aliases from a generated gray scale.\n *\n * `--tempest-bg` is pushed past the ramp on purpose: pure white in light mode and\n * a shade darker than `gray-50` in dark mode, so a raised surface still reads as\n * raised against the page.\n */\nfunction writeNeutralAliases(\n tokens: Record<string, string>,\n scale: ColorScale,\n scheme: \"light\" | \"dark\",\n): void {\n if (scheme === \"light\") {\n tokens[\"--tempest-bg\"] = \"#ffffff\";\n tokens[\"--tempest-surface\"] = \"var(--tempest-gray-50)\";\n tokens[\"--tempest-surface-2\"] = \"var(--tempest-gray-100)\";\n tokens[\"--tempest-surface-3\"] = \"var(--tempest-gray-200)\";\n tokens[\"--tempest-border\"] = \"var(--tempest-gray-200)\";\n tokens[\"--tempest-border-strong\"] = \"var(--tempest-gray-300)\";\n tokens[\"--tempest-text\"] = \"var(--tempest-gray-900)\";\n tokens[\"--tempest-text-muted\"] = \"var(--tempest-gray-600)\";\n tokens[\"--tempest-text-subtle\"] = \"var(--tempest-gray-500)\";\n } else {\n tokens[\"--tempest-bg\"] = scale[50];\n tokens[\"--tempest-surface\"] = \"var(--tempest-gray-100)\";\n tokens[\"--tempest-surface-2\"] = \"var(--tempest-gray-200)\";\n tokens[\"--tempest-surface-3\"] = \"var(--tempest-gray-300)\";\n tokens[\"--tempest-border\"] = \"var(--tempest-gray-300)\";\n tokens[\"--tempest-border-strong\"] = \"var(--tempest-gray-400)\";\n tokens[\"--tempest-text\"] = \"var(--tempest-gray-900)\";\n tokens[\"--tempest-text-muted\"] = \"var(--tempest-gray-700)\";\n tokens[\"--tempest-text-subtle\"] = \"var(--tempest-gray-600)\";\n }\n tokens[\"--tempest-neutral-on-solid\"] = readableForeground(scale[700], \"#ffffff\", ON_SOLID_INK);\n}\n\nfunction renderBlock(selector: string, tokens: Record<string, string>): string {\n const entries = Object.entries(tokens);\n if (entries.length === 0) return \"\";\n const body = entries.map(([name, value]) => ` ${name}: ${value};`).join(\"\\n\");\n return `${selector} {\\n${body}\\n}`;\n}\n\n/**\n * Generate `--tempest-*` overrides from a small brand description.\n *\n * Only the families you pass are generated; everything else falls through to the\n * SDK's own tokens, so a theme stays a patch and not a fork of `colors.css`.\n *\n * @example\n * ```ts\n * import { applyTheme, createTheme } from \"tempest-react-sdk\";\n *\n * const theme = createTheme({\n * primary: \"#7c3aed\",\n * radius: \"lg\",\n * chart: [\"#7c3aed\", \"#0ea5e9\", \"#22c55e\", \"#f59e0b\"],\n * });\n *\n * applyTheme(theme);\n * ```\n *\n * @param options - Brand colors plus optional radius / chart / focus-ring tuning.\n * @returns The light and dark token maps and the CSS text that carries them.\n */\nexport function createTheme(options: CreateThemeOptions = {}): GeneratedTheme {\n const {\n primary,\n gray,\n chart,\n radius,\n focusRingAlpha,\n selector = \":root\",\n darkSelector = '[data-tempest-theme=\"dark\"]',\n inverse,\n inverseSelector = INVERSE_SELECTOR,\n } = options;\n\n warnOnTranslucentFocusRing(focusRingAlpha);\n\n const light: Record<string, string> = {};\n const dark: Record<string, string> = {};\n\n // `anchor: false`: a neutral ramp has to keep its tuned lightness curve.\n // Anchoring it at the input compressed both halves and dropped\n // text-muted-on-surface-3 to ~4.2:1 — below AA — which the browser axe\n // sweep caught on the generated themes.\n const grayScales = gray\n ? {\n light: createColorScale(gray, \"light\", { anchor: false, neutral: true }),\n dark: createColorScale(gray, \"dark\", { anchor: false, neutral: true }),\n }\n : undefined;\n\n if (primary) {\n const lightScale = createColorScale(primary, \"light\");\n const darkScale = createColorScale(primary, \"dark\");\n writeScale(light, \"primary\", lightScale);\n writeScale(dark, \"primary\", darkScale);\n writePrimaryAliases(\n light,\n lightScale,\n \"light\",\n surfacesOf(\"light\", grayScales),\n focusRingAlpha,\n );\n writePrimaryAliases(\n dark,\n darkScale,\n \"dark\",\n surfacesOf(\"dark\", grayScales),\n focusRingAlpha,\n );\n }\n\n if (grayScales) {\n writeScale(light, \"gray\", grayScales.light);\n writeScale(dark, \"gray\", grayScales.dark);\n writeNeutralAliases(light, grayScales.light, \"light\");\n writeNeutralAliases(dark, grayScales.dark, \"dark\");\n }\n\n for (const status of THEME_STATUSES) {\n const value = options[status];\n if (!value) continue;\n writeStatus(light, status, createColorScale(value, \"light\"), \"light\");\n writeStatus(dark, status, createColorScale(value, \"dark\"), \"dark\");\n }\n\n if (chart?.length) {\n chart.forEach((color, index) => {\n light[`--tempest-chart-${index + 1}`] = color;\n });\n // Declares how many series colors this theme owns. Without it the reader\n // keeps walking into the SDK's built-in `--tempest-chart-7`/`-8`, so a\n // 6-color brand palette silently mixes with two leftover defaults — which\n // is exactly what a 7-series chart would show.\n light[\"--tempest-chart-count\"] = String(chart.length);\n }\n\n /*\n * Continuous scales follow the brand too.\n *\n * Without this a rebranded app gets its categorical slots recoloured and then a\n * heatmap still painted in the SDK's blue — the one chart on the page that\n * silently ignores the brand. Both scales derive from the first series hue (or\n * the primary), stepped by perceptual lightness for each mode separately.\n *\n * Skipped entirely when the theme names no colour to derive from, so the\n * built-in tokens survive instead of being overwritten with a guess.\n */\n const scaleSource = chart?.[0] ?? primary;\n if (scaleSource) {\n const coolHue = hueOf(scaleSource);\n // The opposite pole: the brand's own danger colour when it has one, since a\n // diverging scale's warm arm and \"bad\" should not disagree on screen.\n const warmHue = options.danger ? hueOf(options.danger) : (coolHue + 180) % 360;\n for (const [target, mode] of [\n [light, \"light\"],\n [dark, \"dark\"],\n ] as const) {\n buildRamp(coolHue, mode).forEach((stepColor, index) => {\n target[`--tempest-chart-sequential-${index + 1}`] = stepColor;\n });\n const diverging = buildDivergingRamp({\n coolHue,\n warmHue,\n mid: mode === \"light\" ? \"#e4e7ec\" : \"#262d3f\",\n mode,\n });\n [...diverging.cool, diverging.mid, ...diverging.warm].forEach((stepColor, index) => {\n target[`--tempest-chart-diverging-${index + 1}`] = stepColor;\n });\n }\n }\n\n if (radius) {\n const steps = typeof radius === \"string\" ? RADIUS_PRESETS[radius] : radius;\n for (const [step, value] of Object.entries(steps)) {\n light[`--tempest-radius-${step}`] = value;\n }\n }\n\n const inverseSurface =\n primary && inverse\n ? inverse({\n primary,\n statuses: {\n success: options.success,\n warning: options.warning,\n danger: options.danger,\n info: options.info,\n },\n chart,\n selector: inverseSelector,\n page: { selector, darkSelector },\n })\n : null;\n\n const css = [\n renderBlock(selector, light),\n renderBlock(darkSelector, dark),\n inverseSurface ? renderInverseSurface(inverseSelector, inverseSurface) : \"\",\n ]\n .filter(Boolean)\n .join(\"\\n\\n\");\n\n return inverseSurface\n ? { light, dark, inverse: inverseSurface.tokens, css }\n : { light, dark, css };\n}\n\n/**\n * Contrast ratio of `--tempest-primary-foreground` over the brand color.\n *\n * Exposed so an app (or a test) can assert its own brand clears WCAG AA (4.5) for\n * body text or AA-large (3.0) for button labels, instead of trusting the pick.\n *\n * @param options - The same input given to {@link createTheme}.\n * @returns The ratio, or `null` when no `primary` was provided.\n */\nexport function themeContrast(options: CreateThemeOptions): number | null {\n if (!options.primary) return null;\n const scale = createColorScale(options.primary, \"light\");\n return contrastRatio(scale[500], readableForeground(scale[500]));\n}\n"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
1
  var e=`[data-tempest-tone="inverse"]`;function t(e,t){let n=Object.entries(t.tokens).map(([e,t])=>` ${e}: ${t};`).join(`
2
- `);return`${e} {\n color-scheme: ${t.scheme};\n${n}\n}\n\n:where(${e}) {\n background-color: var(--tempest-bg);\n color: var(--tempest-text);\n}`}exports.INVERSE_SELECTOR=e,exports.renderInverseSurface=t;
2
+ `);return`${e} {\n color-scheme: ${t.scheme};\n${n}\n}\n\n:where(${e}) {\n background-color: var(--tempest-bg);\n color: var(--tempest-text);\n}`+(t.rules?`\n\n${t.rules}`:``)}exports.INVERSE_SELECTOR=e,exports.renderInverseSurface=t;
3
3
  //# sourceMappingURL=inverse-render.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"inverse-render.cjs","names":[],"sources":["../../src/theme/inverse-render.ts"],"sourcesContent":["/**\n * The small, always-loaded half of the inverse surface: its types, its default\n * selector and the CSS renderer. The generator lives in `inverse-surface.ts`,\n * which only an app that asks for it imports.\n */\nimport type { ScaleStep } from \"./color\";\nimport type { ThemeStatus } from \"./status-tokens\";\n\n/** Default selector the inverse tokens are written under. */\nexport const INVERSE_SELECTOR = '[data-tempest-tone=\"inverse\"]';\n\n/** Which way the inverse surface reads: light ink on a dark fill, or the reverse. */\nexport type InverseScheme = \"light\" | \"dark\";\n\n/** The inverse surface for one brand. */\nexport interface InverseSurface {\n /** Every color custom property the surface redefines, as literal colors. */\n tokens: Record<string, string>;\n /**\n * `\"dark\"` when the fill carries light ink (a navy brand), `\"light\"` when it\n * carries dark ink (a yellow brand). Written as `color-scheme`, so native\n * controls inside the section follow the fill rather than the page.\n */\n scheme: InverseScheme;\n /** Step of the brand's light ramp used as the fill. */\n step: ScaleStep;\n}\n\n/** Input for an {@link InverseSurfaceGenerator} — `createInverseSurface`. */\nexport interface InverseSurfaceOptions {\n /** Brand color — the fill is picked from its ramp. */\n primary: string;\n /**\n * Status colors the theme names; the rest use the SDK's defaults. A named\n * `danger` is also the warm pole of the diverging chart scale, as on the page.\n */\n statuses?: Partial<Record<ThemeStatus, string>>;\n /** Categorical chart colors the theme names; else the SDK's defaults. */\n chart?: readonly string[];\n}\n\n/**\n * The inverse-surface generator `createTheme` calls when it is handed one —\n * in practice `createInverseSurface`.\n *\n * A function rather than a flag on purpose: `createTheme` never imports the\n * generator, so an app that does not paint a section in its brand does not ship\n * it. Measured with `npx size-limit`, `{ createTheme }` is 3.18 kB brotli on its\n * own and 4.86 kB with the generator inside it — what a boolean option would\n * have charged every theme.\n */\nexport type InverseSurfaceGenerator = (options: InverseSurfaceOptions) => InverseSurface;\n\n/**\n * Render the inverse surface as CSS: the token block, plus a zero-specificity\n * rule that paints the fill and the ink on the element itself.\n *\n * The paint rule is what makes the attribute a complete shortcut — without it a\n * `<section data-tempest-tone=\"inverse\">` would get inverted tokens and keep the\n * page's background, which is white text on white. It goes through `:where()`\n * so any background the app sets on the same element wins without a fight.\n *\n * @param selector - Selector the surface is scoped to.\n * @param surface - The surface an {@link InverseSurfaceGenerator} returned.\n * @returns The CSS text.\n */\nexport function renderInverseSurface(selector: string, surface: InverseSurface): string {\n const body = Object.entries(surface.tokens)\n .map(([name, value]) => ` ${name}: ${value};`)\n .join(\"\\n\");\n return (\n `${selector} {\\n color-scheme: ${surface.scheme};\\n${body}\\n}\\n\\n` +\n `:where(${selector}) {\\n background-color: var(--tempest-bg);\\n color: var(--tempest-text);\\n}`\n );\n}\n"],"mappings":"AASA,IAAa,EAAmB,gCAyDhC,SAAgB,EAAqB,EAAkB,EAAiC,CACpF,IAAM,EAAO,OAAO,QAAQ,EAAQ,MAAM,CAAC,CACtC,KAAK,CAAC,EAAM,KAAW,OAAO,EAAK,IAAI,EAAM,EAAE,CAAC,CAChD,KAAK;CAAI,EACd,MACI,GAAG,EAAS,wBAAwB,EAAQ,OAAO,KAAK,EAAK,gBACnD,EAAS,kFAE3B"}
1
+ {"version":3,"file":"inverse-render.cjs","names":[],"sources":["../../src/theme/inverse-render.ts"],"sourcesContent":["/**\n * The small, always-loaded half of the inverse surface: its types, its default\n * selector and the CSS renderer. The generator lives in `inverse-surface.ts`,\n * which only an app that asks for it imports.\n */\nimport type { ScaleStep } from \"./color\";\nimport type { ThemeStatus } from \"./status-tokens\";\n\n/** Default selector the inverse tokens are written under. */\nexport const INVERSE_SELECTOR = '[data-tempest-tone=\"inverse\"]';\n\n/** Which way the inverse surface reads: light ink on a dark fill, or the reverse. */\nexport type InverseScheme = \"light\" | \"dark\";\n\n/** Where the page's schemes are written: the selectors `createTheme` uses. */\nexport interface PageSelectors {\n /** Selector of the light scheme. Default `:root`. */\n selector: string;\n /** Selector of the dark scheme. Default `'[data-tempest-theme=\"dark\"]'`. */\n darkSelector: string;\n}\n\n/** The inverse surface for one brand. */\nexport interface InverseSurface {\n /** Every color custom property the surface redefines, as literal colors. */\n tokens: Record<string, string>;\n /**\n * `\"dark\"` when the fill carries light ink (a navy brand), `\"light\"` when it\n * carries dark ink (a yellow brand). Written as `color-scheme`, so native\n * controls inside the section follow the fill rather than the page.\n */\n scheme: InverseScheme;\n /** Step of the brand's light ramp used as the fill. */\n step: ScaleStep;\n /**\n * CSS written after the surface: the rules that let a region inside it go\n * back to the page's tokens with `data-tempest-tone=\"default\"`. Rendered by\n * the generator, so `createTheme` alone does not ship the code that writes it.\n */\n rules?: string;\n}\n\n/** Input for an {@link InverseSurfaceGenerator} — `createInverseSurface`. */\nexport interface InverseSurfaceOptions {\n /** Brand color — the fill is picked from its ramp. */\n primary: string;\n /**\n * Status colors the theme names; the rest use the SDK's defaults. A named\n * `danger` is also the warm pole of the diverging chart scale, as on the page.\n */\n statuses?: Partial<Record<ThemeStatus, string>>;\n /** Categorical chart colors the theme names; else the SDK's defaults. */\n chart?: readonly string[];\n /**\n * Selector the surface is written under, which the default-tone rules keep\n * out of the elements that save the page's values. Default\n * `'[data-tempest-tone=\"inverse\"]'`.\n */\n selector?: string;\n /**\n * Selectors the page's schemes are written under, so a region restores a\n * theme scoped to them too. Default `createTheme`'s: `:root` and\n * `'[data-tempest-theme=\"dark\"]'`.\n */\n page?: PageSelectors;\n}\n\n/**\n * The inverse-surface generator `createTheme` calls when it is handed one —\n * in practice `createInverseSurface`.\n *\n * A function rather than a flag on purpose: `createTheme` never imports the\n * generator, so an app that does not paint a section in its brand does not ship\n * it. Measured with `npx size-limit`, `{ createTheme }` is 3.18 kB brotli on its\n * own and 4.86 kB with the generator inside it — what a boolean option would\n * have charged every theme.\n */\nexport type InverseSurfaceGenerator = (options: InverseSurfaceOptions) => InverseSurface;\n\n/**\n * Render the inverse surface as CSS: the token block, a zero-specificity rule\n * that paints the fill and the ink on the element itself, and the surface's own\n * {@link InverseSurface.rules}.\n *\n * The paint rule is what makes the attribute a complete shortcut — without it a\n * `<section data-tempest-tone=\"inverse\">` would get inverted tokens and keep the\n * page's background, which is white text on white. It goes through `:where()`\n * so any background the app sets on the same element wins without a fight.\n *\n * @param selector - Selector the surface is scoped to.\n * @param surface - The surface an {@link InverseSurfaceGenerator} returned.\n * @returns The CSS text.\n */\nexport function renderInverseSurface(selector: string, surface: InverseSurface): string {\n const body = Object.entries(surface.tokens)\n .map(([name, value]) => ` ${name}: ${value};`)\n .join(\"\\n\");\n return (\n `${selector} {\\n color-scheme: ${surface.scheme};\\n${body}\\n}\\n\\n` +\n `:where(${selector}) {\\n background-color: var(--tempest-bg);\\n color: var(--tempest-text);\\n}` +\n (surface.rules ? `\\n\\n${surface.rules}` : \"\")\n );\n}\n"],"mappings":"AASA,IAAa,EAAmB,gCAoFhC,SAAgB,EAAqB,EAAkB,EAAiC,CACpF,IAAM,EAAO,OAAO,QAAQ,EAAQ,MAAM,CAAC,CACtC,KAAK,CAAC,EAAM,KAAW,OAAO,EAAK,IAAI,EAAM,EAAE,CAAC,CAChD,KAAK;CAAI,EACd,MACI,GAAG,EAAS,wBAAwB,EAAQ,OAAO,KAAK,EAAK,gBACnD,EAAS,oFAClB,EAAQ,MAAQ,OAAO,EAAQ,QAAU,GAElD"}