Bestax

0

Agent Skills and an offline MCP server for building React apps with Bestax, a component library for Bulma v1.

7 skills

bestax-custom-component

Build a custom React component in the bestax/Bulma style. In an app using @allxsmith/bestax-bulma — compose existing components, helper props, public hooks (useBulmaClasses, usePrefixedClassNames), and --bulma-* CSS variables. In the bestax monorepo — the full component pipeline (SCSS partial, stories, tests, docs, wiring). Use when creating a component beyond stock Bulma or extending one.

# Building a custom component the bestax way This skill teaches how to build a component that isn't in the library — composed from bestax pieces in an app, or as a full library "extra" inside the bestax monorepo. ## Which context are you in? - **The bestax monorepo** (the repo contains `bulma-ui/src/`) → follow `references/library-contributor.md` instead of this file: five-file layout, SCSS partial, stories, jest tests, docs page, wiring. - **An app depending on `@allxsmith/bestax-bulma`** (e.g. scaffolded by `npm create bestax`) → continue here. Everything below assumes public package imports and a plain Vite app. For **form** components (Field/Control/Input/etc.) use the `bestax-form` skill instead. ## Check for an existing component first Before building anything, **search the library for a component that already does this — or a close synonym** — and tell the user what you found. Many requests are covered by an existing element, or are best built by composing existing ones. Building a near-duplicate (a "label" when `Tag` exists, a "banner" when `Notification` exists) fragments the API and is usually the wrong call. Where to look: - `references/component-catalog.md` — **start here.** Every documented component with a one-line purpose, grouped by category. Scan it for the name and its synonyms before anything else. - https://bestax.io/docs/api — one doc page per shipped component (full props). Then decide, and **surface the decision to the user**: - **Exact / synonym match exists** → recommend using it. Don't build a duplicate. (E.g. a small colored label/badge/chip → `Tag` / `Tags` already exist.) - **Partial overlap** → prefer **composing** the existing pieces inside your new component rather than re-implementing them. (E.g. a "profile card" → there's no `ProfileCard`, but `Card`, `Image`, `Title`, `SubTitle`, and `Content` exist; build `ProfileCard` to compose them.) - **Genuine gap** → build the new component using the pattern below. State plainly which case applies before writing code, e.g. _"`Tag` already covers a colored label — use that instead"_ or _"No `ProfileCard` exists; I'll build one composing the existing `Card`/`Image`/`Title` elements."_ ## Composition first Build from existing components before writing any CSS: `Box`, `Card`, `Title`, `SubTitle`, `Icon`, `Block`, `Content`, `Tag`, plus the shared Bulma helper props (spacing, color, typography, flexbox). Some compound sub-parts are the exception — `Modal.Card`, `Tabs.Tab`, `Message.Body` take **no Bulma helper props**, just `className` + HTML attributes plus their own few (`Tabs.Tab` requires `index={i}` and has built-in `disabled` and `icon`/`iconLibrary`/`iconVariant`/`iconSize`/`iconFeatures` — don't nest an `<Icon>` there) — so put helper props on the parent or on an element inside them, never invent them there. (`Card.*` sub-parts do take helper props, like `Table.*`/`Menu.*`/`Hero.*`.) Most "custom components" are a composition function — zero new styles. See `examples/stat-card.tsx` for a complete worked example. ## The component spine Same shape the library itself uses, with all imports from the package. Every reusable component gets it — including pure compositions with zero CSS (a heading block, a labeled wrapper): extend `BulmaClassesProps`, run your props through `useBulmaClasses`, merge its `bulmaHelperClasses` into `className`, and spread the `rest` **it** returns — spreading the raw props instead leaks helper props onto the DOM and emits none of their classes. The `usePrefixedClassNames` root class is needed only when component-scoped CSS (or a variant class) targets it — a zero-CSS composition may omit that call. File at `src/components/MyComponent.tsx`: ```tsx import type React from 'react'; import { classNames, usePrefixedClassNames, useBulmaClasses, type BulmaClassesProps, } from '@allxsmith/bestax-bulma'; export interface MyComponentProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'color'>, Omit<BulmaClassesProps, 'color'> { color?: 'primary' | 'link' | 'info' | 'success' | 'warning' | 'danger'; } export function MyComponent({ color, className, children, ...props }: MyComponentProps) { const { bulmaHelperClasses, rest } = useBulmaClasses(props); const mainClasses = usePrefixedClassNames('mycomponent', { [`is-${color}`]: !!color, }); return ( <div className={classNames(mainClasses, bulmaHelperClasses, className)} {...rest} > {children} </div> ); } ``` This gives your component the full Bulma helper-prop surface (`m`, `p`, `textAlign`, …) for free. `references/api.md` documents the helpers. ## Styling ladder — use the lowest rung that works **Rung 1 — helper props only (default).** House rules: never `style={{}}`. Layout with `Block`/`Box` and `display="flex"`, `flexDirection`, `alignItems`, `justifyContent`. Flex layouts have **no `gap` helper** — space children with `m*`/`p*` margins instead (`Grid` and `Columns` take a `gap` prop). Before writing `style={{ … }}` anywhere, translate each declaration: | Inline style you're about to write | Helper props instead | | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `marginTop: '1rem'` (any margin/padding) | `mt="4"` — `m`/`mt`/`mx`/`p`/`py`/… scale: `1`=0.25rem, `2`=0.5rem, `3`=0.75rem, `4`=1rem, `5`=1.5rem, `6`=3rem (nearest step) | | `textAlign: 'center'` | `textAlign="centered"` (also `left`, `right`, `justified`) | | `color: '#…'` | `textColor` with the nearest Bulma color: `primary`, `link`, `info`, `success`, `warning`, `danger`, `white`, `black`, `grey` (+ `grey-light`, `grey-dark`, …) | | `backgroundColor: '#…'` | `bgColor` (same palette) | | `fontSize: …` | `textSize="1"`…`"7"` (`1` largest) — for headings use `Title`/`SubTitle` `size` | | `fontWeight: …` | `textWeight`: `light`, `normal`, `medium`, `semibold`, `bold` | | `textTransform`, italics | `textTransform`: `uppercase`, `lowercase`, `capitalized`, `italic` | | `display: 'flex'` + flex properties | same-named props: `display="flex"`, `flexDirection`, `justifyContent`, `alignItems`, `flexWrap` | | `height: '100%'` on a flex child | `flexGrow="1"` | | `display: 'none'` | `visibility="hidden"`, or responsive `display*` props (`displayMobile`, `displayTablet`, …) | Spacing, typography, and flex helpers are on every component; `textColor`/`bgColor` are on the content components you'll compose with (`Box`, `Block`, `Title`, `Content`, `Card`, …) — the ones with a semantic `color` variant (`Tag`, `Tabs`, `Panel`) take `color` instead. `Notification` is the mixed case: it takes `textColor`, but its background comes from the semantic `color` prop, not `bgColor`. A value with no helper equivalent (`maxWidth: 720`, a one-off gradient) moves you to rung 2 — a named class in a stylesheet — never to inline `style`. **Rung 2 — a plain CSS file**, scoped under the component's class, consuming `--bulma-*` variables — never literal colors, so `Theme` and dark mode keep working: ```css /* src/components/MyComponent.css — import from the .tsx file */ .mycomponent { /* component-scoped custom props, initialized from Bulma tokens: any ancestor (or Theme) can re-theme by overriding them */ --mycomponent-radius: var(--bulma-radius); --mycomponent-accent: var(--bulma-primary); border-radius: var(--mycomponent-radius); border: 1px solid var(--bulma-border); background: var(--bulma-scheme-main); color: var(--bulma-text); } .mycomponent .mycomponent-value { color: var(--mycomponent-accent); } ``` Caveat: with the prefixed CSS flavor / `ConfigProvider classPrefix`, `usePrefixedClassNames` prefixes your classes too — your CSS selectors must match (or build them with plain `classNames` instead). **Rung 3 — real Sass (optional).** `npm i -D sass` — nothing else; Vite compiles imported `.scss` zero-config, and `bulma` is resolvable because it's a runtime dependency of bestax-bulma. Then the full `register-vars`/`getVar` pattern from `references/library-contributor.md` works in-app. Prefixed flavor: `@use 'bulma/sass/utilities/initial-variables' with ($class-prefix: 'bestax-')`. ## Verify in the browser Types don't see layout. Run `npm run dev`, render the component, and actually look at it: vertical centering of inline text (use `display="flex" alignItems="center"`, not line-height hacks), balanced padding, nothing clipping, every color/size variant, and **dark mode** legibility. Fix what you see, then re-check. No browser available (headless)? Fall back to `npm run build` plus a Node `renderToString` smoke render, grep the emitted HTML for the expected classes, and flag the visual pass as not done. ## Tests and stories in an app The scaffolded app has **no test runner and no Storybook** — do not install or scaffold them unasked. If the app already has vitest/jest + Testing Library, write the four test shapes: render, prop→class mapping, helper-prop passthrough (`m="3"` → `m-3`), and the `ConfigProvider classPrefix` case if the app uses a prefix. ## Checklist - [ ] Inventory checked (catalog + bestax.io/docs/api) and the decision surfaced to the user. - [ ] All imports from `@allxsmith/bestax-bulma` (no deep/internal paths). - [ ] Composition first — existing components + helper props before any CSS; every reusable component gets the spine. - [ ] No inline `style={{}}` anywhere — translate via the rung-1 mapping table; values with no helper equivalent get a named class (rung 2). - [ ] Lowest sufficient ladder rung (helper props → scoped CSS vars → Sass). - [ ] All colors/radii derived from `--bulma-*` variables — no literals. - [ ] Renders correctly via `npm run dev`, including dark mode.

