bestax-mcp 1.18.7 → 1.18.9

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.
@@ -13082,6 +13082,11 @@
13082
13082
  "match": "^is-(?:apple|bitbucket|discord|dropbox|facebook|flickr|foursquare|github|gitlab|instagram|linkedin|microsoft|odnoklassniki|openid|pinterest|reddit|slack|soundcloud|tumblr|twitch|twitter|vimeo|vk|x|yahoo|youtube|animated)$",
13083
13083
  "glyph": false
13084
13084
  },
13085
+ {
13086
+ "why": "a bulma-social helper, a Bulma extension's text or background color in a brand's color, with `-dark` and `-light` for its shades: keep loading bulma-social and keep the class in `className` on the element that has it, since bestax has no prop for it (the bestax-migrate skill's `html-asset:bulma-extension` has the recipe, and the brand's own stylesheet holds it)",
13087
+ "match": "^has-(?:text|background)-(?:apple|bitbucket|discord|dropbox|facebook|flickr|foursquare|github|gitlab|instagram|linkedin|microsoft|odnoklassniki|openid|pinterest|reddit|slack|soundcloud|tumblr|twitch|twitter|vimeo|vk|x|yahoo|youtube)(?:-dark|-light)?$",
13088
+ "glyph": false
13089
+ },
13085
13090
  {
13086
13091
  "why": "a bulma-carousel class, a Bulma extension bestax's `Carousel` does the job of (the bestax-migrate skill's `html-asset:bulma-extension` says how). Once the page stops loading bulma-carousel nothing styles it: `is-covered`'s `object-fit: cover` on a slide's image goes in the app's own CSS",
13087
13092
  "match": "^(?:has-carousel|hero-carousel|is-background|is-covered|slider-(?:container|item|navigation-next|navigation-previous|page|pagination)|video-container)$",
package/data/catalog.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "schemaVersion": 2,
3
3
  "generatedFrom": {
4
4
  "package": "@allxsmith/bestax-bulma",
5
- "version": "5.45.1"
5
+ "version": "5.47.0"
6
6
  },
7
7
  "docsBase": "https://bestax.io/docs",
8
8
  "categories": [
@@ -641,7 +641,7 @@
641
641
  "import": "import { Input, Field, Control, Button, Select, Icon } from '@allxsmith/bestax-bulma';",
642
642
  "compound": false,
643
643
  "propCount": 29,
644
- "exampleCount": 30
644
+ "exampleCount": 31
645
645
  },
646
646
  {
647
647
  "name": "Kbd",
@@ -1136,7 +1136,7 @@
1136
1136
  "import": "import { Theme } from '@allxsmith/bestax-bulma';",
1137
1137
  "compound": false,
1138
1138
  "propCount": 43,
1139
- "exampleCount": 12
1139
+ "exampleCount": 13
1140
1140
  },
1141
1141
  {
1142
1142
  "name": "TimeInput",
@@ -127,6 +127,7 @@
127
127
  ],
128
128
  "extraProps": [],
129
129
  "catchAll": "All standard <input> attributes and Bulma helper props",
130
+ "helperPropsNote": "Helper props go on the outer `<label>`, with `className`. The other attributes go on the `<input>`.",
130
131
  "types": []
131
132
  }
132
133
  ],
@@ -242,6 +242,7 @@
242
242
  ],
243
243
  "extraProps": [],
244
244
  "catchAll": "All standard <input> attributes and Bulma helper props",
245
+ "helperPropsNote": "Helper props go on the outer `<label>`, with `className`. The other attributes go on the `<input>`.",
245
246
  "types": []
246
247
  }
247
248
  ],
@@ -298,6 +298,7 @@
298
298
  ],
299
299
  "extraProps": [],
300
300
  "catchAll": "All standard <input> attributes and Bulma helper props",
301
+ "helperPropsNote": "Helper props go on the `.file` wrapper `<div>`, with `className`. The other attributes go on the `<input>`.",
301
302
  "types": []
302
303
  }
303
304
  ],
@@ -49,6 +49,10 @@
49
49
  "title": "Help Text Colors",
50
50
  "code": "<>\n <Input\n label=\"Username\"\n color=\"success\"\n value=\"bulma\"\n message=\"This username is available\"\n messageColor=\"success\"\n iconLeftName=\"user\"\n iconRightName=\"check\"\n onChange={() => {}}\n />\n <Input\n label=\"Email\"\n type=\"email\"\n color=\"danger\"\n value=\"hello@\"\n message=\"This email is invalid\"\n messageColor=\"danger\"\n iconLeftName=\"envelope\"\n iconRightName=\"exclamation-triangle\"\n onChange={() => {}}\n />\n</>"
51
51
  },
52
+ {
53
+ "title": "Visually Hidden Label for Screen Readers",
54
+ "code": "<Input\n label=\"Email\"\n labelProps={{ className: 'is-sr-only' }}\n type=\"email\"\n placeholder=\"you@example.com\"\n/>"
55
+ },
52
56
  {
53
57
  "title": "Form Addons › Input + Button",
54
58
  "code": "<Field hasAddons>\n <Control isExpanded>\n <Input placeholder=\"Find a repository\" aria-label=\"Find a repository\" />\n </Control>\n <Control>\n <Button color=\"info\">Search</Button>\n </Control>\n</Field>"
@@ -39,11 +39,11 @@
39
39
  },
40
40
  {
41
41
  "title": "Compound (dot-notation) usage › Modal.Card with compound components",
42
- "code": "function example() {\n const [open, setOpen] = useState(false);\n const closeModal = () => setOpen(false);\n\n return (\n <>\n <Button color=\"success\" onClick={() => setOpen(true)}>\n Show Modal\n </Button>\n <Modal isActive={open} onClose={closeModal}>\n <Modal.Background onClick={closeModal} />\n <Modal.Card>\n <Modal.Card.Head>\n <Modal.Card.Title>Compound Component Modal</Modal.Card.Title>\n <Modal.Close />\n </Modal.Card.Head>\n <Modal.Card.Body>\n This modal uses the compound components API for better control and\n readability.\n </Modal.Card.Body>\n <Modal.Card.Foot>\n <Buttons>\n <Button color=\"success\" onClick={closeModal}>\n Save\n </Button>\n <Button onClick={closeModal}>Cancel</Button>\n </Buttons>\n </Modal.Card.Foot>\n </Modal.Card>\n </Modal>\n </>\n );\n}"
42
+ "code": "function example() {\n const [open, setOpen] = useState(false);\n const closeModal = () => setOpen(false);\n\n return (\n <>\n <Button color=\"success\" onClick={() => setOpen(true)}>\n Show Modal\n </Button>\n <Modal\n isActive={open}\n onClose={closeModal}\n aria-labelledby=\"compound-modal-title\"\n >\n <Modal.Background onClick={closeModal} />\n <Modal.Card>\n <Modal.Card.Head>\n <Modal.Card.Title id=\"compound-modal-title\">\n Compound Component Modal\n </Modal.Card.Title>\n <Modal.Close />\n </Modal.Card.Head>\n <Modal.Card.Body>\n This modal uses the compound components API for better control and\n readability.\n </Modal.Card.Body>\n <Modal.Card.Foot>\n <Buttons>\n <Button color=\"success\" onClick={closeModal}>\n Save\n </Button>\n <Button onClick={closeModal}>Cancel</Button>\n </Buttons>\n </Modal.Card.Foot>\n </Modal.Card>\n </Modal>\n </>\n );\n}"
43
43
  },
44
44
  {
45
45
  "title": "Compound (dot-notation) usage › Modal.Content with compound components",
46
- "code": "function example() {\n const [open, setOpen] = useState(false);\n const closeModal = () => setOpen(false);\n\n return (\n <>\n <Button color=\"success\" onClick={() => setOpen(true)}>\n Show Modal\n </Button>\n <Modal isActive={open} onClose={closeModal}>\n <Modal.Background onClick={closeModal} />\n <Modal.Content>\n <Box>\n <Title size=\"4\">Custom Content</Title>\n <Paragraph>Using compound components for full control!</Paragraph>\n </Box>\n </Modal.Content>\n <Modal.Close variant=\"floating\" />\n </Modal>\n </>\n );\n}"
46
+ "code": "function example() {\n const [open, setOpen] = useState(false);\n const closeModal = () => setOpen(false);\n\n return (\n <>\n <Button color=\"success\" onClick={() => setOpen(true)}>\n Show Modal\n </Button>\n <Modal\n isActive={open}\n onClose={closeModal}\n aria-labelledby=\"custom-content-title\"\n >\n <Modal.Background onClick={closeModal} />\n <Modal.Content>\n <Box>\n <Title size=\"4\" id=\"custom-content-title\">\n Custom Content\n </Title>\n <Paragraph>Using compound components for full control!</Paragraph>\n </Box>\n </Modal.Content>\n <Modal.Close variant=\"floating\" />\n </Modal>\n </>\n );\n}"
47
47
  }
48
48
  ],
49
49
  "accessibility": "- The modal root gets `role=\"dialog\"` and `aria-modal=\"true\"` while active — pass an explicit `role` to override (e.g. `Dialog` passes `role=\"presentation\"` on the `Modal` it wraps so its own `role=\"alertdialog\"` is the only one announced).\n- **Give the dialog an accessible name.** With the legacy API, `modalCardTitle` is wired up as `aria-labelledby` for you. With the compound API, pass your own `aria-label` or `aria-labelledby` on `<Modal>` — a `role=\"dialog\"` with no name is announced as just \"dialog\". An explicit `aria-labelledby` always wins over the generated one.\n- Escape closes the modal (calls `onClose`) by default; opt out with `closeOnEscape={false}`. One Escape closes the topmost overlay only, whichever open Modal, [Dialog](https://bestax.io/docs/api/components/dialog), [Sidebar](https://bestax.io/docs/api/components/sidebar) or full-page [Loading](https://bestax.io/docs/api/components/loading) opened last, and the next Escape closes the one under it. A modal on top with `closeOnEscape={false}` still keeps Escape from the layers under it. A [Toast](https://bestax.io/docs/api/components/toast) waits until no overlay is open.\n- The modal calls `preventDefault()` on the Escape it closes on, so a listener of your own that runs after it can tell the key was used.\n- A popup inside the modal takes its own Escape first. With an Autocomplete list, a Dropdown menu, a Popover or a date picker's calendar open, the first Escape closes that and the next one closes the modal.\n- A popup of your own inside a modal should call `preventDefault()` on the Escape it closes on. The modal leaves an Escape that was prevented alone, whether your popup hears it in a React `onKeyDown` or in a listener on `document`. Call it only when the popup is open and the key was pressed in it, so an Escape with nothing to close still closes the modal, and so does one pressed in a modal opened over your popup.\n- Body scroll is locked while the modal is active; opt out with `lockScroll={false}`. The lock is ref-counted and shared with `Dialog`, `Sidebar` and `Loading`, so whichever overlay closes first doesn't unlock the page underneath one that is still open.\n- Focus moves to the first tab stop (or the modal itself) on open, and is restored to the previously focused element on close — unless another modal is still open on top, in which case that one keeps focus.\n- Tab and Shift+Tab cycle within the modal while it is the topmost modal overlay, so the keyboard order agrees with what `aria-modal=\"true\"` tells assistive technology. They wrap at the same tab stops [`useFocusTrap`](https://bestax.io/docs/api/helpers/usefocustrap) does, so disabled, hidden and `tabIndex={-1}` elements are skipped, and so is inert content, such as a closed [Sidebar](https://bestax.io/docs/api/components/sidebar) inside the modal, and the content of a closed `<details>`. A radio group counts once. A Tab from outside the modal brings focus back in, and so does a Tab the browser takes out anyway, as Safari does past links when its settings say to. With another Modal or Dialog, a [Sidebar](https://bestax.io/docs/api/components/sidebar) with an overlay or a full-page Loading opened over it, the modal leaves Tab alone until that one closes. A Sidebar without an overlay isn't modal: opened over the modal, it takes Escape first, but the modal keeps holding Tab and keeps focus, so the sidebar doesn't take focus when it opens, and a Tab from its controls comes back into the modal. A focus trap of your own inside it, such as a portaled panel's, keeps a Tab it calls `preventDefault()` on, and the modal leaves focus where your trap put it. Note that `aria-modal` does not by itself make background content inert, and no `inert` attribute is applied, so background content remains reachable by pointer and by browser UI such as find-in-page.\n- In the props form, the background closes the modal on click, and a close button is provided for both modal-card and modal-content variants (both call `onClose`, so pass it).\n- In the compound form, `Modal.Close` calls the modal's `onClose` unless you give it an `onClick` of your own, and `Modal.Background` closes the modal only when you pass it `onClick={onClose}`.\n\n:::note\nAlways provide an `onClose` handler for accessibility and to allow users to dismiss the modal.\n:::",