bestax-form

Build forms with @allxsmith/bestax-bulma — Field/Control/Label/Help composition, inputs, selects, checkboxes, radios, switches, and advanced controls (Autocomplete, Slider, Numberinput, Rate, Taginput, File, date/time). There is no form/validation library; this skill shows the components and the validate-it-yourself error pattern. Use when building a form, wiring inputs to state, or showing validation/error state.

# Building forms with bestax-bulma This skill covers the form components in `@allxsmith/bestax-bulma` and how to compose them. **Important:** bestax-bulma ships **no form/validation library** — there is no integration with formik, react-hook-form, yup, or zod, and no `useForm`-style hook. You own your form state with plain React (`useState` / `useReducer` or any library you choose) and feed validation results back via each input's own `color`, `message`, and `messageColor` props on the convenience inputs (`Input`, `Select`, `TextArea`, …). Always put validation state on the **input**: `Field` has no `message`/`messageColor`, and although `FieldProps` types a `color`, `Field` discards it — it renders no class, so setting it looks right and does nothing. (`FieldLabel`/`FieldBody` do honor `color`, as the `has-text-*` helper.) See **Validation without a library** below. ## Use when - Building a form out of bestax inputs, selects, checkboxes, switches, or advanced controls. - Deciding between the convenience components (`<Input label=… />`) and explicit `Field` + `Control` + `*Base` composition. - Showing help text and error/success states on fields. To build a brand-new input component (not just use the existing ones), use the `bestax-custom-component` skill instead. ## Field / Control / Label / Help composition Bulma forms are a three-tier structure. bestax models it directly: ``` Field // container + layout (horizontal / grouped / hasAddons) ├── label // rendered from Field's `label` prop, or <Field.Label> when horizontal └── Control // wraps ONE input; adds icons + loading ├── InputBase / SelectBase / TextAreaBase // the raw styled element └── <p class="help">…</p> // help / validation message ``` You rarely write all of this by hand. The convenience components (`Input`, `Select`, `TextArea`, …) auto-wrap themselves in `Field` + `Control` when they aren't already inside one, using context (`useInsideField` / `useInsideControl`) to detect their surroundings. ```tsx // Convenience: one line, auto-wrapped in Field + Control. <Input label="Email" type="email" placeholder="you@example.com" /> // Explicit composition: full control over layout. <Field label="Email"> <Control iconLeftName="envelope" hasIconsLeft> <InputBase type="email" placeholder="you@example.com" /> </Control> </Field> ``` ### Layout via Field - `horizontal` — label and control side by side (wraps children in `Field.Body`; use `Field.Label` / `Field.Body` directly for multi-control rows). - `grouped` — `true | 'centered' | 'right' | 'multiline'`, controls in a row. - `hasAddons` — `true | 'centered' | 'right'`, attached controls (input + button). ```tsx <Field hasAddons> <Control isExpanded> <InputBase placeholder="Search" /> </Control> <Control> <Button color="primary">Go</Button> </Control> </Field> ``` ## Component inventory All import from `@allxsmith/bestax-bulma`. Convenience components auto-wrap Field+Control; `*Base` components are the raw styled elements for explicit composition. | Component | What it is | | ------------------------------------------------------- | ------------------------------------------------------------------------- | | `Field`, `Field.Label`, `Field.Body` | Field container + horizontal label/body parts. | | `Control` | Wraps one input; left/right icons, `isLoading`, `isExpanded`, `size`. | | `Input` / `InputBase` | Text input (convenience / raw). | | `Select` / `SelectBase` | Dropdown select. | | `TextArea` / `TextAreaBase` | Multiline text. | | `Checkbox` / `Checkboxes` | Single checkbox / managed group (array value). | | `Radio` / `Radios` | Single radio / managed single-select group. | | `Switch` | Toggle switch (`isRounded`, `isThin`, `isOutlined`, RTL). | | `File` | File upload input with label/message. | | `Autocomplete` | Input with filtered dropdown suggestions + keyboard nav. | | `Slider` | Range slider; single/dual thumbs, steps, tooltips, vertical. | | `Numberinput` | Numeric input with increment/decrement, min/max, step, stepper. | | `Rate` | Star rating; `max`, `precision` (half/quarter), custom icons, `disabled`. | | `Taginput` | Tag/chip input; suggestions, confirm keys, closable tags. | | `DateInput` / `TimeInput` / `DateTimeInput` (+ `*Base`) | Date / time / datetime pickers. | (`NumberInput` and `TagInput` also exist as deprecated aliases of `Numberinput`/`Taginput` — same components; prefer the lowercase-second-word spellings.) ## Common props Across the convenience inputs (`Input`, `Select`, `TextArea`, and similar): | Prop | Type | Purpose | | -------------------------------- | --------------------------------------------------------------------- | -------------------------------------------------------------------- | | `color` | `'primary' \| 'link' \| 'info' \| 'success' \| 'warning' \| 'danger'` | Visual state — use `'danger'` for errors, `'success'` for valid. | | `size` | `'small' \| 'medium' \| 'large'` | Input size. | | `value` / `onChange` | controlled value + handler | Standard React controlled inputs. | | `defaultValue` | uncontrolled initial value | When not controlling state. | | `disabled`, `readOnly` | `boolean` | Native states (`readOnly` on `*Base`). | | `label` | `ReactNode` | Field label (convenience components; auto-associated via `htmlFor`). | | `message` | `ReactNode` | Help / validation text rendered as `<p class="help">`. | | `messageColor` | a Bulma color | Colors the help text (`'danger'` for errors). | | `iconLeftName` / `iconRightName` | `string` | Icon shortcuts; pair with `hasIconsLeft/Right`. | | `isLoading` | `boolean` | Loading indicator on the Control. | Plus the full Bulma **helper props** (`m`, `p`, `textColor`, `display`, …) on every component via `useBulmaClasses`. Full-width is `isFullwidth` on every component that supports it (`Button`, `LinkButton`, `Select`, `File`, `Table`, `Tabs`, `Sidebar`) — always write `isFullwidth`. The deprecated spellings compile only where they historically existed: `isFullWidth` everywhere except `Sidebar`, `fullwidth` on `Tabs` only, `fullWidth` on `Sidebar` only. The `label` prop on the single-control convenience inputs (`Input`, `Select`, `TextArea`, `File`, `Numberinput`, `Slider`, `DateInput`, `TimeInput`, `DateTimeInput`, `Autocomplete`, `Taginput`) wires `htmlFor`/`id` automatically — your `id` is used when provided, a generated one otherwise, and an explicit `labelProps={{ htmlFor }}` wins. The wiring only happens when the component renders its own `Field` (nested inside one, the `label` prop is dropped); the date/time pickers skip it in `inline` mode and `Taginput` skips it at `maxTags` (no visible input to label). The group inputs (`Checkboxes`, `Radios`, `Rate`) associate their `label` too, but group-style: the wrapper gets `role="group"`/`"radiogroup"` and `aria-labelledby` pointing at the label. Composing `Field` + bases yourself also associates: `Field`'s own `label` wires to a single composed `InputBase`/`SelectBase`/ `TextAreaBase` (skipped for `grouped`/`hasAddons`). Pass `labelProps={{ htmlFor }}` plus a matching `id` only when you want a stable id, or `labelProps={{ htmlFor: undefined }}` to opt out — e.g. when the labeled `Field` wraps something that is not one of those bases. ## Convenience vs composed - **Convenience** (`<Input label message … />`) — for typical, single-control fields. Fewer lines, auto-wrapping, built-in `message`/`messageColor`. Default to this. - **Composed** (`Field` + `Control` + `InputBase`) — when you need grouped controls, addons, multiple controls per field, or custom layout. The convenience components detect they're already inside a `Field`/`Control` and won't double-wrap, so you can mix the two. ## Validation without a library There is no built-in validation. The pattern is: **own your state, compute errors yourself, and reflect them with `color` + `message` + `messageColor`.** ```tsx import { useState } from 'react'; import { Input, Button } from '@allxsmith/bestax-bulma'; function SignupForm() { const [email, setEmail] = useState(''); const [touched, setTouched] = useState(false); const valid = /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email); const error = touched && !valid ? 'Please enter a valid email address.' : undefined; return ( <form onSubmit={e => { e.preventDefault(); setTouched(true); if (valid) { // submit… } }} > <Input label="Email" type="email" value={email} onChange={e => setEmail(e.target.value)} onBlur={() => setTouched(true)} color={error ? 'danger' : undefined} message={error} messageColor={error ? 'danger' : undefined} iconLeftName="envelope" /> <Button color="primary" type="submit" mt="3"> Sign up </Button> </form> ); } ``` Rules of thumb: - Set `color="danger"` on the input **and** `messageColor="danger"` on the help text so both the control and the message read as an error. Use `'success'` to signal a valid field. - Want a different validation library? Wire it up yourself — pass its `value`/`onChange`/error string into these props. bestax does not prescribe one. - For grouped controls, render the `<p class="help">` via the `message` prop of the convenience component, or add it manually inside the `Field` when composing. See `references/api.md` for per-component props and `references/patterns.md` for a full multi-field form plus the advanced inputs. ## Reuse the shipped components bestax ships the whole form surface — Input, Select, TextArea, Checkbox(es), Radio(s), Switch, File, Autocomplete, Slider, Numberinput, Rate, Taginput, and the date/time inputs (see the inventory above). **Compose these; don't hand-roll raw `<input class="input">` markup or reinvent a control.** If you think a control is missing, check `bulma-ui/src/index.ts` and `docs/docs/api/form/` first — it's probably already there under a different name. ## What happens after submit A form is not finished at the last field. The two components that carry the result are easy to miss because Bulma has something that looks close: - **Confirmation** — `Toast`, not `Notification`/`Message`. Mount `<ToastContainer position="top-right" />` once at the app root, then call `toast.success('Demo booked')` from the submit handler (`.danger` for a failed submit). It self-dismisses. - **"Are you sure?"** — `Dialog`, not `Modal`. Mount `<DialogContainer />` at the root, then `if (await dialog.confirm({ title: 'Delete this key?', message: '…', type: 'danger' })) …`. It resolves to a boolean, so a destructive action stays one `if` rather than a state machine. `Modal` is an empty shell — with it you rebuild the title, message and button row by hand. Both also work as plain controlled components (`<Toast message … onClose>`, `<Dialog isOpen … onConfirm onCancel>`) when the state should live in your component. ## Visually inspect it in a browser Forms have layout, spacing, and _stateful_ behavior that types and unit tests don't cover. Before calling a form done, **render it and look at it**: run `pnpm storybook` (in `bulma-ui`) or the docs dev server, open the form, and check field alignment/spacing, the help-text/error states, and the validation flow (submit empty → fields turn `danger` with messages; fix → errors clear). If claude-in-chrome or Playwright is available, drive the browser and screenshot the valid and error states; otherwise eyeball it yourself. No browser at all (headless CI)? Fall back to a production build plus a Node `renderToString` smoke render, grep the emitted HTML for the expected classes/states, and say plainly that the visual pass is still owed. ## Checklist - [ ] Built from the shipped form components (no hand-rolled inputs / reinvented controls). - [ ] Every label is programmatically associated — the convenience `label` prop, the group inputs, and `Field` + single-base composition all do this automatically; pass `labelProps={{ htmlFor }}` plus a matching `id` only for a stable id, and label a multi-control `Field`'s controls individually (`aria-label`, `aria-labelledby`, or a `<label htmlFor>` matching each control's `id`). - [ ] Controlled inputs have both `value` and `onChange` (or use `defaultValue` uncontrolled). - [ ] Error state shows via `color="danger"` + `message` + `messageColor="danger"`. - [ ] Grouped/addon layouts use explicit `Field` + `Control` composition. - [ ] No assumption of a built-in validation/form library — state is owned by the app. - [ ] Submit feedback is a `Toast` and any "are you sure?" is a `Dialog` — not a hand-placed `Notification` or a `Modal` you filled in yourself. - [ ] **Rendered and visually inspected in a browser** — layout and the error/validation states look right, not just green tests. No browser available? The `renderToString` fallback above counts only if you grepped the emitted classes/states **and** said the visual pass is owed.