@@ -314,6 +314,7 @@
314
314
  }
315
315
  ],
316
316
  "catchAll": "All standard <input> attributes and Bulma helper props",
317
+ "helperPropsNote": "Helper props go on the outer `.numberinput` `<div>`, with `className`. The other attributes go on the `<input>`.",
317
318
  "types": []
318
319
  }
319
320
  ],
@@ -296,6 +296,7 @@
296
296
  }
297
297
  ],
298
298
  "catchAll": "All standard <input> attributes and Bulma helper props",
299
+ "helperPropsNote": "Helper props go on the `.pin-input` `<div>`, the row of boxes, with `className`. The other attributes go on the `<input>` that holds the code.",
299
300
  "types": []
300
301
  }
301
302
  ],
@@ -110,6 +110,7 @@
110
110
  ],
111
111
  "extraProps": [],
112
112
  "catchAll": "All standard <input> attributes and Bulma helper props",
113
+ "helperPropsNote": "Helper props go on the outer `<label>`, with `className`. The other attributes go on the `<input>`.",
113
114
  "types": []
114
115
  }
115
116
  ],
@@ -233,6 +233,7 @@
233
233
  ],
234
234
  "extraProps": [],
235
235
  "catchAll": "All standard <input> attributes and Bulma helper props",
236
+ "helperPropsNote": "Helper props go on the outer `<label>`, with `className`. The other attributes go on the `<input>`.",
236
237
  "types": []
237
238
  }
238
239
  ],
@@ -348,6 +348,7 @@
348
348
  ],
349
349
  "extraProps": [],
350
350
  "catchAll": "All standard <select> attributes and Bulma helper props",
351
+ "helperPropsNote": "Helper props go on the `.select` wrapper `<div>`, with `className`. The other attributes go on the `<select>`.",
351
352
  "types": []
352
353
  },
353
354
  {
@@ -494,6 +495,7 @@
494
495
  ],
495
496
  "extraProps": [],
496
497
  "catchAll": "All standard <select> attributes and Bulma helper props",
498
+ "helperPropsNote": "Helper props go on the `.select` wrapper `<div>`, with `className`. The other attributes go on the `<select>`.",
497
499
  "types": [],
498
500
  "standalone": "base"
499
501
  }
@@ -372,6 +372,7 @@
372
372
  }
373
373
  ],
374
374
  "catchAll": "All standard <input> attributes and Bulma helper props",
375
+ "helperPropsNote": "Helper props go on the `.slider` wrapper `<div>`, with `className`. The other attributes go on the `<input>`, the low thumb's in range mode.",
375
376
  "types": []
376
377
  }
377
378
  ],
@@ -203,6 +203,7 @@
203
203
  }
204
204
  ],
205
205
  "catchAll": "All standard <input> attributes and Bulma helper props",
206
+ "helperPropsNote": "Helper props go on the outer `<label>`, with `className`. The other attributes go on the `<input>`.",
206
207
  "types": []
207
208
  }
208
209
  ],
@@ -17,6 +17,10 @@
17
17
  "title": "Dark Mode",
18
18
  "code": "// Pairs with an inline <head> script in index.html that writes data-theme\n// before the first paint: the choice saved under STORAGE_KEY, or else the\n// OS preference. The Theme API page's Dark Mode section has the script.\nimport { useState } from 'react';\nimport { Theme, ToggleButton } from '@allxsmith/bestax-bulma';\n\ntype ColorMode = 'light' | 'dark';\n\n// The key the index.html script reads the saved choice from.\nconst STORAGE_KEY = 'theme';\n\n// The scheme the index.html script wrote on <html>.\nfunction initialColorMode(): ColorMode {\n if (typeof document === 'undefined') return 'light';\n return document.documentElement.getAttribute('data-theme') === 'dark'\n ? 'dark'\n : 'light';\n}\n\nfunction App() {\n const [mode, setMode] = useState<ColorMode>(initialColorMode);\n\n const setDark = (dark: boolean) => {\n const next: ColorMode = dark ? 'dark' : 'light';\n setMode(next);\n try {\n localStorage.setItem(STORAGE_KEY, next);\n } catch {\n // Storage is blocked, so the choice lasts until the page reloads.\n }\n };\n\n return (\n <Theme isRoot colorMode={mode}>\n <ToggleButton pressed={mode === 'dark'} onPressedChange={setDark}>\n Dark mode\n </ToggleButton>\n </Theme>\n );\n}"
19
19
  },
20
+ {
21
+ "title": "Label and Text Lightnesses",
22
+ "code": "function StockTurquoise() {\n // Stock Bulma's turquoise, with the values from bulma/css/bulma.css.\n return (\n <Theme\n isRoot\n primaryH=\"171deg\"\n primaryS=\"100%\"\n primaryL=\"41%\"\n bulmaVars={{\n '--bulma-primary-invert-l': 'var(--bulma-primary-05-l)',\n '--bulma-primary-on-scheme-l': { light: '21%', dark: '41%' },\n }}\n >\n <Button color=\"primary\">Dark label</Button>\n <Block textColor=\"primary\" colorShade=\"on-scheme\">\n Turquoise text that reads in both schemes\n </Block>\n </Theme>\n );\n}"
23
+ },
20
24
  {
21
25
  "title": "Local Theme (Component Level)",
22
26
  "code": "function LocalTheme() {\n return (\n <div>\n <Button color=\"primary\">Standard Primary</Button>\n\n <Theme primaryH=\"120\" primaryS=\"100%\" primaryL=\"40%\">\n <Button color=\"primary\">Green Primary</Button>\n </Theme>\n\n <Button color=\"primary\">Standard Primary Again</Button>\n </div>\n );\n}"
@@ -89,7 +93,7 @@
89
93
  "name": "isRoot",
90
94
  "type": "boolean",
91
95
  "default": "false",
92
- "description": "Inject the variables globally at `:root` instead of scoping them to a wrapper div. Default: false. A root Theme renders its children with no wrapper, so `className` and the helper props, such as `m` or `shadow`, have no element to land on. They do nothing, and say so in development. `radius` and `columnGap` describe what each does at the root. Several root Themes can be mounted at once, and each contributes its own variables. Where two set the same one, the inner or later-mounted Theme wins, as with nested scoped Themes, and unmounting a Theme removes only what it contributed. The variables are written from an effect, so a server render does not include them.",
96
+ "description": "Inject the variables globally at `:root` instead of scoping them to a wrapper div. Default: false. A root Theme renders its children with no wrapper, so `className` and the helper props, such as `m` or `shadow`, have no element to land on. They do nothing, and say so in development. `radius` and `columnGap` describe what each does at the root. Several root Themes can be mounted at once, and each contributes its own variables. Where two set the same one, the inner or later-mounted Theme wins, as with nested scoped Themes, and unmounting a Theme removes only what it contributed. The variables are written before the browser paints. A server render has none of them, so put what a server-rendered page needs at first paint in CSS.",
93
97
  "required": false,
94
98
  "inherited": false,
95
99
  "deprecated": false
@@ -98,7 +102,7 @@
98
102
  "name": "colorMode",
99
103
  "type": "'light' | 'dark' | 'system'",
100
104
  "default": null,
101
- "description": "Set Bulma's light/dark scheme by writing its theme attribute on the document root (`<html>`). This is always global, even on a scoped Theme. `'system'` removes the attribute so Bulma follows the OS `prefers-color-scheme`. Omit to leave the current setting untouched. The attribute is `data-theme`. A stylesheet built with a class prefix reads Bulma's prefixed form instead (`data-bestax-theme` for the `bestax-prefixed` builds), so under a `ConfigProvider` `classPrefix` Theme writes both, and `'system'` or unmounting clears or restores both. If the prefix cannot form an attribute name, `'light'` and `'dark'` write only `data-theme` and warn in development. The attribute is written from an effect, so a page rendered in the browser paints its first frame with whatever `<html>` already has, the visitor's OS scheme when that is nothing. Put a scheme that never changes on `<html>` in `index.html` instead, and write a saved choice from an inline `<head>` script, starting `colorMode` from what it wrote.",
105
+ "description": "Set Bulma's light/dark scheme by writing its theme attribute on the document root (`<html>`). This is always global, even on a scoped Theme. `'system'` removes the attribute so Bulma follows the OS `prefers-color-scheme`. Omit to leave the current setting untouched. The attribute is `data-theme`. A stylesheet built with a class prefix reads Bulma's prefixed form instead (`data-bestax-theme` for the `bestax-prefixed` builds), so under a `ConfigProvider` `classPrefix` Theme writes both, and `'system'` or unmounting clears or restores both. If the prefix cannot form an attribute name, `'light'` and `'dark'` write only `data-theme` and warn in development. It is written before the browser paints, but the page can paint before the app's script runs, in the scheme `<html>` already has. Put a fixed scheme on `<html>` in `index.html`, and write a saved choice from an inline `<head>` script, starting `colorMode` from it.",
102
106
  "required": false,
103
107
  "inherited": false,
104
108
  "deprecated": false
@@ -107,7 +111,7 @@
107
111
  "name": "bulmaVars",
108
112
  "type": "BulmaVars",
109
113
  "default": "{}",
110
- "description": "Bulma CSS variables to set, by name, such as `{ '--bulma-primary-h': '210' }`. Only Bulma's own variables, the keys the `BulmaVars` type allows, are applied, and an empty value is skipped. Where a variable prop, such as `primaryH`, sets the same variable, the prop wins. Theme writes them on its wrapper, or at `:root` under `isRoot`, so only a variable that inherits from there changes anything. Bulma declares many component variables, such as `--bulma-card-radius` or `--bulma-delete-dimensions`, on the component's own element, and that declaration beats any value set further up. Those keys are accepted but do nothing here, and warn in development. Set them on the component itself, with `className` or `style`, or through Sass. Each component's API page says where Bulma declares its variables. Some global variables Bulma also declares again on one component, which keeps its own value while everything else takes the Theme's. Those do not warn, because the value still reaches the rest of the page.",
114
+ "description": "Bulma CSS variables to set, by name, such as `{ '--bulma-primary-h': '210' }`. Only Bulma's own variables, the keys the `BulmaVars` type allows, are applied, and an empty value is skipped. Where a variable prop, such as `primaryH`, sets the same variable, the prop wins. Theme writes them on its wrapper, or at `:root` under `isRoot`, so only a variable that inherits from there changes anything. Bulma declares many component variables, such as `--bulma-card-radius` or `--bulma-delete-dimensions`, on the component's own element, and that declaration beats any value set further up. Those keys are accepted but do nothing here, and warn in development. Set them on the component itself, with `className` or `style`, or through Sass. Each component's API page says where Bulma declares its variables. Some global variables Bulma also declares again on one component, which keeps its own value while everything else takes the Theme's. Those do not warn, because the value still reaches the rest of the page. A color's trio leaves its label (`--bulma-<color>-invert-l`) and text (`--bulma-<color>-on-scheme-l`) lightnesses where the stylesheet's build put them, so a new color sets them too. On an `isRoot` Theme the on-scheme keys also take `{ light, dark }`, one value per scheme, which a scoped Theme skips with a warning in development.",
111
115
  "required": false,
112
116
  "inherited": false,
113
117
  "deprecated": false
@@ -467,5 +471,5 @@
467
471
  }
468
472
  ],
469
473
  "cssVars": [],