bestax-icons

Use icons in an app built with @allxsmith/bestax-bulma — the Icon/IconText components and the five supported libraries (Font Awesome, Material Design Icons, Ionicons, Google Material Icons, Material Symbols). Use when adding an icon, choosing or configuring the app-wide icon library, fixing an icon that renders blank, pairing icons with text, or making icons accessible (decorative vs labeled).

# Icons with @allxsmith/bestax-bulma `Icon` renders a Bulma icon container (`span.icon`) around a glyph from any of five icon libraries behind one normalized API. `IconText` pairs icons with text. The library ships **no icon fonts** — the chosen library's package (or CDN script) must be installed in the app. ## Quick start ```tsx import { ConfigProvider, Icon, IconText } from '@allxsmith/bestax-bulma'; // Set the library ONCE at the app root; <Icon> then never needs `library`. <ConfigProvider iconLibrary="fa"> <App /> </ConfigProvider>; // Inside the app: <Icon name="rocket" ariaLabel="Launch" />; <IconText iconProps={{ name: 'star', 'aria-hidden': 'true' }}> Starred </IconText>; ``` ## The five libraries | Library | `iconLibrary` / `library` value | Name format | Example `name` | | ---------------------- | ------------------------------- | ---------------------------- | --------------- | | Font Awesome (default) | `'fa'` | kebab-case, no `fa-` prefix | `rocket` | | Material Design Icons | `'mdi'` | kebab-case, no `mdi-` prefix | `rocket-launch` | | Ionicons | `'ion'` | kebab-case | `rocket` | | Google Material Icons | `'material-icons'` | snake_case (a text ligature) | `rocket_launch` | | Material Symbols | `'material-symbols'` | snake_case (a text ligature) | `rocket_launch` | ⚠️ **The Ionicons value is `'ion'`, not `'ionicons'`.** The `npm create bestax` scaffold's `--icon ionicons` flag maps to `iconLibrary="ion"` — passing `'ionicons'` to `ConfigProvider` or `library` silently renders nothing. **The same glyph has a different name per library** (`rocket` vs `rocket-launch` vs `rocket_launch`). When an icon renders blank, the name format for the active library is the first thing to check. A redundant `fa-`/`mdi-` prefix in `name` is tolerated (stripped), but don't rely on it. ## Styling - `size` — `'small' | 'medium' | 'large'` sizes the Bulma **container** (`is-small` ≈ 1rem, `is-medium` ≈ 2rem, `is-large` ≈ 3rem box). To scale the **glyph**, use `features` (Font Awesome `'fa-lg'`/`'fa-2x'`) or a Bulma text-size class (`'is-size-3'`). - `variant` — per-library style: Font Awesome `solid` (default) / `regular` / `brands` / `light` / `duotone` / `thin`; Material Icons `filled` (default) / `outlined` / `round` / `sharp`; Material Symbols `outlined` (default) / `rounded` / `sharp`; Ionicons `outline` / `sharp`. MDI has no variants. Note Material **Icons** uses `round`, Material **Symbols** uses `rounded`. - `features` — extra library classes, string or array: `'fa-spin'`, `['fa-lg', 'fa-border']`. - Color via the helper props: `textColor="primary"`, `textColor="danger"`, etc. ## Custom node (SVG, react-icons, FontAwesome React) `Icon` also accepts `children` instead of `name` — an inline SVG, a `react-icons` component, a Font Awesome React `<FontAwesomeIcon>`, … — rendered in place of a class-based glyph. `name` and `children` are mutually exclusive (the type rejects passing both, or neither). `size`, `textColor`, `bgColor`, `ariaLabel` and `containerClassName` behave identically; `library`, `variant`, `features` and `libraryFeatures` are ignored since there's no class-based glyph to style. ```tsx <Icon ariaLabel="Custom icon"> <MyReactIconsComponent /> </Icon> ``` `IconText`'s `iconProps` / `items[].iconProps` and `Control`'s `iconLeft`/`iconRight` accept the same escape hatch — pass a node directly (instead of an `IconProps` object) and it is wrapped in an `Icon` for you: `<IconText iconProps={<MySvg />}>Starred</IconText>`. `Panel.Icon` takes the same `children`, but its container is `panel-icon` rather than `icon` — it always overrides `containerClassName`, so style and query that class instead. ⚠️ **`children` excludes `undefined`.** Write a conditional icon as `cond ? <MySvg /> : null` (or `cond && <MySvg />`), never `cond ? <MySvg /> : undefined` — the latter is a type error, because the renderer would fall through to the `name` path with no name. In the `IconText` and `Control` slots a falsy node counts as "no icon": nothing is rendered, and `Control` falls back to `iconLeftName`/`iconRightName` if one is given, otherwise leaving the icon column unreserved. ## Accessibility Every `Icon` renders `aria-label` (default `"icon"`), set via its camelCase `ariaLabel` prop. Only a few components declare that prop (`Icon`, `Delete`, `Slider`, `Carousel`) — everything else takes the standard `aria-label` attribute, e.g. `<Navbar.Burger aria-label="menu" />`. - **Meaningful icon** (stands alone, conveys information): pass a descriptive `ariaLabel="Delete item"`. - **Decorative icon** (next to visible text that says the same thing, e.g. inside `IconText` or a labeled `Button`): hide it from screen readers with `aria-hidden`: `<Icon name="check" aria-hidden="true" />` — otherwise "icon" (or a duplicate label) is announced alongside the text. ## References - `references/icon-libraries.md` — per-library setup (install/import/CDN), the full name-format and variant tables, `features` values, and the blank-icon troubleshooting list. - `examples/icon-usage.tsx` — runnable example: ConfigProvider setup, sizes, variants, colors, IconText, and decorative-vs-labeled patterns.

bestax-layout-scaffold

Scaffold a complete, responsive page layout with @allxsmith/bestax-bulma — app shells/dashboards, marketing/landing pages, centered auth/settings pages, and card-grid catalogs. Use when building a full page or overall app layout (not a single component).

# Scaffolding a page layout with @allxsmith/bestax-bulma Turn a high-level request ("admin dashboard", "landing page", "login screen", "product catalog") into a complete responsive page built from bestax-bulma layout components. ## Behavioral rule Select an archetype from the request and build it in one shot. Do **not** ask layout questions ("how many columns?", "where should the nav go?", "what width?") — infer the structure from the request and proceed. The archetype determines the structure; fill it with the requested content. Ask **at most one** clarifying question, and only for a high-level fork the request genuinely does not imply: whether the page is **public-facing** (marketing) or an **internal tool** (authenticated app). When the request already signals this ("dashboard", "admin", "landing", "login", "pricing"), skip the question and default. ## Select an archetype | Request signals | Archetype | | ---------------------------------------------------------------------- | ------------- | | dashboard, admin, console, internal tool, authenticated app, "sidebar" | **App shell** | | landing, marketing, homepage, product/pricing page, public site | **Landing** | | login, sign up, auth, settings, checkout, a single focused form | **Centered** | | catalog, gallery, products, listing, "grid of cards", search results | **Card grid** | Default when ambiguous: internal tool → App shell; public-facing → Landing; one focused task → Centered; a collection of items → Card grid. For mixed requests, pick the dominant intent (e.g. "admin dashboard with a product list" → App shell whose main column holds a Card grid). ## Approach - Compose pages from the shipped layout components — `Container`, `Section`, `Hero`, `Footer`, `Level`, `Columns`/`Column`, `Grid`/`Cell`, `Navbar`, `Menu`, `Card`. There is **no `Tile` component**. For **uniform grids** (card grids, galleries — same-shaped items) prefer `Grid`/`Cell`: CSS Grid gives equal-height cells for free (per row — each row's cells match its tallest, same row-level behavior as the flex recipe). Use `Columns`/`Column` for proportional or per-breakpoint column layouts — and when cards there must be equal height, apply the flex recipe (`Column display="flex" flexDirection="column"` + `Card flexGrow="1"`; `height: 100%` on the card doesn't help — the column's height is auto). - Rely on Bulma's responsive defaults: `Columns` sit side by side on tablet and up and stack on mobile. Add responsive `size*` props only to tune the breakpoints. - Interactive extras don't share a state API — never transfer one by analogy: `Collapse trigger={node} open/defaultOpen onOpen/onClose`, `Tabs value={i}/onChange` (each `Tabs.Tab`/`Tabs.Content.Item` requires `index={i}`, and `Tabs.Content` must be a **child of `<Tabs>`** — the active-tab context lives on it; a sibling panel never switches), `Dropdown active/onActiveChange`, `Steps value={i}/onStepClick items={[{label, icon?}]}` (child form is `Steps.Step`, not `Steps.Item`). `Reveal cascade` staggers only its **direct children** — to stagger a grid, put `<Reveal delay={i * 80}>` inside each `Cell`, not around the container. - Link lists (footer nav, sidebars): a bare `UnorderedList` of `ListItem`s is already marker-less and flush — Bulma's reset unstyles `ul` — so no prop or CSS is needed; bullets appear only inside `Content`. - `Navbar.Burger`/`Navbar.Menu` are **controlled** — wire the same `active` state to both: `active` on `Navbar.Menu` shows/hides the mobile menu, while `active` + `onClick` on `Navbar.Burger` make the burger toggle it and animate. Left unwired, clicking the burger does nothing (no error, silent failure). For a `fixed="top"` `Navbar`, add the `has-navbar-fixed-top` class to `<html>` so content is not hidden behind it — the library does not do this automatically, and an inline padding offset is not a substitute. - **Style with helper props — no inline `style`, no raw Bulma `className`s.** Before writing `style={{ … }}` anywhere, translate each declaration with the mapping table below — the helper props cover the common cases. Bare markup has wrapper elements that take all helper props: `<Span textSize="7" textColor="grey">`, `Paragraph`, `Strong` — never a raw `<span className="is-size-7 has-text-grey">`. Table cells: `Th`/`Td` take `textAlign="right"`, `textWeight`, `textSize` directly (their `color` prop colors the cell; for muted cell text wrap content in `Span textColor="grey"`). Set the app-wide icon library once with `<ConfigProvider iconLibrary="…">` at the root rather than `library` on every `<Icon>`. - **Alternating section bands are a prop, not CSS: `bgColor="scheme-main-bis"` on every other `Section` (next band `scheme-main-ter`).** The scheme values render as a dark-mode-safe inline `background-color: var(--bulma-scheme-*)` — zero custom CSS — and still never `bgColor="light"`/`"white"`: those are fixed colors that stay light when dark mode flips the text. - **Decorative CSS is budgeted: two compact rules, ≤10 lines per app — comments count: at most one short inline note, never a file-header comment block — every value derived from `--bulma-*`.** A marketing page gets at most one hero wash and one featured-card ring, applied via `className` — no resets (Bulma ships one; body/list margins are already zero) and no grid textures, masks, or multi-layer backdrops; the components carry the design: ```css .hero-wash { background-image: radial-gradient( 60rem 30rem at 20% -10%, hsl(var(--bulma-primary-h) var(--bulma-primary-s) 50% / 0.2), transparent 60% ); } .featured-ring { --bulma-shadow: 0 0 0 2px var(--bulma-primary); } ``` The ring works by overriding the **upstream token**, not the component's own var: `.card` and `.box` re-declare `--bulma-card-shadow`/`--bulma-box-shadow` from `--bulma-shadow` on their own selector, so setting _those_ from an ancestor never wins (same for `--bulma-box-radius`; `--bulma-card-radius` is a literal with no ancestor route at all). A class-scoped CSS rule keeps the ring on just the one featured card without wrapping it in its own `Theme`; `<Theme bulmaVars={{ '--bulma-shadow': … }}>` also compiles and is the better fit when the override already applies to a whole scoped subtree. Either way the subtree stays theme- and dark-mode-aware. - **CTAs on a colored hero must stay legible in both schemes.** On a fixed-color surface (`Hero color="primary"`, a dark banner), use **filled** buttons — `color="light"` or `color="primary" isInverted` — never a thin `isOutlined` secondary: a light outline + light label on a dark surface is low-contrast and gets worse under OS dark mode. And when the page's design is single-mode (a fixed light or dark look), pin it at the root — `<Theme isRoot colorMode="light">` — so a visitor's OS dark mode can't flip Bulma's text colors out from under the fixed palette (details: the `bestax-theming` skill's contrast rules). ## Three components core Bulma will talk you out of Most of this library's additions get found on their own, because nothing in Bulma does the job. These three do not: each has a Bulma near-miss close enough to stop the search. Across 44 cold-start builds, `Dialog` was used **zero** times and `LinkButton` in two thirds — and every miss shipped the "not this" column instead. | You need | Use | Not this | | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | A brief confirmation after an action, self-dismissing | `Toast` — mount `<ToastContainer position="top-right" />` once at the app root, then `toast.success('Saved')` (also `.danger`/`.warning`/`.info`) from anywhere | `Notification`/`Message` | | A confirm or alert the user must answer before anything | `Dialog` — mount `<DialogContainer />` at the root, then `if (await dialog.confirm({ title, message })) …` | `Modal` — an empty shell; the title, message, button row and confirm/cancel wiring are all yours to rebuild | | A control that reads as text or a link but _does_ something | `LinkButton` (`variant="text" \| "ghost" \| "underline"`, optional `color`) | `<a href="#">`/`<div onClick>` (no keyboard or screen-reader support) or `Button color="text"` (still button-shaped) | Mounting a container without ever calling `toast.*`/`dialog.*` is not usage — the container is the mount point, the imperative call is the thing that shows something. Both also work as ordinary controlled components when you would rather hold the state yourself — `<Toast message … duration onClose>`, `<Dialog isOpen title message type onConfirm onCancel>` — but in an app with more than one call site the root container plus the imperative helper is less wiring, not more. ## Inline style → helper prop mapping Look up the declaration you were about to inline. The spacing, typography, and flex helpers below are on every component; `textColor`/`bgColor` are on the content components you'll target (`Box`, `Block`, `Title`, `Content`, `Hero`, `Card`, …) — the ones with a semantic `color` variant (`Tag`, `Tabs`, `Panel`) take `color` instead; wrap content in a `Block` if you need a text color there. `Notification` is the mixed case: it takes `textColor`, but its background comes from the semantic `color` prop, not `bgColor`. | Inline style you're about to write | Helper props instead | | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `marginTop: '1rem'` (any margin/padding) | `mt="4"` — `m`/`mt`/`mx`/`p`/`py`/… scale: `1`=0.25rem, `2`=0.5rem, `3`=0.75rem, `4`=1rem, `5`=1.5rem, `6`=3rem (nearest step) | | `textAlign: 'center'` | `textAlign="centered"` (also `left`, `right`, `justified`) | | `color: '#…'` | `textColor` with the nearest Bulma color: `primary`, `link`, `info`, `success`, `warning`, `danger`, `white`, `black`, `grey` (+ `grey-light`, `grey-dark`, …) | | `backgroundColor: '#…'` | `bgColor` (same palette) | | `fontSize: …` | `textSize="1"`…`"7"` (`1` largest) — for headings use `Title`/`SubTitle` `size` | | `fontWeight: …` | `textWeight`: `light`, `normal`, `medium`, `semibold`, `bold` | | `textTransform`, italics | `textTransform`: `uppercase`, `lowercase`, `capitalized`, `italic` | | `display: 'flex'` + flex properties | same-named props: `display="flex"`, `flexDirection`, `justifyContent`, `alignItems`, `flexWrap` | | `height: '100%'` on a flex child | `flexGrow="1"` | | `display: 'none'` | `visibility="hidden"`, or responsive `display*` props (`displayMobile`, `displayTablet`, …) | | `gap: …` in a flex layout | no `gap` helper exists — space children with `m*` margins; `Grid` and `Columns` take a `gap` prop, so prefer those there | No helper matches (e.g. `maxWidth`, a one-off gradient)? Add a named class to the project stylesheet (`src/App.css` in a scaffolded app) and pass it via `className` — still never inline `style`. ## References - `references/layout-components.md` — the layout component inventory: real prop names, types, and accepted values, plus subcomponent nesting. - `references/archetypes.md` — the four archetypes: selection criteria, JSX skeleton, and responsive behavior. ## Examples - `examples/app-shell.tsx` — fixed `Navbar` + sidebar `Menu` + content (dashboard). - `examples/landing.tsx` — fixed `Navbar` (controlled burger) + `Hero` + `Section`s + `Footer`. - `examples/centered.tsx` — centered single column (auth/settings). - `examples/card-grid.tsx` — multiline `Columns` of `Card`s (catalog). - `examples/content-page.tsx` — hero + feature cards + CTA styled with helper props (no inline `style`), wrapped in `ConfigProvider`. ## Checklist - [ ] Map the request to one archetype; do not ask layout questions. - [ ] Wrap page content in `Container` (+ `Section` for vertical rhythm). - [ ] Use `Grid`/`Cell` for uniform grids (equal heights per row, free); `Columns`/`Column` for proportional or per-breakpoint side-by-side layout — with the flex recipe when its cards must match height. - [ ] Wire `active` state to **both** `Navbar.Burger` and `Navbar.Menu` (they are controlled). - [ ] For a fixed navbar, add `has-navbar-fixed-top` to `<html>`. - [ ] Do not use `Tile` — it is not shipped. - [ ] Action feedback goes through `Toast`, a confirmation through `Dialog`, a text-styled action through `LinkButton` — not `Notification`, `Modal` or a bare `<a>`. - [ ] Style with helper props, not inline `style` — translate via the mapping table; values with no helper get a named class in the stylesheet, never `style={{}}`. No raw Bulma `className`s either (`Span`/`Paragraph` wrap bare text; `Th`/`Td` take `textAlign`/`textWeight`). - [ ] Alternating bands are `bgColor="scheme-main-bis"` (next band `scheme-main-ter`) on every other `Section` — never band CSS, never `bgColor="light"`/`"white"`. - [ ] Decorative CSS ≤10 lines total incl. comments — no file-header comment (hero wash + featured-card ring), `--bulma-*`-derived; no resets — Bulma ships one. The ring sets `--bulma-shadow` (via a scoped CSS rule or `Theme bulmaVars`). - [ ] Set the icon library once via `<ConfigProvider iconLibrary="…">` at the root. - [ ] Site built? ~800 KB raw / ~82 KB gzip CSS is the expected default-flavor size — to shrink it, run the `bestax-optimize` skill (measure first).