470
- "doc": "# Theme\n\n## Overview\n\nThe `Theme` component provides a powerful way to customize Bulma's appearance using CSS custom properties (CSS variables). It allows you to override Bulma's design tokens like colors, spacing, typography, and other visual properties either globally or locally within specific component trees. The Theme component supports both CSS variable injection via props and direct CSS variable objects.\n\n---\n\n## Import\n\n```tsx\nimport { Theme } from '@allxsmith/bestax-bulma';\n```\n\n---\n\n## Usage\n\n### Basic Theme Customization\n\n```tsx\nfunction BasicThemeCustomization() {\n return (\n <Theme primaryH=\"270\" primaryS=\"100%\" primaryL=\"50%\">\n <Box p=\"4\">\n <Button color=\"primary\">Purple Primary Button</Button>\n </Box>\n </Theme>\n );\n}\n```\n\n### Global Theme (Root Level)\n\n```tsx\nfunction GlobalTheme() {\n return (\n <Theme isRoot={true} primaryH=\"270\" primaryS=\"100%\" primaryL=\"50%\">\n <div>\n <Button color=\"primary\">Global Purple Theme</Button>\n <Button color=\"info\">Info Button</Button>\n </div>\n </Theme>\n );\n}\n```\n\n### Dark Mode\n\nThe `colorMode` prop sets Bulma's `data-theme` on `<html>`, flipping the light/dark scheme globally.\nUse `'system'` to follow the OS preference.\n\nA prefixed stylesheet reads Bulma's prefixed attribute instead (`data-bestax-theme` for\n`bestax-prefixed.css` and `bestax-no-helpers-prefixed.css`). Under a `ConfigProvider`\n`classPrefix`, `colorMode` writes that attribute as well as `data-theme`, so it works with the\nprefixed builds.\n\n:::warning Single-mode designs should pin the scheme\n\nOmitting `colorMode` preserves whatever `data-theme` is already set — and when nothing has set\none (the usual case), the scheme follows the visitor's OS: Bulma's text colors flip on a\ndark-mode machine even if your design is light-only, breaking contrast against any fixed custom\nbackgrounds. If you support only one mode, pin it on the `<html>` in your `index.html`\n(`<html lang=\"en\" data-theme=\"light\">`); if you support both, don't hardcode surface/text\ncolors — see [Dark Mode & Contrast](https://bestax.io/docs/guides/features/css-variables#dark-mode--contrast).\n\n:::\n\n`colorMode` writes the attribute from an effect, so a page rendered in the browser paints its\nfirst frame with whatever `<html>` already has, which is why a fixed scheme belongs in\n`index.html`. A toggle that remembers the visitor's choice has two parts. First, an inline script\nin the `<head>` of `index.html` writes the saved choice, or else the OS preference, before the\nfirst paint. Make it a plain `<script>`, not `type=\"module\"`, so it runs before the body renders.\nWith a prefixed stylesheet, have it set `data-bestax-theme` as well.\n\n```html\n<script>\n // Bulma's scheme on <html> before the first paint: the saved choice,\n // or else the OS preference. The app saves under the same key.\n (() => {\n let saved = null;\n try {\n saved = localStorage.getItem('theme');\n } catch {\n // Storage is blocked, so fall back to the OS preference.\n }\n const dark =\n saved === 'light' || saved === 'dark'\n ? saved === 'dark'\n : window.matchMedia('(prefers-color-scheme: dark)').matches;\n document.documentElement.setAttribute(\n 'data-theme',\n dark ? 'dark' : 'light'\n );\n })();\n</script>\n```\n\nSecond, the app starts `colorMode` from the attribute the script wrote, and saves each choice\nunder the key the script reads. The `bestax-theming` skill's\n[dark-mode example](https://github.com/allxsmith/bestax/blob/main/skills/bestax-theming/examples/dark-mode.tsx)\nis the same recipe, and also covers a page that has no script.\n\n```tsx\n// Pairs with an inline <head> script in index.html that writes data-theme\n// before the first paint: the choice saved under STORAGE_KEY, or else the\n// OS preference. The Theme API page's Dark Mode section has the script.\nimport { useState } from 'react';\nimport { Theme, ToggleButton } from '@allxsmith/bestax-bulma';\n\ntype ColorMode = 'light' | 'dark';\n\n// The key the index.html script reads the saved choice from.\nconst STORAGE_KEY = 'theme';\n\n// The scheme the index.html script wrote on <html>.\nfunction initialColorMode(): ColorMode {\n if (typeof document === 'undefined') return 'light';\n return document.documentElement.getAttribute('data-theme') === 'dark'\n ? 'dark'\n : 'light';\n}\n\nfunction App() {\n const [mode, setMode] = useState<ColorMode>(initialColorMode);\n\n const setDark = (dark: boolean) => {\n const next: ColorMode = dark ? 'dark' : 'light';\n setMode(next);\n try {\n localStorage.setItem(STORAGE_KEY, next);\n } catch {\n // Storage is blocked, so the choice lasts until the page reloads.\n }\n };\n\n return (\n <Theme isRoot colorMode={mode}>\n <ToggleButton pressed={mode === 'dark'} onPressedChange={setDark}>\n Dark mode\n </ToggleButton>\n </Theme>\n );\n}\n```\n\n### Local Theme (Component Level)\n\n```tsx\nfunction LocalTheme() {\n return (\n <div>\n <Button color=\"primary\">Standard Primary</Button>\n\n <Theme primaryH=\"120\" primaryS=\"100%\" primaryL=\"40%\">\n <Button color=\"primary\">Green Primary</Button>\n </Theme>\n\n <Button color=\"primary\">Standard Primary Again</Button>\n </div>\n );\n}\n```\n\n### Using CSS Variables Object\n\n```tsx\nfunction SunsetTheme() {\n const sunsetTheme = {\n '--bulma-scheme-h': '18',\n '--bulma-scheme-s': '90%',\n '--bulma-light-l': '85%',\n '--bulma-dark-l': '20%',\n '--bulma-primary-h': '25',\n '--bulma-primary-s': '85%',\n '--bulma-primary-l': '55%',\n };\n\n return (\n <Theme bulmaVars={sunsetTheme} isRoot>\n <Box p=\"4\">\n <Title>Sunset Theme</Title>\n <Button color=\"primary\">Sunset Button</Button>\n <Button color=\"info\">Info Button</Button>\n </Box>\n </Theme>\n );\n}\n```\n\n### Advanced CSS Variables Usage\n\nBulma derives the body font and the control radius from these variables at `:root`, so set\ntypography and radius on an `isRoot` theme.\n\n```tsx\nfunction TypographyTheme() {\n const typographyTheme = {\n '--bulma-family-primary': '\"Helvetica Neue\", sans-serif',\n '--bulma-family-code': '\"Fira Code\", monospace',\n '--bulma-size-normal': '16px',\n '--bulma-weight-bold': '700',\n };\n\n return (\n <Theme isRoot bulmaVars={typographyTheme}>\n <Title>Custom Typography</Title>\n <SubTitle>With custom fonts and weights</SubTitle>\n </Theme>\n );\n}\n```\n\n```tsx\nfunction RadiusTheme() {\n // `radius` is the border radius helper, as on every component, so the radius scale goes through bulmaVars\n const radii = {\n '--bulma-radius-small': '6px',\n '--bulma-radius': '12px',\n '--bulma-radius-medium': '16px',\n '--bulma-radius-large': '20px',\n };\n\n return (\n <Theme isRoot bulmaVars={radii}>\n <Button color=\"primary\">Rounder Button</Button>\n </Theme>\n );\n}\n```\n\nSpacing that Bulma reads where it is used, such as the block and column gaps, works on a scoped\ntheme:\n\n```tsx\nfunction SpacingTheme() {\n const spacingTheme = {\n '--bulma-block-spacing': '2rem',\n '--bulma-column-gap': '1rem',\n };\n\n return (\n <Theme bulmaVars={spacingTheme}>\n <Columns>\n <Column>\n <Box>Wider column gap</Box>\n <Box>Wider block spacing</Box>\n </Column>\n <Column>\n <Box>Wider column gap</Box>\n </Column>\n </Columns>\n </Theme>\n );\n}\n```\n\n### Complete Color Scheme\n\n```tsx\nfunction ForestTheme() {\n const forestScheme = {\n schemeH: '150', // forest green hue\n schemeS: '50%',\n lightL: '80%',\n lightInvertL: '20%',\n darkL: '18%',\n darkInvertL: '85%',\n softL: '55%',\n boldL: '35%',\n primaryH: '160',\n primaryS: '60%',\n primaryL: '45%',\n linkH: '155',\n linkS: '65%',\n linkL: '40%',\n successH: '120',\n successS: '70%',\n successL: '45%',\n warningH: '45',\n warningS: '85%',\n warningL: '55%',\n dangerH: '355',\n dangerS: '75%',\n dangerL: '50%',\n hoverBackgroundLDelta: '4%',\n activeBackgroundLDelta: '8%',\n };\n\n return (\n <Theme {...forestScheme} isRoot>\n <Box p=\"4\">\n <Title>Forest Theme</Title>\n <Button color=\"primary\">Forest Primary</Button>\n <Button color=\"success\">Success</Button>\n <Button color=\"info\">Info</Button>\n <Button color=\"warning\">Warning</Button>\n <Button color=\"danger\">Danger</Button>\n </Box>\n </Theme>\n );\n}\n```\n\n### Theme with Styling\n\n```tsx\nfunction StyledTheme() {\n return (\n <Theme\n className=\"custom-theme-wrapper\"\n primaryH=\"45\"\n primaryS=\"100%\"\n primaryL=\"50%\"\n p=\"5\"\n m=\"3\"\n textAlign=\"centered\"\n >\n <Title>Styled Theme Container</Title>\n <Button color=\"primary\">Styled Button</Button>\n </Theme>\n );\n}\n```\n\n### Nested Themes\n\n```tsx\nfunction NestedThemes() {\n return (\n <Theme primaryH=\"210\" primaryS=\"60%\" primaryL=\"45%\" p=\"4\">\n <Title>Outer Theme (Blue)</Title>\n <Button color=\"primary\">Blue Primary</Button>\n\n <Theme primaryH=\"120\" primaryS=\"70%\" primaryL=\"40%\" p=\"3\" mt=\"4\">\n <Title size=\"4\">Inner Theme (Green)</Title>\n <Button color=\"primary\">Green Primary</Button>\n </Theme>\n\n <Button color=\"primary\" mt=\"3\">\n Blue Primary Again\n </Button>\n </Theme>\n );\n}\n```\n\n### Theme with ConfigProvider\n\n```tsx\nfunction PrefixedTheme() {\n return (\n <ConfigProvider classPrefix=\"bestax-\">\n <Theme primaryH=\"300\" primaryS=\"80%\" primaryL=\"50%\" isRoot>\n <Button color=\"primary\">Prefixed & Themed Button</Button>\n </Theme>\n </ConfigProvider>\n );\n}\n```\n\n---\n\n## Best Practices\n\n### Global vs Local Themes\n\n- Use `isRoot={true}` for application-wide themes that should affect all components\n- Use local themes (default `isRoot={false}`) for component-specific styling or theme variations\n- Local themes inherit from parent themes and can override specific variables\n- Several `isRoot` themes can be mounted at once, such as a color theme at the app root and a\n typography theme next to it. Each adds its own variables at `:root`; where two set the same\n variable, the inner or later-mounted one wins, as with nested local themes, and unmounting one\n removes only its own variables\n\n### CSS Variable Naming\n\n- All Bulma CSS variables follow the `--bulma-*` naming convention\n- Named props exist for the scheme and color variables only (like `primaryH`); every other variable, typography and radius included, goes through `bulmaVars`\n- `bulmaVars` keys are the full variable names (`'--bulma-family-primary'`), not camelCase\n- `bulmaVars` also accepts the scheme surface variables — `--bulma-scheme-main`, `--bulma-scheme-main-bis`, `--bulma-scheme-main-ter`, `--bulma-scheme-invert`, `--bulma-scheme-invert-bis`, `--bulma-scheme-invert-ter` — so overriding `--bulma-scheme-main-bis`/`-ter` re-tints every scheme-background band (e.g., `<Section bgColor=\"scheme-main-bis\">`) at once\n- `bulmaVars` also accepts `--bulma-shadow` (plus `--bulma-shadow-h`/`-s`/`-l`) — the upstream token that `.box`, `.card`, `.dropdown`, and `.panel` derive their own shadow variables from. Those selectors re-declare their derived variable (e.g. `--bulma-box-shadow`) on themselves, so setting it from an ancestor `Theme` is only inherited and never wins; overriding `--bulma-shadow` itself is the one override that cascades. There is no individual `shadow` prop for this — `shadow` already names the unrelated `shadowless` helper class — so reach it through `bulmaVars`\n\n### Performance Considerations\n\n- Prefer setting themes at higher levels in your component tree rather than deeply nested\n- CSS variables are inherited, so child themes only need to override specific variables\n\n### Color System\n\n- Bulma uses HSL (Hue, Saturation, Lightness) for its color system\n- Hue values range from 0-360 (color wheel degrees)\n- Saturation and Lightness are typically percentages (e.g., '50%')\n- The scheme variables control the base color relationships\n\n---\n\n## API Reference\n\n### Named Props and `bulmaVars`\n\n`ThemeProps` has a named, camelCase prop for each scheme and color variable, listed under\n[CSS Variable Props](https://bestax.io/docs/api/helpers/theme#css-variable-props). Each converts to its CSS variable by the same rule:\n\n```\npropName → --bulma-prop-name\n```\n\n- `primaryH` → `--bulma-primary-h`\n- `schemeS` → `--bulma-scheme-s`\n- `lightL` → `--bulma-light-l`\n- `hoverBackgroundLDelta` → `--bulma-hover-background-l-delta`\n\nEvery other variable (typography, radius, spacing, and the rest) has no named prop in\n`ThemeProps` and goes through `bulmaVars`, keyed by its full `--bulma-*` name. JavaScript code\nthat passes one as a camelCase prop (`familyPrimary`) may still render, but it is outside the\ntyped API, so move it to `bulmaVars`:\n\n```tsx\n<Theme\n isRoot\n primaryH=\"350\"\n primaryS=\"73%\"\n primaryL=\"44%\"\n bulmaVars={{\n '--bulma-radius': '2px',\n '--bulma-radius-small': '1px',\n '--bulma-family-primary': '\"IBM Plex Sans\", system-ui, sans-serif',\n '--bulma-family-code': '\"IBM Plex Mono\", ui-monospace, monospace',\n }}\n>\n <App />\n</Theme>\n```\n\nSome of those names are already helper props, so they cannot double as CSS-variable props:\n\n- `radius` is the border radius helper, as on every other component, so `<Theme radius=\"radiusless\">`\n adds `is-radiusless` to the wrapper. On a Theme it also sets `--bulma-radius` to `0`, so what\n is inside the Theme loses its radius too; under `isRoot` it writes that at `:root`, squaring\n everything on the page that takes its radius from it. The sizes (`small`, `normal`, `large`,\n `rounded`) add their `has-radius-*` class to the wrapper and set no variable, so they round\n the wrapper and leave what is inside alone; under `isRoot` there is no wrapper, so they do\n nothing and warn in development. Set any other `--bulma-radius` through\n `bulmaVars`. `<Theme radius=\"2px\" />` is a type error; in JavaScript it still sets\n `--bulma-radius`, which is how the prop used to behave, but that route is deprecated and logs\n a warning in development.\n- `shadow` is the `shadowless` helper. Set `--bulma-shadow` through `bulmaVars`.\n- `columnGap` is the column gap helper, as on every other component, so `<Theme columnGap=\"2\">`\n adds `is-column-gap-2` to the wrapper and sets no variable; under `isRoot` there is no wrapper,\n so it does nothing and warns in development. Set the columns gutter, `--bulma-column-gap`,\n through `bulmaVars`. Before the gap helpers, JavaScript could pass `columnGap` to set that\n variable. A string that is not a gap step (`columnGap=\"1rem\"`) still does, through a\n deprecated route that logs a warning in development, but a gap step does not:\n `columnGap=\"0\"`, which used to zero the gutters inside the Theme, now adds `is-column-gap-0`\n to the wrapper.\n\n### Props vs bulmaVars\n\nYou can set the scheme and color variables in two ways:\n\n1. **Named Props** (recommended where one exists):\n\n ```tsx\n <Theme primaryH=\"270\" primaryS=\"100%\" primaryL=\"50%\">\n <App />\n </Theme>\n ```\n\n2. **bulmaVars Object** (for every other variable, or when you have many to set):\n\n ```tsx\n <Theme\n bulmaVars={{\n '--bulma-primary-h': '270',\n '--bulma-primary-s': '100%',\n '--bulma-primary-l': '50%',\n '--bulma-block-spacing': '2rem',\n }}\n >\n <App />\n </Theme>\n ```\n\n3. **Combined** (props take precedence over bulmaVars):\n ```tsx\n <Theme\n primaryH=\"270\" // This takes precedence\n bulmaVars={{\n '--bulma-primary-h': '180', // This is overridden\n '--bulma-block-spacing': '2rem',\n }}\n >\n <App />\n </Theme>\n ```\n\n### TypeScript Support\n\n`bulmaVars` is typed as a partial record keyed by the union of every variable `Theme` accepts.\nWrite it as an object literal and your editor autocompletes the keys and flags a misspelled or\nunsupported one as a type error. A separately declared object only has to share one valid key\nto typecheck, so a typo in it is silently dropped; annotate it with\n`satisfies ThemeProps['bulmaVars']` to keep the check:\n\n```tsx\n<Theme\n primaryH=\"270\"\n bulmaVars={{\n '--bulma-block-spacing': '2rem', // start typing '--bulma-' to list the rest\n }}\n>\n <App />\n</Theme>\n```\n\n---\n\n## See Also\n\n- [ConfigProvider](https://bestax.io/docs/api/helpers/config) - For class prefixing and configuration\n- [useBulmaClasses](https://bestax.io/docs/api/helpers/usebulmaclasses) - For applying Bulma helper classes\n- [Bulma CSS Variables Documentation](https://bulma.io/documentation/features/css-variables/) - Official Bulma CSS variables reference\n\n---\n\n## Props\n\n| Prop | Type | Description |\n| ----------- | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `children` | `ReactNode` | The child components to apply the theme to. |\n| `className` | `string` | Additional CSS classes for the theme wrapper. A root theme has no wrapper, so under `isRoot` it does nothing and warns in development. |\n| `isRoot` | `boolean` | When `true`, applies CSS variables globally at `:root` level. When `false` (default), applies variables only to the wrapper div. Several root themes compose; an inner or later-mounted one wins a variable they share. A root theme renders no wrapper, so `className` and the helper props, such as `m` or `shadow`, do nothing on it and warn in development. |\n| `colorMode` | `'light' \\| 'dark' \\| 'system'` | Sets Bulma's light/dark scheme by writing the `data-theme` attribute on `<html>`, plus `data-<prefix>theme` inside a `ConfigProvider` with a `classPrefix`. If the prefix cannot form an attribute name, `'light'` and `'dark'` write only `data-theme` and warn in development. Always global (even on a scoped `Theme`). `'system'` removes them so Bulma follows the OS `prefers-color-scheme`. Omit to leave the current setting untouched. |\n| `bulmaVars` | `ThemeProps['bulmaVars']` | Object mapping Bulma CSS variable names to string values (e.g., `{'--bulma-primary-h': '210'}`). Keys are limited to the `BulmaVars` type; anything else is not applied. A variable Bulma declares on the component itself changes nothing from here and warns in development; see [Which Variables a Theme Can Change](https://bestax.io/docs/api/helpers/theme#which-variables-a-theme-can-change). |\n| `columnGap` | [`BulmaGapStep`](https://bestax.io/docs/api/helpers/valid-values) | The column gap helper: a gap step adds `is-column-gap-<step>` to the wrapper div and sets no variable, so under `isRoot` it does nothing and warns in development. Set `--bulma-column-gap` through `bulmaVars`. Any other string still sets `--bulma-column-gap`, a deprecated route that warns in development. |\n| `radius` | `'radiusless' \\| 'small' \\| 'normal' \\| 'large' \\| 'rounded'` | The border radius helper. `radiusless` adds `is-radiusless` to the wrapper div and sets `--bulma-radius` to `0`, at `:root` under `isRoot`. The sizes add their `has-radius-*` class to the wrapper and set no variable, so under `isRoot` they do nothing and warn in development. Any other string still sets `--bulma-radius`, a deprecated route that warns in development. |\n\n### CSS Variable Props\n\nThe Theme component accepts individual CSS variable props that correspond to Bulma's design tokens. These props are automatically converted to their corresponding CSS custom properties (e.g., `primaryH` → `--bulma-primary-h`).\n\n#### Scheme Variables\n\n| Prop | Type | Description | CSS Variable |\n| ------------------------ | -------- | ------------------------------------------------- | ----------------------------------- |\n| `schemeH` | `string` | Base hue for the color scheme (0-360) | `--bulma-scheme-h` |\n| `schemeS` | `string` | Base saturation for the color scheme (percentage) | `--bulma-scheme-s` |\n| `lightL` | `string` | Lightness value for light backgrounds | `--bulma-light-l` |\n| `lightInvertL` | `string` | Inverted lightness for light backgrounds | `--bulma-light-invert-l` |\n| `darkL` | `string` | Lightness value for dark backgrounds | `--bulma-dark-l` |\n| `darkInvertL` | `string` | Inverted lightness for dark backgrounds | `--bulma-dark-invert-l` |\n| `softL` | `string` | Lightness value for soft colors | `--bulma-soft-l` |\n| `boldL` | `string` | Lightness value for bold colors | `--bulma-bold-l` |\n| `softInvertL` | `string` | Inverted lightness for soft colors | `--bulma-soft-invert-l` |\n| `boldInvertL` | `string` | Inverted lightness for bold colors | `--bulma-bold-invert-l` |\n| `hoverBackgroundLDelta` | `string` | Lightness delta for hover background states | `--bulma-hover-background-l-delta` |\n| `activeBackgroundLDelta` | `string` | Lightness delta for active background states | `--bulma-active-background-l-delta` |\n| `hoverBorderLDelta` | `string` | Lightness delta for hover border states | `--bulma-hover-border-l-delta` |\n| `activeBorderLDelta` | `string` | Lightness delta for active border states | `--bulma-active-border-l-delta` |\n| `hoverColorLDelta` | `string` | Lightness delta for hover text color states | `--bulma-hover-color-l-delta` |\n| `activeColorLDelta` | `string` | Lightness delta for active text color states | `--bulma-active-color-l-delta` |\n| `hoverShadowADelta` | `string` | Alpha delta for hover shadow states | `--bulma-hover-shadow-a-delta` |\n| `activeShadowADelta` | `string` | Alpha delta for active shadow states | `--bulma-active-shadow-a-delta` |\n\n#### Color Variables\n\n| Prop | Type | Description | CSS Variable |\n| ---------- | -------- | ------------------------------------- | ------------------- |\n| `primaryH` | `string` | Primary color hue (0-360) | `--bulma-primary-h` |\n| `primaryS` | `string` | Primary color saturation (percentage) | `--bulma-primary-s` |\n| `primaryL` | `string` | Primary color lightness (percentage) | `--bulma-primary-l` |\n| `linkH` | `string` | Link color hue (0-360) | `--bulma-link-h` |\n| `linkS` | `string` | Link color saturation (percentage) | `--bulma-link-s` |\n| `linkL` | `string` | Link color lightness (percentage) | `--bulma-link-l` |\n| `infoH` | `string` | Info color hue (0-360) | `--bulma-info-h` |\n| `infoS` | `string` | Info color saturation (percentage) | `--bulma-info-s` |\n| `infoL` | `string` | Info color lightness (percentage) | `--bulma-info-l` |\n| `successH` | `string` | Success color hue (0-360) | `--bulma-success-h` |\n| `successS` | `string` | Success color saturation (percentage) | `--bulma-success-s` |\n| `successL` | `string` | Success color lightness (percentage) | `--bulma-success-l` |\n| `warningH` | `string` | Warning color hue (0-360) | `--bulma-warning-h` |\n| `warningS` | `string` | Warning color saturation (percentage) | `--bulma-warning-s` |\n| `warningL` | `string` | Warning color lightness (percentage) | `--bulma-warning-l` |\n| `dangerH` | `string` | Danger color hue (0-360) | `--bulma-danger-h` |\n| `dangerS` | `string` | Danger color saturation (percentage) | `--bulma-danger-s` |\n| `dangerL` | `string` | Danger color lightness (percentage) | `--bulma-danger-l` |\n\n#### Shadow Variables\n\nReachable only through `bulmaVars` — there is no individual prop, since `shadow` already names\nthe `BulmaOtherProps` `shadowless` helper class. As noted above, `--bulma-shadow` is the one\noverride that cascades into `.box`/`.card`/`.dropdown`/`.panel` shadows.\n\n| CSS Variable | Description |\n| ------------------ | ----------------------------------------------------------------------------------------------------- |\n| `--bulma-shadow-h` | Shadow color hue (0-360) |\n| `--bulma-shadow-s` | Shadow color saturation (percentage) |\n| `--bulma-shadow-l` | Shadow color lightness (percentage) |\n| `--bulma-shadow` | The full shadow value that `.box`/`.card`/`.dropdown`/`.panel` derive their own shadow variables from |\n\n#### Which Variables a Theme Can Change\n\n`bulmaVars` takes any key of the `BulmaVars` type. Write it as an object literal and your editor\nlists them as you type `'--bulma-'`. Not every one of those keys does something from a `Theme`,\nthough, and the reason is where Bulma declares the variable.\n\nA `Theme` writes its variables on its wrapper `div`, or at `:root` under `isRoot`. A component\ninherits them from there only if it declares no value of its own, because an element's own\ndeclaration always beats an inherited one.\n\n- **Global variables reach everything under the Theme.** Bulma declares the scheme, color and\n shadow variables above, and the typography, radius, spacing and timing tokens (such as\n `--bulma-family-primary` and `--bulma-radius`), on `:root`. Set them on a `Theme`, or on any\n ancestor. Some of them Bulma also declares again on one component, which keeps its own value\n while everything else takes the Theme's. That component's API page lists the variable as\n declared on it.\n- **Component variables have to be set on the component.** Where Bulma declares a variable on\n the component's own element, as it does `--bulma-card-radius`, `--bulma-tag-h` and\n `--bulma-delete-dimensions`, a value a `Theme` sets never reaches it. Those keys are still\n accepted, so existing code keeps compiling, but they change nothing, and `Theme` logs a\n warning in development naming them. Set them on the component instead, with\n `className` (a class in your own stylesheet) or the `style` prop, or at build time through\n [Sass](https://bestax.io/docs/guides/features/sass-customization).\n\nEach component's API page lists its variables under **CSS & Sass Variables** and says where Bulma\ndeclares them, so check there before reaching for a `Theme`. The\n[shadow note](https://bestax.io/docs/api/helpers/theme#css-variable-naming) above is the same rule in action: `--bulma-box-shadow` lives on\n`.box`, so the way to change box shadows from a `Theme` is the global `--bulma-shadow` they derive\nfrom.\n\nThe Theme component also supports all [Bulma helper class props](https://bestax.io/docs/api/helpers/usebulmaclasses) for styling the wrapper element."
474
+ "doc": "# Theme\n\n## Overview\n\nThe `Theme` component provides a powerful way to customize Bulma's appearance using CSS custom properties (CSS variables). It allows you to override Bulma's design tokens like colors, spacing, typography, and other visual properties either globally or locally within specific component trees. The Theme component supports both CSS variable injection via props and direct CSS variable objects.\n\n---\n\n## Import\n\n```tsx\nimport { Theme } from '@allxsmith/bestax-bulma';\n```\n\n---\n\n## Usage\n\n### Basic Theme Customization\n\n```tsx\nfunction BasicThemeCustomization() {\n return (\n <Theme primaryH=\"270\" primaryS=\"100%\" primaryL=\"50%\">\n <Box p=\"4\">\n <Button color=\"primary\">Purple Primary Button</Button>\n </Box>\n </Theme>\n );\n}\n```\n\n### Global Theme (Root Level)\n\n```tsx\nfunction GlobalTheme() {\n return (\n <Theme isRoot={true} primaryH=\"270\" primaryS=\"100%\" primaryL=\"50%\">\n <div>\n <Button color=\"primary\">Global Purple Theme</Button>\n <Button color=\"info\">Info Button</Button>\n </div>\n </Theme>\n );\n}\n```\n\nA root theme writes its variables before the browser paints what it rendered, so an app rendered\nin the browser shows them from its first frame of content. A server render doesn't include them,\nso a page rendered on the server paints with the stylesheet's values until it hydrates. Put the\nvalues that page needs from its first paint in your CSS, on `:root`.\n\n### Dark Mode\n\nThe `colorMode` prop sets Bulma's `data-theme` on `<html>`, flipping the light/dark scheme globally.\nUse `'system'` to follow the OS preference.\n\nA prefixed stylesheet reads Bulma's prefixed attribute instead (`data-bestax-theme` for\n`bestax-prefixed.css` and `bestax-no-helpers-prefixed.css`). Under a `ConfigProvider`\n`classPrefix`, `colorMode` writes that attribute as well as `data-theme`, so it works with the\nprefixed builds.\n\n:::warning Single-mode designs should pin the scheme\n\nOmitting `colorMode` preserves whatever `data-theme` is already set — and when nothing has set\none (the usual case), the scheme follows the visitor's OS: Bulma's text colors flip on a\ndark-mode machine even if your design is light-only, breaking contrast against any fixed custom\nbackgrounds. If you support only one mode, pin it on the `<html>` in your `index.html`\n(`<html lang=\"en\" data-theme=\"light\">`); if you support both, don't hardcode surface/text\ncolors — see [Dark Mode & Contrast](https://bestax.io/docs/guides/features/css-variables#dark-mode--contrast).\n\n:::\n\n`colorMode` writes the attribute before the browser paints what `Theme` rendered, but the page can\npaint before the app's script runs at all, with whatever `<html>` already has, which is why a\nfixed scheme belongs in `index.html`. A toggle that remembers the visitor's choice has two parts.\nFirst, an inline script in the `<head>` of `index.html` writes the saved choice, or else the OS\npreference, before the first paint. Make it a plain `<script>`, not `type=\"module\"`, so it runs\nbefore the body renders. With a prefixed stylesheet, have it set `data-bestax-theme` as well.\n\n```html\n<script>\n // Bulma's scheme on <html> before the first paint: the saved choice,\n // or else the OS preference. The app saves under the same key.\n (() => {\n let saved = null;\n try {\n saved = localStorage.getItem('theme');\n } catch {\n // Storage is blocked, so fall back to the OS preference.\n }\n const dark =\n saved === 'light' || saved === 'dark'\n ? saved === 'dark'\n : window.matchMedia('(prefers-color-scheme: dark)').matches;\n document.documentElement.setAttribute(\n 'data-theme',\n dark ? 'dark' : 'light'\n );\n })();\n</script>\n```\n\nSecond, the app starts `colorMode` from the attribute the script wrote, and saves each choice\nunder the key the script reads. The `bestax-theming` skill's\n[dark-mode example](https://github.com/allxsmith/bestax/blob/main/skills/bestax-theming/examples/dark-mode.tsx)\nis the same recipe, and also covers a page that has no script.\n\n```tsx\n// Pairs with an inline <head> script in index.html that writes data-theme\n// before the first paint: the choice saved under STORAGE_KEY, or else the\n// OS preference. The Theme API page's Dark Mode section has the script.\nimport { useState } from 'react';\nimport { Theme, ToggleButton } from '@allxsmith/bestax-bulma';\n\ntype ColorMode = 'light' | 'dark';\n\n// The key the index.html script reads the saved choice from.\nconst STORAGE_KEY = 'theme';\n\n// The scheme the index.html script wrote on <html>.\nfunction initialColorMode(): ColorMode {\n if (typeof document === 'undefined') return 'light';\n return document.documentElement.getAttribute('data-theme') === 'dark'\n ? 'dark'\n : 'light';\n}\n\nfunction App() {\n const [mode, setMode] = useState<ColorMode>(initialColorMode);\n\n const setDark = (dark: boolean) => {\n const next: ColorMode = dark ? 'dark' : 'light';\n setMode(next);\n try {\n localStorage.setItem(STORAGE_KEY, next);\n } catch {\n // Storage is blocked, so the choice lasts until the page reloads.\n }\n };\n\n return (\n <Theme isRoot colorMode={mode}>\n <ToggleButton pressed={mode === 'dark'} onPressedChange={setDark}>\n Dark mode\n </ToggleButton>\n </Theme>\n );\n}\n```\n\n### Label and Text Lightnesses\n\nA color's trio recolors the color, but two of its lightnesses stay where the stylesheet's Sass\nbuild put them for the color it was built with: its label on a solid surface,\n`--bulma-<color>-invert-l`, and its text on the page, `--bulma-<color>-on-scheme-l`. bestax builds\nits primary from a dark blue, so a light primary set through the trio alone keeps white labels\nit can't carry. A color that reads lighter or darker than the built one sets both through\n`bulmaVars`.\n\nThe text has to read on a light page and on a dark one, so Bulma gives each scheme its own\non-scheme lightness. On a root theme, an on-scheme key takes `{ light, dark }`, and each value\napplies wherever Bulma's own value for that scheme would. A scheme you leave out keeps the\nstylesheet's value, and a string sets one value for both.\n\n```tsx\nfunction StockTurquoise() {\n // Stock Bulma's turquoise, with the values from bulma/css/bulma.css.\n return (\n <Theme\n isRoot\n primaryH=\"171deg\"\n primaryS=\"100%\"\n primaryL=\"41%\"\n bulmaVars={{\n '--bulma-primary-invert-l': 'var(--bulma-primary-05-l)',\n '--bulma-primary-on-scheme-l': { light: '21%', dark: '41%' },\n }}\n >\n <Button color=\"primary\">Dark label</Button>\n <Block textColor=\"primary\" colorShade=\"on-scheme\">\n Turquoise text that reads in both schemes\n </Block>\n </Theme>\n );\n}\n```\n\nA local theme writes its variables as an inline style, which can't hold a value per scheme, so it\nskips `{ light, dark }` and warns in development. Give it a string, or set the per-scheme values\nin your CSS. `bestax-no-dark-mode.css` has no dark scheme, so with it give a string. The\n`bestax-theming` skill's\n[CSS variables reference](https://github.com/allxsmith/bestax/blob/main/skills/bestax-theming/references/css-variables.md)\nsays what reads each lightness and how to pick the values.\n\n### Local Theme (Component Level)\n\n```tsx\nfunction LocalTheme() {\n return (\n <div>\n <Button color=\"primary\">Standard Primary</Button>\n\n <Theme primaryH=\"120\" primaryS=\"100%\" primaryL=\"40%\">\n <Button color=\"primary\">Green Primary</Button>\n </Theme>\n\n <Button color=\"primary\">Standard Primary Again</Button>\n </div>\n );\n}\n```\n\n### Using CSS Variables Object\n\n```tsx\nfunction SunsetTheme() {\n const sunsetTheme = {\n '--bulma-scheme-h': '18',\n '--bulma-scheme-s': '90%',\n '--bulma-light-l': '85%',\n '--bulma-dark-l': '20%',\n '--bulma-primary-h': '25',\n '--bulma-primary-s': '85%',\n '--bulma-primary-l': '55%',\n };\n\n return (\n <Theme bulmaVars={sunsetTheme} isRoot>\n <Box p=\"4\">\n <Title>Sunset Theme</Title>\n <Button color=\"primary\">Sunset Button</Button>\n <Button color=\"info\">Info Button</Button>\n </Box>\n </Theme>\n );\n}\n```\n\n### Advanced CSS Variables Usage\n\nBulma derives the body font and the control radius from these variables at `:root`, so set\ntypography and radius on an `isRoot` theme.\n\n```tsx\nfunction TypographyTheme() {\n const typographyTheme = {\n '--bulma-family-primary': '\"Helvetica Neue\", sans-serif',\n '--bulma-family-code': '\"Fira Code\", monospace',\n '--bulma-size-normal': '16px',\n '--bulma-weight-bold': '700',\n };\n\n return (\n <Theme isRoot bulmaVars={typographyTheme}>\n <Title>Custom Typography</Title>\n <SubTitle>With custom fonts and weights</SubTitle>\n </Theme>\n );\n}\n```\n\n```tsx\nfunction RadiusTheme() {\n // `radius` is the border radius helper, as on every component, so the radius scale goes through bulmaVars\n const radii = {\n '--bulma-radius-small': '6px',\n '--bulma-radius': '12px',\n '--bulma-radius-medium': '16px',\n '--bulma-radius-large': '20px',\n };\n\n return (\n <Theme isRoot bulmaVars={radii}>\n <Button color=\"primary\">Rounder Button</Button>\n </Theme>\n );\n}\n```\n\nSpacing that Bulma reads where it is used, such as the block and column gaps, works on a scoped\ntheme:\n\n```tsx\nfunction SpacingTheme() {\n const spacingTheme = {\n '--bulma-block-spacing': '2rem',\n '--bulma-column-gap': '1rem',\n };\n\n return (\n <Theme bulmaVars={spacingTheme}>\n <Columns>\n <Column>\n <Box>Wider column gap</Box>\n <Box>Wider block spacing</Box>\n </Column>\n <Column>\n <Box>Wider column gap</Box>\n </Column>\n </Columns>\n </Theme>\n );\n}\n```\n\n### Complete Color Scheme\n\n```tsx\nfunction ForestTheme() {\n const forestScheme = {\n schemeH: '150', // forest green hue\n schemeS: '50%',\n lightL: '80%',\n lightInvertL: '20%',\n darkL: '18%',\n darkInvertL: '85%',\n softL: '55%',\n boldL: '35%',\n primaryH: '160',\n primaryS: '60%',\n primaryL: '45%',\n linkH: '155',\n linkS: '65%',\n linkL: '40%',\n successH: '120',\n successS: '70%',\n successL: '45%',\n warningH: '45',\n warningS: '85%',\n warningL: '55%',\n dangerH: '355',\n dangerS: '75%',\n dangerL: '50%',\n hoverBackgroundLDelta: '4%',\n activeBackgroundLDelta: '8%',\n };\n\n return (\n <Theme {...forestScheme} isRoot>\n <Box p=\"4\">\n <Title>Forest Theme</Title>\n <Button color=\"primary\">Forest Primary</Button>\n <Button color=\"success\">Success</Button>\n <Button color=\"info\">Info</Button>\n <Button color=\"warning\">Warning</Button>\n <Button color=\"danger\">Danger</Button>\n </Box>\n </Theme>\n );\n}\n```\n\n### Theme with Styling\n\n```tsx\nfunction StyledTheme() {\n return (\n <Theme\n className=\"custom-theme-wrapper\"\n primaryH=\"45\"\n primaryS=\"100%\"\n primaryL=\"50%\"\n p=\"5\"\n m=\"3\"\n textAlign=\"centered\"\n >\n <Title>Styled Theme Container</Title>\n <Button color=\"primary\">Styled Button</Button>\n </Theme>\n );\n}\n```\n\n### Nested Themes\n\n```tsx\nfunction NestedThemes() {\n return (\n <Theme primaryH=\"210\" primaryS=\"60%\" primaryL=\"45%\" p=\"4\">\n <Title>Outer Theme (Blue)</Title>\n <Button color=\"primary\">Blue Primary</Button>\n\n <Theme primaryH=\"120\" primaryS=\"70%\" primaryL=\"40%\" p=\"3\" mt=\"4\">\n <Title size=\"4\">Inner Theme (Green)</Title>\n <Button color=\"primary\">Green Primary</Button>\n </Theme>\n\n <Button color=\"primary\" mt=\"3\">\n Blue Primary Again\n </Button>\n </Theme>\n );\n}\n```\n\n### Theme with ConfigProvider\n\n```tsx\nfunction PrefixedTheme() {\n return (\n <ConfigProvider classPrefix=\"bestax-\">\n <Theme primaryH=\"300\" primaryS=\"80%\" primaryL=\"50%\" isRoot>\n <Button color=\"primary\">Prefixed & Themed Button</Button>\n </Theme>\n </ConfigProvider>\n );\n}\n```\n\n---\n\n## Best Practices\n\n### Global vs Local Themes\n\n- Use `isRoot={true}` for application-wide themes that should affect all components\n- Use local themes (default `isRoot={false}`) for component-specific styling or theme variations\n- Local themes inherit from parent themes and can override specific variables\n- Several `isRoot` themes can be mounted at once, such as a color theme at the app root and a\n typography theme next to it. Each adds its own variables at `:root`; where two set the same\n variable, the inner or later-mounted one wins, as with nested local themes, and unmounting one\n removes only its own variables\n\n### CSS Variable Naming\n\n- All Bulma CSS variables follow the `--bulma-*` naming convention\n- Named props exist for the scheme and color variables only (like `primaryH`); every other variable, typography and radius included, goes through `bulmaVars`\n- `bulmaVars` keys are the full variable names (`'--bulma-family-primary'`), not camelCase\n- `bulmaVars` also accepts the scheme surface variables — `--bulma-scheme-main`, `--bulma-scheme-main-bis`, `--bulma-scheme-main-ter`, `--bulma-scheme-invert`, `--bulma-scheme-invert-bis`, `--bulma-scheme-invert-ter` — so overriding `--bulma-scheme-main-bis`/`-ter` re-tints every scheme-background band (e.g., `<Section bgColor=\"scheme-main-bis\">`) at once\n- `bulmaVars` also accepts each color's label and text lightnesses, `--bulma-<color>-invert-l` and `--bulma-<color>-on-scheme-l`, which a new trio does not move. On a root theme the on-scheme ones take a value per scheme (see [Label and Text Lightnesses](https://bestax.io/docs/api/helpers/theme#label-and-text-lightnesses))\n- `bulmaVars` also accepts `--bulma-shadow` (plus `--bulma-shadow-h`/`-s`/`-l`) — the upstream token that `.box`, `.card`, `.dropdown`, and `.panel` derive their own shadow variables from. Those selectors re-declare their derived variable (e.g. `--bulma-box-shadow`) on themselves, so setting it from an ancestor `Theme` is only inherited and never wins; overriding `--bulma-shadow` itself is the one override that cascades. There is no individual `shadow` prop for this — `shadow` already names the unrelated `shadowless` helper class — so reach it through `bulmaVars`\n\n### Performance Considerations\n\n- Prefer setting themes at higher levels in your component tree rather than deeply nested\n- CSS variables are inherited, so child themes only need to override specific variables\n\n### Color System\n\n- Bulma uses HSL (Hue, Saturation, Lightness) for its color system\n- Hue values range from 0-360 (color wheel degrees)\n- Saturation and Lightness are typically percentages (e.g., '50%')\n- The scheme variables control the base color relationships\n\n---\n\n## API Reference\n\n### Named Props and `bulmaVars`\n\n`ThemeProps` has a named, camelCase prop for each scheme and color variable, listed under\n[CSS Variable Props](https://bestax.io/docs/api/helpers/theme#css-variable-props). Each converts to its CSS variable by the same rule:\n\n```\npropName → --bulma-prop-name\n```\n\n- `primaryH` → `--bulma-primary-h`\n- `schemeS` → `--bulma-scheme-s`\n- `lightL` → `--bulma-light-l`\n- `hoverBackgroundLDelta` → `--bulma-hover-background-l-delta`\n\nEvery other variable (typography, radius, spacing, and the rest) has no named prop in\n`ThemeProps` and goes through `bulmaVars`, keyed by its full `--bulma-*` name. JavaScript code\nthat passes one as a camelCase prop (`familyPrimary`) may still render, but it is outside the\ntyped API, so move it to `bulmaVars`:\n\n```tsx\n<Theme\n isRoot\n primaryH=\"350\"\n primaryS=\"73%\"\n primaryL=\"44%\"\n bulmaVars={{\n '--bulma-radius': '2px',\n '--bulma-radius-small': '1px',\n '--bulma-family-primary': '\"IBM Plex Sans\", system-ui, sans-serif',\n '--bulma-family-code': '\"IBM Plex Mono\", ui-monospace, monospace',\n }}\n>\n <App />\n</Theme>\n```\n\nSome of those names are already helper props, so they cannot double as CSS-variable props:\n\n- `radius` is the border radius helper, as on every other component, so `<Theme radius=\"radiusless\">`\n adds `is-radiusless` to the wrapper. On a Theme it also sets `--bulma-radius` to `0`, so what\n is inside the Theme loses its radius too; under `isRoot` it writes that at `:root`, squaring\n everything on the page that takes its radius from it. The sizes (`small`, `normal`, `large`,\n `rounded`) add their `has-radius-*` class to the wrapper and set no variable, so they round\n the wrapper and leave what is inside alone; under `isRoot` there is no wrapper, so they do\n nothing and warn in development. Set any other `--bulma-radius` through\n `bulmaVars`. `<Theme radius=\"2px\" />` is a type error; in JavaScript it still sets\n `--bulma-radius`, which is how the prop used to behave, but that route is deprecated and logs\n a warning in development.\n- `shadow` is the `shadowless` helper. Set `--bulma-shadow` through `bulmaVars`.\n- `columnGap` is the column gap helper, as on every other component, so `<Theme columnGap=\"2\">`\n adds `is-column-gap-2` to the wrapper and sets no variable; under `isRoot` there is no wrapper,\n so it does nothing and warns in development. Set the columns gutter, `--bulma-column-gap`,\n through `bulmaVars`. Before the gap helpers, JavaScript could pass `columnGap` to set that\n variable. A string that is not a gap step (`columnGap=\"1rem\"`) still does, through a\n deprecated route that logs a warning in development, but a gap step does not:\n `columnGap=\"0\"`, which used to zero the gutters inside the Theme, now adds `is-column-gap-0`\n to the wrapper.\n\n### Props vs bulmaVars\n\nYou can set the scheme and color variables in two ways:\n\n1. **Named Props** (recommended where one exists):\n\n ```tsx\n <Theme primaryH=\"270\" primaryS=\"100%\" primaryL=\"50%\">\n <App />\n </Theme>\n ```\n\n2. **bulmaVars Object** (for every other variable, or when you have many to set):\n\n ```tsx\n <Theme\n bulmaVars={{\n '--bulma-primary-h': '270',\n '--bulma-primary-s': '100%',\n '--bulma-primary-l': '50%',\n '--bulma-block-spacing': '2rem',\n }}\n >\n <App />\n </Theme>\n ```\n\n3. **Combined** (props take precedence over bulmaVars):\n ```tsx\n <Theme\n primaryH=\"270\" // This takes precedence\n bulmaVars={{\n '--bulma-primary-h': '180', // This is overridden\n '--bulma-block-spacing': '2rem',\n }}\n >\n <App />\n </Theme>\n ```\n\n### TypeScript Support\n\n`bulmaVars` is typed as a partial record keyed by the union of every variable `Theme` accepts.\nWrite it as an object literal and your editor autocompletes the keys and flags a misspelled or\nunsupported one as a type error. A separately declared object only has to share one valid key\nto typecheck, so a typo in it is silently dropped; annotate it with\n`satisfies ThemeProps['bulmaVars']` to keep the check:\n\n```tsx\n<Theme\n primaryH=\"270\"\n bulmaVars={{\n '--bulma-block-spacing': '2rem', // start typing '--bulma-' to list the rest\n }}\n>\n <App />\n</Theme>\n```\n\n---\n\n## See Also\n\n- [ConfigProvider](https://bestax.io/docs/api/helpers/config) - For class prefixing and configuration\n- [useBulmaClasses](https://bestax.io/docs/api/helpers/usebulmaclasses) - For applying Bulma helper classes\n- [Bulma CSS Variables Documentation](https://bulma.io/documentation/features/css-variables/) - Official Bulma CSS variables reference\n\n---\n\n## Props\n\n| Prop | Type | Description |\n| ----------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `children` | `ReactNode` | The child components to apply the theme to. |\n| `className` | `string` | Additional CSS classes for the theme wrapper. A root theme has no wrapper, so under `isRoot` it does nothing and warns in development. |\n| `isRoot` | `boolean` | When `true`, applies CSS variables globally at `:root` level. When `false` (default), applies variables only to the wrapper div. Several root themes compose; an inner or later-mounted one wins a variable they share. The variables are written before the browser paints the theme's content, and a server render doesn't include them. A root theme renders no wrapper, so `className` and the helper props, such as `m` or `shadow`, do nothing on it and warn in development. |\n| `colorMode` | `'light' \\| 'dark' \\| 'system'` | Sets Bulma's light/dark scheme by writing the `data-theme` attribute on `<html>`, plus `data-<prefix>theme` inside a `ConfigProvider` with a `classPrefix`. If the prefix cannot form an attribute name, `'light'` and `'dark'` write only `data-theme` and warn in development. Always global (even on a scoped `Theme`). `'system'` removes them so Bulma follows the OS `prefers-color-scheme`. Omit to leave the current setting untouched. |\n| `bulmaVars` | `ThemeProps['bulmaVars']` | Object mapping Bulma CSS variable names to string values (e.g., `{'--bulma-primary-h': '210'}`). Keys are limited to the `BulmaVars` type; anything else is not applied. Each color's `--bulma-<color>-on-scheme-l` also takes `{ light, dark }` on a root theme, as [Label and Text Lightnesses](https://bestax.io/docs/api/helpers/theme#label-and-text-lightnesses) says. A variable Bulma declares on the component itself changes nothing from here and warns in development; see [Which Variables a Theme Can Change](https://bestax.io/docs/api/helpers/theme#which-variables-a-theme-can-change). |\n| `columnGap` | [`BulmaGapStep`](https://bestax.io/docs/api/helpers/valid-values) | The column gap helper: a gap step adds `is-column-gap-<step>` to the wrapper div and sets no variable, so under `isRoot` it does nothing and warns in development. Set `--bulma-column-gap` through `bulmaVars`. Any other string still sets `--bulma-column-gap`, a deprecated route that warns in development. |\n| `radius` | `'radiusless' \\| 'small' \\| 'normal' \\| 'large' \\| 'rounded'` | The border radius helper. `radiusless` adds `is-radiusless` to the wrapper div and sets `--bulma-radius` to `0`, at `:root` under `isRoot`. The sizes add their `has-radius-*` class to the wrapper and set no variable, so under `isRoot` they do nothing and warn in development. Any other string still sets `--bulma-radius`, a deprecated route that warns in development. |\n\n### CSS Variable Props\n\nThe Theme component accepts individual CSS variable props that correspond to Bulma's design tokens. These props are automatically converted to their corresponding CSS custom properties (e.g., `primaryH` → `--bulma-primary-h`).\n\n#### Scheme Variables\n\n| Prop | Type | Description | CSS Variable |\n| ------------------------ | -------- | ------------------------------------------------- | ----------------------------------- |\n| `schemeH` | `string` | Base hue for the color scheme (0-360) | `--bulma-scheme-h` |\n| `schemeS` | `string` | Base saturation for the color scheme (percentage) | `--bulma-scheme-s` |\n| `lightL` | `string` | Lightness value for light backgrounds | `--bulma-light-l` |\n| `lightInvertL` | `string` | Inverted lightness for light backgrounds | `--bulma-light-invert-l` |\n| `darkL` | `string` | Lightness value for dark backgrounds | `--bulma-dark-l` |\n| `darkInvertL` | `string` | Inverted lightness for dark backgrounds | `--bulma-dark-invert-l` |\n| `softL` | `string` | Lightness value for soft colors | `--bulma-soft-l` |\n| `boldL` | `string` | Lightness value for bold colors | `--bulma-bold-l` |\n| `softInvertL` | `string` | Inverted lightness for soft colors | `--bulma-soft-invert-l` |\n| `boldInvertL` | `string` | Inverted lightness for bold colors | `--bulma-bold-invert-l` |\n| `hoverBackgroundLDelta` | `string` | Lightness delta for hover background states | `--bulma-hover-background-l-delta` |\n| `activeBackgroundLDelta` | `string` | Lightness delta for active background states | `--bulma-active-background-l-delta` |\n| `hoverBorderLDelta` | `string` | Lightness delta for hover border states | `--bulma-hover-border-l-delta` |\n| `activeBorderLDelta` | `string` | Lightness delta for active border states | `--bulma-active-border-l-delta` |\n| `hoverColorLDelta` | `string` | Lightness delta for hover text color states | `--bulma-hover-color-l-delta` |\n| `activeColorLDelta` | `string` | Lightness delta for active text color states | `--bulma-active-color-l-delta` |\n| `hoverShadowADelta` | `string` | Alpha delta for hover shadow states | `--bulma-hover-shadow-a-delta` |\n| `activeShadowADelta` | `string` | Alpha delta for active shadow states | `--bulma-active-shadow-a-delta` |\n\n#### Color Variables\n\n| Prop | Type | Description | CSS Variable |\n| ---------- | -------- | ------------------------------------- | ------------------- |\n| `primaryH` | `string` | Primary color hue (0-360) | `--bulma-primary-h` |\n| `primaryS` | `string` | Primary color saturation (percentage) | `--bulma-primary-s` |\n| `primaryL` | `string` | Primary color lightness (percentage) | `--bulma-primary-l` |\n| `linkH` | `string` | Link color hue (0-360) | `--bulma-link-h` |\n| `linkS` | `string` | Link color saturation (percentage) | `--bulma-link-s` |\n| `linkL` | `string` | Link color lightness (percentage) | `--bulma-link-l` |\n| `infoH` | `string` | Info color hue (0-360) | `--bulma-info-h` |\n| `infoS` | `string` | Info color saturation (percentage) | `--bulma-info-s` |\n| `infoL` | `string` | Info color lightness (percentage) | `--bulma-info-l` |\n| `successH` | `string` | Success color hue (0-360) | `--bulma-success-h` |\n| `successS` | `string` | Success color saturation (percentage) | `--bulma-success-s` |\n| `successL` | `string` | Success color lightness (percentage) | `--bulma-success-l` |\n| `warningH` | `string` | Warning color hue (0-360) | `--bulma-warning-h` |\n| `warningS` | `string` | Warning color saturation (percentage) | `--bulma-warning-s` |\n| `warningL` | `string` | Warning color lightness (percentage) | `--bulma-warning-l` |\n| `dangerH` | `string` | Danger color hue (0-360) | `--bulma-danger-h` |\n| `dangerS` | `string` | Danger color saturation (percentage) | `--bulma-danger-s` |\n| `dangerL` | `string` | Danger color lightness (percentage) | `--bulma-danger-l` |\n\n#### Shadow Variables\n\nReachable only through `bulmaVars` — there is no individual prop, since `shadow` already names\nthe `BulmaOtherProps` `shadowless` helper class. As noted above, `--bulma-shadow` is the one\noverride that cascades into `.box`/`.card`/`.dropdown`/`.panel` shadows.\n\n| CSS Variable | Description |\n| ------------------ | ----------------------------------------------------------------------------------------------------- |\n| `--bulma-shadow-h` | Shadow color hue (0-360) |\n| `--bulma-shadow-s` | Shadow color saturation (percentage) |\n| `--bulma-shadow-l` | Shadow color lightness (percentage) |\n| `--bulma-shadow` | The full shadow value that `.box`/`.card`/`.dropdown`/`.panel` derive their own shadow variables from |\n\n#### Which Variables a Theme Can Change\n\n`bulmaVars` takes any key of the `BulmaVars` type. Write it as an object literal and your editor\nlists them as you type `'--bulma-'`. Not every one of those keys does something from a `Theme`,\nthough, and the reason is where Bulma declares the variable.\n\nA `Theme` writes its variables on its wrapper `div`, or at `:root` under `isRoot`. A component\ninherits them from there only if it declares no value of its own, because an element's own\ndeclaration always beats an inherited one.\n\n- **Global variables reach everything under the Theme.** Bulma declares the scheme, color and\n shadow variables above, and the typography, radius, spacing and timing tokens (such as\n `--bulma-family-primary` and `--bulma-radius`), on `:root`. Set them on a `Theme`, or on any\n ancestor. Some of them Bulma also declares again on one component, which keeps its own value\n while everything else takes the Theme's. That component's API page lists the variable as\n declared on it.\n- **Component variables have to be set on the component.** Where Bulma declares a variable on\n the component's own element, as it does `--bulma-card-radius`, `--bulma-tag-h` and\n `--bulma-delete-dimensions`, a value a `Theme` sets never reaches it. Those keys are still\n accepted, so existing code keeps compiling, but they change nothing, and `Theme` logs a\n warning in development naming them. Set them on the component instead, with\n `className` (a class in your own stylesheet) or the `style` prop, or at build time through\n [Sass](https://bestax.io/docs/guides/features/sass-customization).\n\nEach component's API page lists its variables under **CSS & Sass Variables** and says where Bulma\ndeclares them, so check there before reaching for a `Theme`. The\n[shadow note](https://bestax.io/docs/api/helpers/theme#css-variable-naming) above is the same rule in action: `--bulma-box-shadow` lives on\n`.box`, so the way to change box shadows from a `Theme` is the global `--bulma-shadow` they derive\nfrom.\n\nThe Theme component also supports all [Bulma helper class props](https://bestax.io/docs/api/helpers/usebulmaclasses) for styling the wrapper element."
471
475
  }