bestax-migrate

Migrate an existing React app to @allxsmith/bestax-bulma on Bulma v1, from raw Bulma CSS classes on plain JSX (className="button is-primary") or from an unmaintained React Bulma library (react-bulma-components v4, rbx v2, bloomer 0.6). Run the bestax-migrate codemod, then resolve every TODO(bestax-migrate) comment it leaves using the per-source mapping references. Use when a React app styles its markup with Bulma classes and wants bestax components instead, when a repo imports react-bulma-components, rbx or bloomer, when TODO(bestax-migrate) comments are present in a codebase, or when asked to convert Bulma classNames to bestax-bulma.

# Migrating to bestax-bulma `@allxsmith/bestax-bulma` is an actively maintained React library for **Bulma v1**. The `bestax-migrate` codemod automates most of the conversion from an unmaintained predecessor; this skill drives the codemod and finishes what it flags. ## Pick the source The codemod's first argument names the library you are migrating _from_. Check the app's `package.json` and imports: | Source | Argument | References | | -------------------------------------------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------ | | `react-bulma-components` v4 (unmaintained since 2022, Bulma 0.9.x) | `react-bulma-components` | [`references/react-bulma-components/`](references/react-bulma-components/component-map.md) | | `rbx` v2 (abandoned 2019, pins Bulma **0.7.5** plus four extensions) | `rbx` | [`references/rbx/`](references/rbx/component-map.md) | | `bloomer` 0.6 (archived 2018, Bulma 0.6 era, React 16 + `create-react-class`) | `bloomer` | [`references/bloomer/`](references/bloomer/component-map.md) | | Raw Bulma classes on plain JSX (`<div className="box">`), no React Bulma library | `bulma-classes` | [`references/bulma-classes/`](references/bulma-classes/component-map.md) | Everything below is written as `<source>`; substitute the argument from that table. The reference paths follow the same split — `references/<source>/component-map.md`, `prop-map.md`, `unmappables.md` — while [`references/css-migration.md`](references/css-migration.md) is shared, because the stylesheet work is about Bulma, not about which wrapper you came from. ## Workflow Run these steps in order. Don't hand-convert what the codemod converts automatically. 1. **Dry-run the codemod** on the source directory and review the report: ```sh pnpm dlx bestax-migrate <source> src/ --dry ``` 2. **Apply it** (same command without `--dry`), then run the project's formatter — the codemod preserves surrounding formatting but doesn't prettify what it rewrites. Besides the components, it also migrates **stylesheets** (CSS imports → `@allxsmith/bestax-bulma/bestax.css`; SCSS `@import 'bulma/bulma'` + `$var` overrides → `@use 'bulma/sass' with (…)` plus the bestax extras) and **package.json**. Flags: `--css bulma|keep` for other stylesheet targets, `--no-deps` to leave package.json alone. `bulma-classes` is the exception: it defaults to `--css keep`, because the app's own Bulma already styles every class it converts, so its stylesheets and Bulma version stay unless you pass `--css bestax` or `--css bulma`. 3. **Install** — the codemod edits package.json but never runs a package manager. Resolve the retained imports first: a component with no bestax equivalent keeps a trimmed, TODO-annotated import of the source library, which the manifest step has just removed — so a clean install leaves those unresolvable. The report names them in a `deps` entry. ```sh npm install # or pnpm/yarn ``` The report groups its entries by rule. `deps` covers every package.json edit — what was removed, added, or left for you to decide; `imports` covers a source import the codemod could not rewrite and left in place; `unsupported-file` names a file type it cannot parse (`.vue`, `.astro`) that still imports the source; `value-reference` marks a component used as a value rather than as JSX. Rules named `component:X` and `prop:y` are per-component and per-prop, and each is documented in the per-source `references/` pages. The report's `peer-deps` entries predict install failures: bestax-bulma needs **React 18/19** (react-bulma-components also ran on 17, while rbx and bloomer peer-depended on **React 16** — upgrade react/react-dom first) and its optional Font Awesome peer wants **FA ≥ 6.7** (an app pinned to FA 5 either upgrades or installs with `npm install --legacy-peer-deps`). 4. **Resolve every TODO**: `grep -rn "TODO(bestax-migrate)" src/`. Each comment names the prop/component and a hint. Recipes for every recurring case are in `references/<source>/unmappables.md`. Before writing a component by hand, look it up in `references/<source>/component-map.md` and its classes in `references/<source>/prop-map.md`: they name the component and the prop for each class, which the library's type declarations only tell you slowly. For form markup (`.field`, `.control`, inputs, selects), load the `bestax-form` skill too: it covers the label, `id` and help-text wiring. Delete each comment as you resolve it. 5. **Finish the stylesheet layer** — flagged Sass cases (computed variables, indented-syntax `.sass` files), CSS flavor choice, and Bulma 0.9→1 styling changes: follow [references/css-migration.md](references/css-migration.md). 6. **Finish**: typecheck/build, and review the rendered app side by side against the pre-migration UI. ## What the codemod handles vs. flags Every export of every supported source has a mapping entry, held to that library's real export surface by a coverage test. Imports (named, namespace, and destructuring), component renames, prop renames and value conversions, breakpoint objects, CSS/SCSS stylesheet imports, and package.json dependencies convert automatically. It flags with `TODO(bestax-migrate)` instead of guessing: components with no bestax equivalent, controlled APIs whose shape differs, breakpoints bestax has no prop for, dynamic prop values it can't rewrite, and props with no counterpart. Files in formats it can't parse (`.astro`, `.vue`, `.svelte`, `.mdx`) that import the source library are reported as `unsupported-file` — migrate those by hand with the component map. Never "fix" a TODO by silencing it — convert the code per the references, or deliberately keep the old markup with `className` styling. ### Source-specific headlines - **react-bulma-components**: all 32 v4 components are mapped. `Element` and `Tile` have no bestax equivalent; `renderAs` becomes `as` where supported. - **rbx**: migrating removes rbx itself — it pinned `bulma@0.7.5` as a direct dependency plus `bulma-badge`, `bulma-divider`, `bulma-pageloader` and `bulma-tooltip`, so the app can finally choose its own Bulma version. rbx's badge and tooltip _helper props_ become real wrapping `<Badge>` / `<Tooltip>` components. Its `as` is universal, bestax's is not. Because rbx pinned Bulma 0.7.5, you cross **two** Bulma majors — expect more visual drift than the 0.9 → 1 guide alone describes. - **bulma-classes**: there is no library to remove. An element converts only when the bestax component renders the same markup (tag, classes, attributes), so a converted page renders the same HTML; everything else stays as written, with a TODO when there is something to decide. Static class strings convert, and so does a `clsx` or `classnames` call of strings and conditional classes, where a condition on a flag becomes its prop (`isLoading={busy}`); any other computed className gets `dynamic-class:<Target>`, naming the component the element would become. Files a Next.js App Router may render as server components are left alone (`rsc`), and the manifest gains `@allxsmith/bestax-bulma` while the app's Bulma and stylesheets stay (the default `--css keep`). Its rules are `kind:<name>` shaped; see `references/bulma-classes/unmappables.md`. - **bloomer**: every export is a flat name and most become dotted bestax compounds (`CardHeaderTitle` → `Card.Header.Title`). Most of its `is*` booleans already are bestax's (`isActive` becomes `active` where bestax names it so; `isFullWidth` becomes `isFullwidth` where bestax has the prop); the work is in `isSize`/`isColor`/`isAlign` (renamed per component), the `isDisplay`/`isHidden` helpers (flattened, arrays and objects included), `tag` (→ `as` where bestax has one) and `render` (always a TODO). Its icons are className-based and Font Awesome 4 — the biggest visual risk, see `references/bloomer/unmappables.md`. The app declared its own Bulma 0.6, which the codemod bumps; there is no pinned Bulma to free. ## Rules - The codemod is safe to re-run after partial manual work: a library source only touches files that still import the library, and `bulma-classes` skips elements that are already components and never repeats a TODO it already wrote. - Don't downgrade converted props back to the old names; bestax uses `is*` booleans (`isLoading`), string size unions (`textSize="4"`), and `as` rather than `renderAs`. - If a component the app uses isn't in the component map, it wasn't part of that library's public API — check for a local wrapper component and migrate its internals instead.

bestax-optimize

Reduce the built CSS size of an app using @allxsmith/bestax-bulma — measure raw and gzip size, then apply the cheapest lever that fits (lighter prebuilt CSS flavor, hand-rolled modular Sass build, import and icon-asset hygiene). Use when the CSS bundle looks too big, a size budget or Lighthouse audit flags stylesheet weight, switching bestax.css to a versions/*.css flavor, or setting up a modular @use Bulma/Sass build.

# Optimizing CSS size with @allxsmith/bestax-bulma The JS side is tree-shakable (the **entire** library is ~49 KB min+gzip). The stylesheet is not: a prebuilt flavor ships all of Bulma + the bestax extras regardless of which components the app renders. Judge stylesheet weight by the **gzipped** transfer size — never the raw `dist/` number. ## Always measure first Run the production build, then measure the built CSS — raw and gzip: ```sh npm run build wc -c dist/assets/*.css # raw bytes (what Vite reports) gzip -c dist/assets/*.css | wc -c # transfer bytes (what users download) ``` Re-measure after **every** step below and report the before/after delta (raw + gzip). Never claim a saving without a number. ⚠️ **~800 KB raw is ~82 KB over the wire.** Seeing ~800 KB of CSS in `dist/` is expected with the default flavor, not a build misconfiguration — servers and CDNs gzip/brotli by default. If the gzip number is already within the app's budget, say so and stop: "your CSS is already fine" is a valid, honest outcome. ## Decision flow Cheapest first. Use only the library's own shipped CSS builds and SCSS sources — do not add third-party build plugins (no CSS purgers/post-processors). 1. **Flavor switch** — one-line import change, minutes (up to ~15 KB gzip). 2. **Modular Sass build** — small `@use` file, the biggest honest win. 3. **Import & icon-asset hygiene** — minor; JS and icon fonts, not the Bulma CSS. ## Lever 1 — lighter prebuilt flavor Swap the single CSS import (in `src/main.tsx` / `src/main.jsx`) between the shipped flavors. Measured sizes drift slightly between releases — hence measure: | Flavor | Import | Raw | Gzip | | ------------ | -------------------------------------------------------------------- | -----: | -----: | | complete | `import '@allxsmith/bestax-bulma/bestax.css';` | ~800KB | ~82 KB | | no-dark-mode | `import '@allxsmith/bestax-bulma/versions/bestax-no-dark-mode.css';` | ~680KB | ~70 KB | | no-helpers | `import '@allxsmith/bestax-bulma/versions/bestax-no-helpers.css';` | ~595KB | ~67 KB | Prefixed variants (`versions/bestax-prefixed.css` ~875 KB/~84 KB, `versions/bestax-no-helpers-prefixed.css` ~655 KB/~69 KB) exist for CSS-collision **compatibility, not size** — a prefixed flavor must pair with `<ConfigProvider classPrefix="bestax-">` at the app root, and switching a non-prefixed app to one is never a size optimization. - `no-dark-mode` (~12 KB gzip saved) — only when the app pins a single color scheme (a fixed `Theme colorMode` / `data-theme`). - `no-helpers` (~15 KB gzip saved) — **hard gate below**. ⚠️ **The `no-helpers` gate: check for helper props first.** That flavor drops every class the helper props compile to — components still render, but the props silently do nothing. Before recommending it, grep the app's source for helper props on bestax components: - spacing: `m`, `mt`, `mr`, `mb`, `ml`, `mx`, `my`, `p`, `pt`, `pr`, `pb`, `pl`, `px`, `py` - color: `color`, `backgroundColor` (+ `colorShade`/`backgroundColorShade`), `textColor`, `bgColor` - typography: `textSize`, `textAlign`, `textTransform`, `textWeight`, `fontFamily` - display/visibility: `display`, `visibility` (+ `displayMobile`…`visibilityFullhd` viewport variants) - flexbox: `flexDirection`, `flexWrap`, `justifyContent`, `alignContent`, `alignItems`, `alignSelf`, `flexGrow`, `flexShrink` - misc: `float`, `overflow`, `overlay`, `interaction`, `cursor`, `radius`, `shadow`, `skeleton`, `clearfix`, `relative`, `fullHeight` …and for raw Bulma helper classes in `className` strings (`is-*`, `has-*`, `m*-*`, `p*-*`, `is-size-*`, `is-hidden*`, `is-flex*`). **Any hit → do not use `no-helpers`**; fall back to `no-dark-mode` or Lever 2. The bestax extras helpers (`is-cursor-*`, sizing) are dropped too. ## Lever 2 — modular Sass build Compile only the Bulma modules + bestax extras partials the app actually uses. Needs the `sass` compiler as a dev dependency (Bulma's own build tool — Vite compiles `.scss` natively): ```sh npm install -D sass ``` Create `src/styles.scss` — base + themes always come first: ```scss // Configure shared variables FIRST (before anything loads utilities) — // the prebuilt flavors set this brand primary; omit it and buttons revert // to Bulma's default turquoise: @use 'bulma/sass/utilities' with ( $primary: #1e6b99 ); // Required base: reset + CSS variable definitions @use 'bulma/sass/base'; @use 'bulma/sass/themes'; // One line per stock Bulma component the app imports… @use 'bulma/sass/elements/button'; @use 'bulma/sass/components/navbar'; // …and per bestax extras component: @use '@allxsmith/bestax-bulma/scss/components/dialog'; // Helper categories only if the app uses those helper props: @use 'bulma/sass/helpers/spacing'; ``` Then replace the prebuilt CSS import in `src/main.tsx` with `import './styles.scss';`, build, and measure. Derive the `@use` list from the components the app imports from `@allxsmith/bestax-bulma` — the full component→partial mapping, the authoritative extras partial inventory, and a worked example are in `references/modular-build.md`. ## Lever 3 — import & icon-asset hygiene (minor) - **Named imports.** `import * as Bestax from '@allxsmith/bestax-bulma'` defeats tree shaking. Convert to named imports (`import { Button, Card } from …`) — a JS-side saving (whole library ≈ 49 KB min+gzip), so report it honestly as minor. - **Unused icon libraries.** Scaffolded apps may carry an icon library the app never uses: icon-font packages in `package.json` (`@fortawesome/fontawesome-free`, `@mdi/font`, `material-icons`, `material-symbols`) with their CSS imports in `src/main.*`, or Ionicons CDN `<script>` tags in `index.html`. Icon fonts often outweigh Bulma itself — if no `<Icon>` uses that library, delete the dependency/import/script. Pure win, no tooling. ## References - `references/modular-build.md` — full Lever 2 procedure: component→partial mapping, the authoritative extras partial inventory, helper categories, and a worked `styles.scss`. - Docs: [Optimizing CSS Size](https://bestax.io/docs/guides/getting-started/optimizing-css) · [Modular — Option C](https://bestax.io/docs/guides/getting-started/modular#option-c--hand-rolled-modular-scss-advanced) · [CSS Variations](https://bestax.io/docs/guides/getting-started/variations#file-size-comparison)

bestax-theming

Customize colors, branding, dark mode, and visual tokens of an app built with @allxsmith/bestax-bulma. Use when changing the primary/brand color, recoloring components, overriding Bulma --bulma-* CSS variables, setting fonts/radius/spacing tokens, adding light/dark mode, or configuring the app-wide icon library / class prefix via ConfigProvider.

# Theming @allxsmith/bestax-bulma `@allxsmith/bestax-bulma` wraps Bulma 1.x, which is themed through `--bulma-*` CSS custom properties. Theme an app by overriding the right variables — no component re-styling required. ## Approach Recolor a brand color by overriding its **hue/saturation/lightness trio** — Bulma derives every shade, light/dark, and invert variant from `--bulma-<color>-h` / `-s` / `-l`. Override the trio and the whole palette follows. Choose an override path: - **`Theme` component (runtime, preferred).** Exported from the package. Pass named HSL props (`primaryH`, `primaryS`, `primaryL`, …) and/or `bulmaVars={{ '--bulma-*': '…' }}` for everything else. Add `isRoot` to inject the variables globally at `:root` (once, at the app root); omit it to scope the variables to the wrapped subtree. - **Plain CSS.** Set `:root { --bulma-primary-h: …; }` (or any selector) directly. - **Build-time Sass.** `@use 'bulma/sass' with ($primary: #1e6b99)` when compiling Bulma's Sass. For **dark mode**, pass `colorMode` to `Theme` (`'light' | 'dark' | 'system'`). It writes Bulma's `data-theme` attribute on `<html>`, flipping the light/dark scheme — global, even on a scoped `Theme`; `'system'` follows the OS `prefers-color-scheme`. Drive it from state on the app-root `Theme`: `<Theme isRoot colorMode={mode}>`. ## Contrast rules (dark mode is on by default) When nothing sets a `data-theme` attribute (omitting `colorMode` preserves an existing one, but apps that never configured it have none), Bulma follows the visitor's OS: `--bulma-text`, `--bulma-scheme-main`, etc. flip on a dark-mode machine even if the design never intended a dark theme. Custom fixed tokens (`--my-canvas: #f6f4ec`) do **not** flip — producing near-white Bulma text on the author's fixed light background. Apply exactly one of these rules whenever custom color tokens or fixed-color surfaces exist: - **Single-mode design → pin the scheme.** `<Theme isRoot colorMode="light">` (or `"dark"`), so an OS preference can never invert text out from under the fixed palette. - **Both modes → no exposed fixed tokens.** Derive custom tokens from scheme variables (`--my-canvas: var(--bulma-scheme-main)`) — or flip them yourself under **both** dark-mode paths: `[data-theme='dark']` **and** `@media (prefers-color-scheme: dark)` scoped to `:root:not([data-theme])`, since `colorMode="system"` removes the attribute (snippets in `references/css-variables.md`). Alternating/tinted section bands are first-class props: `bgColor="scheme-main-bis"` (then `"scheme-main-ter"`) on `Section` (also `Hero`, `Footer`, `Container`, `Box`, `Card`) renders a scheme-tracking inline background — never `bgColor="light"`: `light`/`white`/grey helper backgrounds are fixed colors that fight dark mode. Keep the derive-from-scheme-vars CSS for other custom surfaces. `Theme bulmaVars` now accepts `--bulma-scheme-main`/`-bis`/`-ter` (and the `-invert` trio) overrides, so one Theme re-tints every band at once. - **Fixed-color surface → fixed-color content.** On a surface that never changes (a dark hero, a brand banner), pin the content's colors too: solid/filled buttons and explicit text colors, never scheme-derived defaults or thin outlines that depend on the flipping scheme. Reach for the helper props (`color` / `textColor` / `bgColor` / `colorShade`, `textSize`, `textWeight`, `fontFamily`) to apply themed colors and type to individual components. Variant flags and value unions are component-specific — never carry one over by analogy: `isLight` exists on `Button` and `Notification` **only** (`Tag` has none, and `LinkButtonProps` omits it); `Tag size` is `normal | medium | large` (no `small`, unlike `Button`); `Buttons` has `isCentered`, `Tags` does not (center tags with `justifyContent="center"`); the verbatim truth table is `references/themeable-components.md`. ## Quick start ```tsx import { Theme, Button } from '@allxsmith/bestax-bulma'; // Global brand theme at the app root. <Theme isRoot primaryH="265" primaryS="65%" primaryL="55%"> <App /> </Theme>; // Themed components recolor automatically. <Button color="primary">Save</Button>; ``` ## App-wide config (icons & class prefix) `ConfigProvider` sets app-wide options once at the root, separate from `Theme`. Wrap the app so you don't repeat the same prop on every component: ```tsx import { ConfigProvider } from '@allxsmith/bestax-bulma'; // Set the icon library once — <Icon> no longer needs a `library` prop. <ConfigProvider iconLibrary="fa"> <App /> </ConfigProvider>; // now <Icon name="check" /> resolves as Font Awesome; no per-icon library="fa". ``` - `iconLibrary` — `'fa' | 'mdi' | 'ion' | 'material-icons' | 'material-symbols'`. `Icon` reads it (`library || iconLibrary || 'fa'`), so set it here instead of on each `<Icon>`. - `classPrefix` — namespaces every Bulma class (e.g. `bulma-`) to avoid collisions with other CSS. Nest `Theme` and `ConfigProvider` together at the root (order doesn't matter). ## References - `references/css-variables.md` — the `--bulma-*` variable map (colors, scheme/text/border, radius, fonts, sizes, weights, dark mode) and all three override mechanisms. - `references/themeable-components.md` — which components take `color`/`size` props, the real accepted values, and the shared helper props. ## Examples - `examples/theme-config.tsx` — a custom brand theme at the app root, plus a scoped override. - `examples/dark-mode.tsx` — a light/dark toggle using Bulma's `data-theme`. ## Checklist - [ ] Recolor brand colors via the HSL trio (`*-h` / `*-s` / `*-l`), not by hard-coding hex on components. - [ ] Apply a global theme once with `<Theme isRoot>` (or `:root`); use scoped `<Theme>` for one-off sections. - [ ] Set non-color tokens (radius, fonts, sizes) through `bulmaVars` or `:root`; a custom `--bulma-family-*` needs its font actually loaded (`index.html` `<link>` or an `@fontsource` import). - [ ] Implement dark mode with `data-theme` on `<html>`; do not expect a shipped dark-mode component. - [ ] Pass `color`/`textColor`/`bgColor` (not custom CSS) to color individual components. - [ ] Set the icon library once with `<ConfigProvider iconLibrary="…">` at the root, not `library` on every `<Icon>`.