@@ -42,7 +42,7 @@
42
42
  "code": "<TreeView\n aria-label=\"Files\"\n selectionMode=\"single\"\n defaultSelected=\"src/App.tsx\"\n defaultExpanded={['src']}\n>\n <TreeView.Item value=\"src\" label=\"src\" icon=\"folder\">\n <TreeView.Item value=\"src/index.ts\" label=\"index.ts\" icon=\"file\" />\n <TreeView.Item value=\"src/App.tsx\" label=\"App.tsx\" icon=\"file\" />\n <TreeView.Item value=\"src/legacy.ts\" label=\"legacy.ts\" disabled />\n </TreeView.Item>\n <TreeView.Item value=\"package.json\" label=\"package.json\" icon=\"file\" />\n</TreeView>"
43
43
  }
44
44
  ],
45
- "accessibility": "`TreeView` follows the [APG Tree View Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/).\n\n- **Roles:** the root is a `<ul role=\"tree\">`, each item an `<li role=\"treeitem\">`, and an open\n parent's children sit in a `<ul role=\"group\">` inside it. A closed item's children have no\n markup.\n- **A name:** a tree needs one, from `aria-label` or `aria-labelledby`, and a development build\n warns when it has neither. Each item is named by `aria-labelledby` pointing at its own label, and\n declares its `aria-level`, which VoiceOver needs according to\n [GitHub's testing](https://github.blog/engineering/user-experience/considerations-for-making-a-tree-view-component-accessible/).\n `aria-expanded` is on parents only.\n- **One tab stop, with real focus:** one item holds `tabIndex={0}` and the rest `-1`, and focus\n moves from item to item, rather than through `aria-activedescendant`. When focus comes into the\n tree, it lands on the selected item, or in a checkbox tree on the first checked item on the page.\n Without one, it lands where focus last was, or on the first item. If the item holding the stop\n goes away, its nearest ancestor on the page takes it, and focus goes too if it had focus.\n- **Selection:** a `single` tree marks each enabled item `aria-selected`, `true` or `false`. A\n `checkbox` tree marks `aria-checked` on every item, `true`, `false` or `mixed`, disabled ones\n included, and adds `aria-multiselectable=\"true\"` to the tree. A tree never has both, and a tree\n with no `selectionMode` has neither. Selection doesn't follow focus.\n- **The box** is a [`Checkbox`](https://bestax.io/docs/api/form/checkbox) inside an `aria-hidden` span, with\n `tabIndex={-1}`, so it's neither a tab stop nor a second thing for a screen reader to read. The\n item's `aria-checked` is what a screen reader hears.\n- **Disabled items** are `aria-disabled` and stay reachable from the arrow keys, Home, End and\n typing, so a screen reader user finds them. They're never selected, checked, opened or closed.\n One that's already open stays open, so what's under it is still reachable.\n- **A loading item's row** is a `treeitem` that says Loading…, marked `aria-disabled` and reached\n the way a disabled item is, so a screen reader user who moves into the folder hears what's\n happening. It has no value: it's never selected or checked, it has no `aria-selected` or\n `aria-checked`, and a checkbox tree doesn't count it. If it goes with focus on it, focus moves to\n the item it was loading for. Bulma's loader turns in its chevron's place, hidden from assistive\n tech.\n- **A failed load's row** is a `treeitem` named by its message and the Retry button's text, such\n as \"Couldn't load src Retry\". Enter, Space or a click on the row calls `onRetry`, in every mode.\n The button is drawn inside an `aria-hidden` span and takes no focus, the way an item's box is,\n so the tree keeps its one tab stop, and nothing in the message should be a link or a button\n either. A retry that starts loading keeps the same row, so focus stays on it. With no `onRetry`,\n the row is disabled and shows the message alone.\n- **Announcements:** a visually hidden `role=\"status\"` region with `aria-live=\"polite\"` sits right\n after the tree, one for the whole tree, from the moment it mounts, so a screen reader is already\n listening when something is written into it. It says \"Loading src\" when an open item starts\n loading, a failed load's message, and \"src loaded\" when the loading row or a failed load's row\n goes with the item still open.\n The name is the item's label as a screen reader reads it. A closed item, or one in a closed\n branch, says nothing. Each announcement is written a moment after the change and cleared soon\n after, so the page doesn't keep a second copy of the text.\n\nEnter and Space do the item's one thing, which depends on the mode:\n\n| Key | No `selectionMode` | `single` | `checkbox` |\n| ----- | ------------------------ | ---------------- | ------------------------ |\n| Enter | Opens or closes a parent | Selects the item | Opens or closes a parent |\n| Space | Opens or closes a parent | Selects the item | Checks or unchecks it |\n\nOn a failed load's row, both retry, whatever the mode. The rest work the same in every mode:\n\n| Key | What it does |\n| ---------- | ----------------------------------------------------------------------------------------- |\n| Down, Up | The next or previous item on the page, without wrapping |\n| Right | Opens a closed parent, and focus stays. On an open parent, moves to its first child |\n| Left | Closes an open parent. Otherwise moves to its parent |\n| Home, End | The first item, and the last one on the page |\n| `*` | Opens every closed parent beside the focused item, itself included. Focus stays |\n| Characters | Moves to the next item whose label starts with what's typed, several characters adding up |\n| Tab | Leaves the tree |\n\n- **Right to left:** Right and Left swap in a right-to-left page, and the chevron points to the\n left while closed.\n- **Keys it leaves alone:** anything with Control, Alt or Meta held, anything while an input method\n is composing, and Escape, so it reaches a Modal or Popover around the tree. An item's own\n `onKeyDown` runs first, and calling `preventDefault()` there keeps the tree from acting on the\n key.\n- **Clicks:** the chevron only opens and closes. The rest of the row does the item's one thing:\n opens or closes it with no `selectionMode`, selects it (and opens or closes a parent) in a\n `single` tree, and checks it in a `checkbox` tree. A click moves focus and the tab stop to the\n item.\n- **Not for site navigation:** APG's own\n [navigation tree example](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/examples/treeview-navigation/)\n says \"A pattern more suited for typical site navigation with expandable groups of links is the\n disclosure pattern\", and a tree's items aren't links. Keep a sidebar on [`Menu`](https://bestax.io/docs/api/components/menu),\n whose `Menu.Section` is that disclosure pattern.\n- **Focus and the selected look differ:** the focus ring sits outside the row, so it shows on the\n selected fill as well as on a plain row.\n- **Forced colors:** in Windows high contrast mode the selected row is the system's Highlight and\n HighlightText, a disabled row or a loading one is GrayText, and the focus ring, the chevrons and\n the guide lines stay visible, since they're outlines and borders. A mixed box keeps its dash, and\n the loader's ring stays open.\n- **Motion:** the chevron turns with a short transition, and the loader spins. Both stop when the\n reader asks for reduced motion, and the loader stays drawn.",
45
+ "accessibility": "`TreeView` follows the [APG Tree View Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/).\n\n- **Roles:** the root is a `<ul role=\"tree\">`, each item an `<li role=\"treeitem\">`, and an open\n parent's children sit in a `<ul role=\"group\">` inside it. A closed item's children have no\n markup.\n- **A name:** a tree needs one, from `aria-label` or `aria-labelledby`, and a development build\n warns when it has neither. Each item is named by `aria-labelledby` pointing at its own label, and\n declares its `aria-level`, which VoiceOver needs according to\n [GitHub's testing](https://github.blog/engineering/user-experience/considerations-for-making-a-tree-view-component-accessible/).\n `aria-expanded` is on parents only.\n- **Positions:** each item also declares `aria-posinset` and `aria-setsize`, its place among the\n items beside it, so a screen reader that doesn't work positions out for itself still has them.\n The tree reads them off the page once it mounts, and again whenever items open, close, load or\n move, so server markup leaves them to the browser until then. They're the values a browser works\n out on its own: a loading row or a failed load's row counts with the items beside it. When the\n page holds only part of a set, such as the first page of a long folder, pass both on every item\n of that set: yours win, and the tree's own stay on any item without them.\n- **One tab stop, with real focus:** one item holds `tabIndex={0}` and the rest `-1`, and focus\n moves from item to item, rather than through `aria-activedescendant`. When focus comes into the\n tree, it lands on the selected item, or in a checkbox tree on the first checked item on the page.\n Without one, it lands where focus last was, or on the first item. If the item holding the stop\n goes away, its nearest ancestor on the page takes it, and focus goes too if it had focus.\n- **Selection:** a `single` tree marks each enabled item `aria-selected`, `true` or `false`. A\n `checkbox` tree marks `aria-checked` on every item, `true`, `false` or `mixed`, disabled ones\n included, and adds `aria-multiselectable=\"true\"` to the tree. A tree never has both, and a tree\n with no `selectionMode` has neither. Selection doesn't follow focus.\n- **The box** is a [`Checkbox`](https://bestax.io/docs/api/form/checkbox) inside an `aria-hidden` span, with\n `tabIndex={-1}`, so it's neither a tab stop nor a second thing for a screen reader to read. The\n item's `aria-checked` is what a screen reader hears.\n- **Disabled items** are `aria-disabled` and stay reachable from the arrow keys, Home, End and\n typing, so a screen reader user finds them. They're never selected, checked, opened or closed.\n One that's already open stays open, so what's under it is still reachable. In a `single` tree a\n disabled item has no `aria-selected`. In a `checkbox` tree it keeps `aria-checked`, so the state\n its box shows is heard as well as seen, such as \"checked, disabled\".\n- **A loading item's row** is a `treeitem` that says Loading…, marked `aria-disabled` and reached\n the way a disabled item is, so a screen reader user who moves into the folder hears what's\n happening. It has no value: it's never selected or checked, it has no `aria-selected` or\n `aria-checked`, and a checkbox tree doesn't count it. If it goes with focus on it, focus moves to\n the item it was loading for. Bulma's loader turns in its chevron's place, hidden from assistive\n tech.\n- **A failed load's row** is a `treeitem` named by its message and the Retry button's text, such\n as \"Couldn't load src Retry\". Enter, Space or a click on the row calls `onRetry`, in every mode.\n The button is drawn inside an `aria-hidden` span and takes no focus, the way an item's box is,\n so the tree keeps its one tab stop, and nothing in the message should be a link or a button\n either. A retry that starts loading keeps the same row, so focus stays on it. With no `onRetry`,\n the row is disabled and shows the message alone.\n- **Announcements:** a visually hidden `role=\"status\"` region with `aria-live=\"polite\"` sits right\n after the tree, one for the whole tree, from the moment it mounts, so a screen reader is already\n listening when something is written into it. It says \"Loading src\" when an open item starts\n loading, a failed load's message, and \"src loaded\" when the loading row or a failed load's row\n goes with the item still open.\n The name is the item's label as a screen reader reads it. A closed item, or one in a closed\n branch, says nothing. Each announcement is written a moment after the change and cleared soon\n after, so the page doesn't keep a second copy of the text. A selection or a check isn't written\n there: it changes `aria-selected` or `aria-checked` on the focused item, which a screen reader\n reads out again, as the simulated one below does.\n\nEnter and Space do the item's one thing, which depends on the mode:\n\n| Key | No `selectionMode` | `single` | `checkbox` |\n| ----- | ------------------------ | ---------------- | ------------------------ |\n| Enter | Opens or closes a parent | Selects the item | Opens or closes a parent |\n| Space | Opens or closes a parent | Selects the item | Checks or unchecks it |\n\nOn a failed load's row, both retry, whatever the mode. The rest work the same in every mode:\n\n| Key | What it does |\n| ---------- | ----------------------------------------------------------------------------------------- |\n| Down, Up | The next or previous item on the page, without wrapping |\n| Right | Opens a closed parent, and focus stays. On an open parent, moves to its first child |\n| Left | Closes an open parent. Otherwise moves to its parent |\n| Home, End | The first item, and the last one on the page |\n| `*` | Opens every closed parent beside the focused item, itself included. Focus stays |\n| Characters | Moves to the next item whose label starts with what's typed, several characters adding up |\n| Tab | Leaves the tree |\n\n- **Right to left:** Right and Left swap in a right-to-left page, and the chevron points to the\n left while closed.\n- **Keys it leaves alone:** anything with Control, Alt or Meta held, anything while an input method\n is composing, and Escape, so it reaches a Modal or Popover around the tree. An item's own\n `onKeyDown` runs first, and calling `preventDefault()` there keeps the tree from acting on the\n key.\n- **Clicks:** the chevron only opens and closes. The rest of the row does the item's one thing:\n opens or closes it with no `selectionMode`, selects it (and opens or closes a parent) in a\n `single` tree, and checks it in a `checkbox` tree. A click moves focus and the tab stop to the\n item.\n- **Not for site navigation:** APG's own\n [navigation tree example](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/examples/treeview-navigation/)\n says \"A pattern more suited for typical site navigation with expandable groups of links is the\n disclosure pattern\", and a tree's items aren't links. Keep a sidebar on [`Menu`](https://bestax.io/docs/api/components/menu),\n whose `Menu.Section` is that disclosure pattern.\n- **Focus and the selected look differ:** the focus ring sits outside the row, so it shows on the\n selected fill as well as on a plain row.\n- **Forced colors:** in Windows high contrast mode the selected row is the system's Highlight and\n HighlightText, a disabled row or a loading one is GrayText, and the focus ring, the chevrons and\n the guide lines stay visible, since they're outlines and borders. A mixed box keeps its dash, and\n the loader's ring stays open.\n- **Motion:** the chevron turns with a short transition, and the loader spins. Both stop when the\n reader asks for reduced motion, and the loader stays drawn.\n\n**What a screen reader says.** What follows is simulated, with\n[Guidepup's Virtual Screen Reader](https://www.guidepup.dev/docs/virtual) 0.33.0, which reads the\naccessibility tree the way the ARIA and HTML specs describe. It isn't a recording of NVDA, JAWS\nor VoiceOver, and each of those words things its own way. The library's tests press these keys\nthrough it on the FileBrowser, Permissions and DeepTree stories, and fail if what it says changes.\n\n1. **Onto a closed parent, then Right and Left:** \"treeitem, helpers, not expanded, level 2,\n position 2, not selected, set size 3\". Right reads it again as \"expanded\" and Left as \"not\n expanded\", with focus staying on it. The items that appear are read when focus reaches them.\n2. **Enter and Space with no `selectionMode`:** each press on a parent reads it again with its new\n state, \"treeitem, src, not expanded, level 1, position 1, set size 4\", then \"expanded\". A tree\n with no selection says nothing about one.\n3. **A level deeper and back up:** Right from components reads its first child, \"treeitem,\n Button.tsx, level 3, position 1, not selected, set size 3\", and Left goes back to \"treeitem,\n components, expanded, level 2, position 1, not selected, set size 3\".\n4. **Enter in a `single` tree:** the item is read again, \"treeitem, TreeView.tsx, level 3, position\n 3, selected, set size 3\". The item that lost the selection says \"not selected\" when focus gets\n back to it.\n5. **Space in a `checkbox` tree:** the item is read again as \"checked\". Its parent turns mixed\n without a word, and Left to it reads \"treeitem, Billing, partially checked, expanded, level 1,\n position 2, set size 2\". Space on the parent reads \"checked\", and so does each item under it as\n focus reaches it.\n6. **A disabled item:** in a `single` tree, \"treeitem, legacy.ts, disabled, level 2, position 3, set\n size 3\", with no selection state. In a `checkbox` tree, \"treeitem, Delete, not checked,\n disabled, level 4, position 3, set size 3\", or \"checked, disabled\" for one its own value checks.\n Enter and Space on it change nothing and say nothing.\n7. **Positions:** each item says its place among the items beside it, such as \"position 2\" and \"set\n size 3\", at every level.\n8. **Tab in and back out, with an item selected:** Tab lands on the selected item, \"treeitem,\n Card.tsx, level 3, position 2, selected, set size 3\", and Tab again leaves the tree for the next\n control. Shift+Tab comes back to Card.tsx, wherever focus was in the tree before.\n\nStill open in [#1171](https://github.com/allxsmith/bestax/issues/1171): recordings with NVDA, JAWS\nand VoiceOver on macOS and iOS, a real Windows contrast theme in place of Chromium's forced colors\nemulation, and runs in Firefox and Safari.",
46
46
  "related": [
47
47
  "Checkbox",
48
48
  "Collapse",
@@ -345,6 +345,24 @@
345
345
  }
346
346
  ],
347
347
  "extraProps": [
348
+ {
349
+ "name": "aria-posinset",
350
+ "type": "number",
351
+ "default": null,
352
+ "description": "The item's number among the items beside it. The tree declares it, and `aria-setsize`, from the items on the page once it mounts. When the page holds only part of a set, such as the first page of a long folder, pass both on every item of that set: yours win, and the tree's own stay on any item without them.",
353
+ "required": false,
354
+ "inherited": false,
355
+ "deprecated": false
356
+ },
357
+ {
358
+ "name": "aria-setsize",
359
+ "type": "number",
360
+ "default": null,
361
+ "description": "How many items sit beside it, itself included. Declared by the tree like `aria-posinset`, and `-1` says the count isn't known.",
362
+ "required": false,
363
+ "inherited": false,
364
+ "deprecated": false
365
+ },
348
366
  {
349
367
  "name": "ref",
350
368
  "type": "React.Ref<HTMLLIElement>",