From 173e1c9109cbd5e26165682587679e5d6438447f Mon Sep 17 00:00:00 2001 From: Alex Smith Date: Thu, 23 Jul 2026 22:40:25 -0400 Subject: [PATCH 1/5] feat(create-bestax): agent-validated guidance for skills, scaffold CLAUDE.md, and catalog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Distilled from a 10-iteration cold-start eval loop (baseline 85/100 -> revised mean 95.2, builder cost -43%). Every fact verified against bulma-ui source before writing; every change validated by at least one subsequent cold-start build. Full evidence on branch chore/skill-improvement-loop (experiment/skill-loop/report.md). πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- create-bestax/src/constants.ts | 18 +++- scripts/gen-component-catalog.mjs | 21 ++++- skills/bestax-custom-component/SKILL.md | 21 +++-- .../examples/stat-card.tsx | 1 + .../bestax-custom-component/references/api.md | 4 +- .../references/component-catalog.md | 21 ++++- skills/bestax-form/SKILL.md | 19 +++- skills/bestax-form/references/api.md | 5 +- skills/bestax-icons/SKILL.md | 4 +- skills/bestax-layout-scaffold/SKILL.md | 53 +++++++++-- .../examples/app-shell.tsx | 4 +- .../examples/card-grid.tsx | 7 +- .../references/archetypes.md | 16 +++- .../references/layout-components.md | 14 +-- skills/bestax-theming/SKILL.md | 17 ++-- .../bestax-theming/examples/theme-config.tsx | 2 + .../references/css-variables.md | 4 + .../references/themeable-components.md | 87 ++++++++++--------- 18 files changed, 230 insertions(+), 88 deletions(-) diff --git a/create-bestax/src/constants.ts b/create-bestax/src/constants.ts index 16000a7b4..69ab50b2a 100644 --- a/create-bestax/src/constants.ts +++ b/create-bestax/src/constants.ts @@ -133,16 +133,29 @@ ${setupLines.join('\n')} ## House style -- Never inline \`style={{}}\` β€” use the helper props every component accepts (\`m*\`/\`p*\` - spacing, \`textColor\`/\`bgColor\`, \`display="flex"\`, \`flexDirection\`, \`alignItems\`). +- Never inline \`style={{}}\` and never hand-write Bulma utility classes β€” use the shared + helper props (\`m*\`/\`p*\` spacing, \`textColor\`/\`bgColor\`, \`display="flex"\`, \`flexDirection\`, + \`alignItems\`). Bare text/markup has wrapper elements that take the same helper props: + \`Span\`, \`Paragraph\`, \`Strong\` β€” not \`\`. The one exception: + companion classes Bulma requires on \`\`/\`\` (e.g. \`has-navbar-fixed-top\` with + \`Navbar fixed="top"\`) are hand-added in \`index.html\` β€” no component renders those elements. Flex layouts have no \`gap\` helper β€” space children with margins (\`Grid\` and \`Columns\` take a \`gap\` prop, so prefer that there). +- Compound sub-parts (\`Card.*\`, \`Modal.*\`, \`Tabs.*\`, \`Message.*\`) take only \`className\` + + HTML attributes β€” no helper props, no \`as\`/\`href\`: nest a \`Link\`/\`Span\` inside instead. + One required addition: \`Tabs.Tab\` and \`Tabs.Content.Item\` each need \`index={i}\`. - Compose existing components before writing custom CSS; theme via \`Theme\` and \`--bulma-*\` variables, never hardcoded colors. +- Reusable components you write get the library's spine so helper props work on them too: + extend \`BulmaClassesProps\`, merge \`className\`, spread \`...rest\` β€” the + bestax-custom-component skill has the full template. - There is no test runner or Storybook in this app β€” don't assume one. - \`index.html\`'s \`\` starts as the project name and \`README.md\` is stock template boilerplate β€” once this app has a real identity, set the title (and any meta tags) to match it and rewrite the README to describe *this* app, not the template. +- Before adding a dependency, match the package manager to the app's lockfile + (\`pnpm-lock.yaml\` β†’ pnpm, \`package-lock.json\` β†’ npm, \`yarn.lock\` β†’ yarn) β€” a mismatched + install fails or forks the lockfile. ## AI skills @@ -158,6 +171,7 @@ automatically when the task matches: - **bestax-migrate** β€” migrate code off react-bulma-components (v4): run the codemod, resolve its TODOs. Prefer the library's components and these skills over hand-written Bulma markup or custom CSS. +Read skill \`references/\` files with absolute paths β€” the shell's cwd is not stable between commands. \`.claude/launch.json\` declares this app's dev server for Claude Code's browser preview (\`npm run dev\` on port 5173, \`--strictPort\`) β€” start it from there rather than rediscovering diff --git a/scripts/gen-component-catalog.mjs b/scripts/gen-component-catalog.mjs index 22aee7995..62cff3a5b 100644 --- a/scripts/gen-component-catalog.mjs +++ b/scripts/gen-component-catalog.mjs @@ -232,12 +232,27 @@ instead of hand-writing markup. - **Full props are not listed here** (that would be too large to keep in context). Follow a component's link for its complete prop table, or see the per-skill - references. Every component also accepts the shared Bulma **helper props** + references. Value unions (\`size\`, \`color\`, variants) differ per component β€” + never reuse one by analogy (\`Tag size\` is \`normal|medium|large\`; \`Button\` + adds \`small\`): the bestax-theming skill's + \`references/themeable-components.md\` lists them verbatim. The installed + types are at \`node_modules/@allxsmith/bestax-bulma/dist/types/\` (the + symlink resolves under pnpm's isolated linker β€” go straight there, no + \`find\` hunt), and when a \`.d.ts\` shows an opaque alias (\`size?: TagSize\`), + grep the alias name in that same file for the literals instead of guessing. + Top-level components accept the shared Bulma **helper props** (\`m\`/\`p\` spacing, \`textColor\`/\`bgColor\`, \`textAlign\`, \`display\`, flex, …) β€” - documented once in \`references/api.md\`. + documented once in \`references/api.md\` β€” with rare exceptions (\`Skeleton\`). - **Compound components** expose sub-parts via dot access (e.g. \`Card.Header\`, \`Navbar.Item\`, \`Tabs.Tab\`, \`Hero.Body\`, \`Columns.Column\`, \`Table.Tr\`); see the - component's linked page for the full set. + component's linked page for the full set. Sub-parts do **not** all take helper + props: the \`Table.*\`, \`Menu.*\`, and \`Hero.*\` families do (most \`Navbar.*\` + too), but \`Card.*\`, \`Modal.*\`, \`Tabs.*\`, and \`Message.*\` sub-parts take only + \`className\` + HTML attributes β€” put helper props on the parent or on an + element inside (\`Span\`, \`Paragraph\`, …) instead. +- **Composing these into your own reusable component?** Use the spine in this + skill's \`SKILL.md\`: extend \`BulmaClassesProps\`, merge \`className\`, spread + \`...rest\` β€” so it takes the same helper props as the library components. - Raw \`*Base\` form exports (\`InputBase\`, \`SelectBase\`, \`TextAreaBase\`, …) are escape-hatch variants of the convenience wrappers above them; see the Form docs. diff --git a/skills/bestax-custom-component/SKILL.md b/skills/bestax-custom-component/SKILL.md index 283a2a247..2824f2a85 100644 --- a/skills/bestax-custom-component/SKILL.md +++ b/skills/bestax-custom-component/SKILL.md @@ -49,13 +49,20 @@ label β€” use that instead"_ or _"No `ProfileCard` exists; I'll build one compos ## Composition first Build from existing components before writing any CSS: `Box`, `Card`, `Title`, `SubTitle`, -`Icon`, `Block`, `Content`, `Tag`, plus the Bulma helper props every component accepts (spacing, -color, typography, flexbox). Most "custom components" are a composition function β€” zero new -styles. See `examples/stat-card.tsx` for a complete worked example. +`Icon`, `Block`, `Content`, `Tag`, plus the shared Bulma helper props (spacing, color, +typography, flexbox). Compound sub-parts are the exception β€” `Card.Content`, `Modal.Card`, +`Tabs.Tab`, `Message.Body` take only `className` + HTML attributes β€” `Tabs.Tab` additionally +requires `index={i}` β€” so put helper props on the parent or on an element inside them, never +invent them there. 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. File at +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`, merge `className`, spread `...rest`. 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 @@ -139,7 +146,9 @@ bestax-bulma. Then the full `register-vars`/`getVar` pattern from 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. +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 @@ -152,7 +161,7 @@ render, propβ†’class mapping, helper-prop passthrough (`m="3"` β†’ `m-3`), and t - [ ] 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. +- [ ] Composition first β€” existing components + helper props before any CSS; every reusable component gets the spine. - [ ] No inline `style={{}}` anywhere. - [ ] Lowest sufficient ladder rung (helper props β†’ scoped CSS vars β†’ Sass). - [ ] All colors/radii derived from `--bulma-*` variables β€” no literals. diff --git a/skills/bestax-custom-component/examples/stat-card.tsx b/skills/bestax-custom-component/examples/stat-card.tsx index d27eee088..a58eecb7f 100644 --- a/skills/bestax-custom-component/examples/stat-card.tsx +++ b/skills/bestax-custom-component/examples/stat-card.tsx @@ -64,6 +64,7 @@ export function StatCard({ size="large" textColor={color} mr="4" + // ariaLabel is Icon's own prop; most components take plain aria-label. ariaLabel={`${label} icon`} /> )} diff --git a/skills/bestax-custom-component/references/api.md b/skills/bestax-custom-component/references/api.md index 3729b0d95..ea99d1f6c 100644 --- a/skills/bestax-custom-component/references/api.md +++ b/skills/bestax-custom-component/references/api.md @@ -30,7 +30,9 @@ that can also be used on their own: | Other | `useOtherClasses` | `float`, `overflow`, `radius`, `shadow`, `interaction`, `cursor`, `skeleton`, `clearfix`, `relative`, `fullHeight`, `responsive` | Because the component destructures these into `bulmaHelperClasses`, callers get the full Bulma -helper surface for free on every component, and `rest` stays clean for DOM spreading. +helper surface for free on every component built this way, and `rest` stays clean for DOM +spreading. (Library compound sub-parts β€” `Card.Content`, `Modal.Card`, `Tabs.Tab`, +`Message.Body` β€” do **not** take helper props: only `className` + HTML attributes.) ## `classNames(...)` and friends β€” `helpers/classNames.ts` diff --git a/skills/bestax-custom-component/references/component-catalog.md b/skills/bestax-custom-component/references/component-catalog.md index 8f4fdf929..317187f2e 100644 --- a/skills/bestax-custom-component/references/component-catalog.md +++ b/skills/bestax-custom-component/references/component-catalog.md @@ -10,12 +10,27 @@ instead of hand-writing markup. - **Full props are not listed here** (that would be too large to keep in context). Follow a component's link for its complete prop table, or see the per-skill - references. Every component also accepts the shared Bulma **helper props** + references. Value unions (`size`, `color`, variants) differ per component β€” + never reuse one by analogy (`Tag size` is `normal|medium|large`; `Button` + adds `small`): the bestax-theming skill's + `references/themeable-components.md` lists them verbatim. The installed + types are at `node_modules/@allxsmith/bestax-bulma/dist/types/` (the + symlink resolves under pnpm's isolated linker β€” go straight there, no + `find` hunt), and when a `.d.ts` shows an opaque alias (`size?: TagSize`), + grep the alias name in that same file for the literals instead of guessing. + Top-level components accept the shared Bulma **helper props** (`m`/`p` spacing, `textColor`/`bgColor`, `textAlign`, `display`, flex, …) β€” - documented once in `references/api.md`. + documented once in `references/api.md` β€” with rare exceptions (`Skeleton`). - **Compound components** expose sub-parts via dot access (e.g. `Card.Header`, `Navbar.Item`, `Tabs.Tab`, `Hero.Body`, `Columns.Column`, `Table.Tr`); see the - component's linked page for the full set. + component's linked page for the full set. Sub-parts do **not** all take helper + props: the `Table.*`, `Menu.*`, and `Hero.*` families do (most `Navbar.*` + too), but `Card.*`, `Modal.*`, `Tabs.*`, and `Message.*` sub-parts take only + `className` + HTML attributes β€” put helper props on the parent or on an + element inside (`Span`, `Paragraph`, …) instead. +- **Composing these into your own reusable component?** Use the spine in this + skill's `SKILL.md`: extend `BulmaClassesProps`, merge `className`, spread + `...rest` β€” so it takes the same helper props as the library components. - Raw `*Base` form exports (`InputBase`, `SelectBase`, `TextAreaBase`, …) are escape-hatch variants of the convenience wrappers above them; see the Form docs. diff --git a/skills/bestax-form/SKILL.md b/skills/bestax-form/SKILL.md index df87a1640..c64991e74 100644 --- a/skills/bestax-form/SKILL.md +++ b/skills/bestax-form/SKILL.md @@ -11,8 +11,9 @@ This skill covers the form components in `@allxsmith/bestax-bulma` and how to co **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 into the components via the `color`, `message`, and `messageColor` props. See -**Validation without a library** below. +back via each input's own `color`, `message`, and `messageColor` props β€” they live on the +convenience inputs (`Input`, `Select`, `TextArea`, …), **not** on `Field` (which takes `label` +but has no `message`). See **Validation without a library** below. ## Use when @@ -113,6 +114,13 @@ Across the convenience inputs (`Input`, `Select`, `TextArea`, and similar): Plus the full Bulma **helper props** (`m`, `p`, `textColor`, `display`, …) on every component via `useBulmaClasses`. +⚠️ Full-width casing is inconsistent across the library: `Select`, `File`, and `Table` take +`isFullwidth` (lowercase w); `Button` alone takes `isFullWidth`; `Tabs` takes bare `fullwidth`. + +⚠️ The `label` prop renders the `<label>` but does **not** wire `htmlFor`/`id` β€” assistive tech +gets no association. Pass `id` on the input plus `labelProps={{ htmlFor: sameId }}` (every +convenience input and `Field` accept `labelProps`). + ## Convenience vs composed - **Convenience** (`<Input label message … />`) β€” for typical, single-control fields. Fewer @@ -195,12 +203,15 @@ Before calling a form done, **render it and look at it**: run `pnpm storybook` ( 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. +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 input has an associated label (`label` prop, or a `<label htmlFor>` when composing). +- [ ] Every label is programmatically associated: `label` prop + `id` on the input + + `labelProps={{ htmlFor }}`, or a `<label htmlFor>` when composing. - [ ] 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. diff --git a/skills/bestax-form/references/api.md b/skills/bestax-form/references/api.md index 610f19a9f..45b020924 100644 --- a/skills/bestax-form/references/api.md +++ b/skills/bestax-form/references/api.md @@ -16,7 +16,7 @@ Container and layout. Compound parts: `Field.Label`, `Field.Body`. | `narrow` | `boolean` | Constrain to content width (inside horizontal bodies). | | `label` | `ReactNode` | Convenience label. | | `labelSize` | `'small' \| 'normal' \| 'medium' \| 'large'` | Label size. | -| `labelProps` | label attributes | Props for the `<label>`. | +| `labelProps` | label attributes | Props for the `<label>` β€” where `htmlFor` goes. | | `textColor` / `bgColor` | Bulma color | Helper colors for the field. | ## Control β€” `form/Control.tsx` @@ -59,7 +59,8 @@ Wraps a single input; adds icons and loading. ## Select / SelectBase, TextArea / TextAreaBase Same convenience/raw split as Input. `Select` supports `isLoading` (on the control), `color`, -`size`, `isRounded`, plus the Field/Control/message props. `TextArea` adds `rows` and +`size`, `isRounded`, `isFullwidth` (lowercase w β€” Button's is `isFullWidth`), `multiple` + +`multipleSize`, plus the Field/Control/message props. `TextArea` adds `rows` and `hasFixedSize`. ## Checkbox / Checkboxes, Radio / Radios diff --git a/skills/bestax-icons/SKILL.md b/skills/bestax-icons/SKILL.md index a70a31171..fd905d281 100644 --- a/skills/bestax-icons/SKILL.md +++ b/skills/bestax-icons/SKILL.md @@ -61,7 +61,9 @@ don't rely on it. ## Accessibility -Every `Icon` renders `aria-label` (default `"icon"`). +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"`. diff --git a/skills/bestax-layout-scaffold/SKILL.md b/skills/bestax-layout-scaffold/SKILL.md index e66843603..b70d64b60 100644 --- a/skills/bestax-layout-scaffold/SKILL.md +++ b/skills/bestax-layout-scaffold/SKILL.md @@ -45,12 +45,51 @@ Centered; a collection of items β†’ Card grid. For mixed requests, pick the domi `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`. - 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. -- **Style with helper props, not inline `style`.** Use `m`/`p` spacing (`mt="4"` = 1rem), - `textAlign="centered"`, and `textColor`/`bgColor` instead of `style={{ marginTop, textAlign, -color }}`. Set the app-wide icon library once with `<ConfigProvider iconLibrary="…">` at the root - rather than `library` on every `<Icon>`. +- **Style with helper props β€” no inline `style`, no raw Bulma `className`s.** Use `m`/`p` + spacing (`mt="4"` = 1rem), `textAlign="centered"`, and `textColor`/`bgColor` instead of + `style={{…}}` or `className="has-text-…"`. 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>`. +- **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 + one alternating section + band, 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% + ); + } + .section-alt { + background: var(--bulma-scheme-main-bis); /* next band: -ter */ + } + ``` + + A highlighted/"featured" card needs **no third rule**: wrap that one card in a scoped + `<Theme bulmaVars={{ '--bulma-box-shadow': '0 0 0 2px var(--bulma-primary), var(--bulma-shadow)' }}>` + β€” component `--bulma-*` vars (`box-shadow`, `card-shadow`, radius, …) are reachable + per-subtree with zero CSS, and stay 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 @@ -84,5 +123,9 @@ color }}`. Set the app-wide icon library once with `<ConfigProvider iconLibrary= cards must match height. - [ ] For a fixed navbar, add `has-navbar-fixed-top` to `<html>`. - [ ] Do not use `Tile` β€” it is not shipped. -- [ ] Style with helper props (`mt`/`p`, `textAlign`, `textColor`), not inline `style`. +- [ ] Style with helper props, never inline `style` or raw Bulma `className`s (`Span`/`Paragraph` wrap bare text; `Th`/`Td` take `textAlign`/`textWeight`). +- [ ] Decorative CSS ≀10 lines total incl. comments β€” no file-header comment (hero wash + section band), `--bulma-*`-derived; + no resets β€” Bulma ships one. A featured-card ring is a scoped `<Theme bulmaVars>`, not CSS. - [ ] 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). diff --git a/skills/bestax-layout-scaffold/examples/app-shell.tsx b/skills/bestax-layout-scaffold/examples/app-shell.tsx index 74113acac..7e4754011 100644 --- a/skills/bestax-layout-scaffold/examples/app-shell.tsx +++ b/skills/bestax-layout-scaffold/examples/app-shell.tsx @@ -3,7 +3,9 @@ // // A fixed-top navbar needs the `has-navbar-fixed-top` class on <html> so the page // is padded below it β€” Bulma requires this and the library does NOT add it for -// you. The columns sit side by side on tablet and up, and stack (menu above +// you. In a real app set it statically in index.html; the useEffect below is the +// fallback for a conditionally-mounted navbar (and keeps this file self-contained). +// The columns sit side by side on tablet and up, and stack (menu above // content) on mobile. // // `ConfigProvider` wraps the shell once at the root to set the app-wide icon diff --git a/skills/bestax-layout-scaffold/examples/card-grid.tsx b/skills/bestax-layout-scaffold/examples/card-grid.tsx index 8d5556c66..3e2c65dc1 100644 --- a/skills/bestax-layout-scaffold/examples/card-grid.tsx +++ b/skills/bestax-layout-scaffold/examples/card-grid.tsx @@ -90,9 +90,10 @@ export default function CatalogPage() { image={product.image} imageAlt={product.name} header={product.name} - footer={ - <span className="card-footer-item">{product.price}</span> - } + // Card wraps each footer item in .card-footer-item itself β€” + // no raw span/className needed (and a literal class would + // break under ConfigProvider classPrefix). + footer={product.price} > <p>{product.blurb}</p> </Card> diff --git a/skills/bestax-layout-scaffold/references/archetypes.md b/skills/bestax-layout-scaffold/references/archetypes.md index 7ab7ab2c3..4b549378e 100644 --- a/skills/bestax-layout-scaffold/references/archetypes.md +++ b/skills/bestax-layout-scaffold/references/archetypes.md @@ -20,6 +20,7 @@ app". ```tsx <> + {/* fixed="top" requires <html class="has-navbar-fixed-top"> β€” set it in index.html */} <Navbar fixed="top" color="dark"> <Navbar.Brand> <Navbar.Item href="#">Brand</Navbar.Item> @@ -57,8 +58,9 @@ app". </> ``` -**Required:** add `has-navbar-fixed-top` to `<html>` (see `examples/app-shell.tsx`) so content is -not hidden behind the fixed navbar. +**Required:** add `has-navbar-fixed-top` to `<html>` β€” statically in `index.html`; an effect +(`examples/app-shell.tsx`) only for a conditionally-mounted navbar β€” so content is not hidden +behind the fixed navbar. **Responsive:** the navbar collapses to a burger on mobile (`Navbar.Burger` + `Navbar.Menu active`). The sidebar and content columns sit side by side on tablet and up, and stack (menu above content) @@ -116,6 +118,14 @@ pricing page. The default for "build me a site/page". **Responsive:** `Section`s already stack vertically. The feature `Columns` collapse to one feature per row on mobile. Use `Hero size="large"` / `"fullheight"` for a taller hero. +**Site chrome:** a full site adds the App-shell `Navbar` (archetype 1, minus the sidebar) above +the `Hero`; if it's `fixed="top"`, the same `<html class="has-navbar-fixed-top">` requirement +applies here too. + +**Alternating section bands:** tint every other `Section` with a scheme step β€” +`.section-alt { background: var(--bulma-scheme-main-bis); }` (next step `-ter`) β€” not +`bgColor="light"`/`"white"`: those are fixed colors that stay light when dark mode flips the text. + **Hero CTAs:** on a colored hero use **filled** buttons only β€” `color="light"` for the primary CTA and `color="primary" isInverted` (solid white, primary text) for a secondary. A thin `isOutlined` button on a fixed-color surface is low-contrast and degrades further under OS dark @@ -172,7 +182,7 @@ search results, "a grid of cards". flexGrow="1" image={item.image} header={item.name} - footer={<span className="card-footer-item">{item.price}</span>} + footer={item.price} // Card wraps footer items in .card-footer-item itself > {item.blurb} </Card> diff --git a/skills/bestax-layout-scaffold/references/layout-components.md b/skills/bestax-layout-scaffold/references/layout-components.md index 14144c645..3bd049196 100644 --- a/skills/bestax-layout-scaffold/references/layout-components.md +++ b/skills/bestax-layout-scaffold/references/layout-components.md @@ -21,7 +21,8 @@ import { ``` Every component also accepts the shared Bulma helper props (`m`/`p` spacing, `textAlign`, -`textColor`, `bgColor`, etc.). +`textColor`, `bgColor`, etc.). Flex helpers take the standard CSS values spelled in full: +`justifyContent="space-between"`, `alignItems="center"`. > **Use helper props, never inline `style`, for spacing / alignment / color.** `mt="4"` (= 1rem) > not `style={{ marginTop: '1rem' }}`; `textAlign="centered"` not `style={{ textAlign: 'center' }}`; @@ -129,6 +130,9 @@ type BulmaColumnSize = | 'four-fifths'; ``` +Numeric sizes are **numbers** β€” `sizeDesktop={7}`, never `"7"`; only the fraction names are +strings. (`gap` is the exception that accepts number **or** string.) + > Columns **stack on mobile** by default and go side-by-side at the tablet breakpoint and up. > Use the per-breakpoint `size*` props to control how many cells share a row at each width. @@ -219,11 +223,9 @@ flexDirection="column"` + `flexGrow="1"` pattern applies to `Cell` + `Card`. The burger/menu is controlled state β€” toggle `Navbar.Burger active`/`onClick` and pass the same flag to `Navbar.Menu active`. **A `fixed="top"` navbar requires `has-navbar-fixed-top` on `<html>`** -(Bulma offsets the page from it); the library adds no helper, so set it yourself: - -```ts -document.documentElement.classList.add('has-navbar-fixed-top'); -``` +(Bulma offsets the page from it); the library adds no helper. Set it statically β€” +`<html class="has-navbar-fixed-top">` in `index.html` β€” reserving a `classList.add` effect for +a navbar that mounts conditionally. **Routing:** in a routed app, don't use `href="#"` β€” render items as the router's link component. `Menu.Item as={Link} to="/x"` and `Navbar.Item as={Link} to="/x"` both compile diff --git a/skills/bestax-theming/SKILL.md b/skills/bestax-theming/SKILL.md index 56fbd05b3..3f0ac2fba 100644 --- a/skills/bestax-theming/SKILL.md +++ b/skills/bestax-theming/SKILL.md @@ -9,12 +9,6 @@ license: MIT `@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. -## Use when - -- Setting a brand/primary color or recoloring `link`/`info`/`success`/`warning`/`danger`. -- Adjusting global tokens β€” radius, fonts, sizes, weights. -- Adding light/dark mode. - ## Approach Recolor a brand color by overriding its **hue/saturation/lightness trio** β€” Bulma derives every @@ -50,13 +44,20 @@ color tokens or fixed-color surfaces exist: (`--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`). + `references/css-variables.md`). Alternating/tinted section bands are this case: + `background: var(--bulma-scheme-main-bis)` (then `-ter`), never `bgColor="light"` β€” + `light`/`white`/grey helper backgrounds are fixed colors that fight dark mode. - **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`, `LinkButton`, and `Notification` **only** (`Tag` has none); +`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 @@ -109,7 +110,7 @@ Nest `Theme` and `ConfigProvider` together at the root (order doesn't matter). - [ ] 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`. +- [ ] 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>`. diff --git a/skills/bestax-theming/examples/theme-config.tsx b/skills/bestax-theming/examples/theme-config.tsx index 871b64985..d009f05aa 100644 --- a/skills/bestax-theming/examples/theme-config.tsx +++ b/skills/bestax-theming/examples/theme-config.tsx @@ -34,6 +34,8 @@ export function ThemedApp({ children }: { children: React.ReactNode }) { bulmaVars={{ '--bulma-radius': '0.75rem', '--bulma-radius-large': '1.25rem', + // 'Inter' must actually be loaded (index.html <link> or an @fontsource + // import) β€” declaring the family var alone falls back to system-ui. '--bulma-family-primary': "'Inter', system-ui, sans-serif", }} > diff --git a/skills/bestax-theming/references/css-variables.md b/skills/bestax-theming/references/css-variables.md index b9430817c..271b85ec9 100644 --- a/skills/bestax-theming/references/css-variables.md +++ b/skills/bestax-theming/references/css-variables.md @@ -33,6 +33,10 @@ theming means overriding the right `--bulma-*` values. > Note: the type of the `bulmaVars` keys is not exported β€” pass it as an object literal (TypeScript > still checks the keys against the allowed `--bulma-*` names). +> `--bulma-family-*` only selects the family β€” also load the font itself (a `<link>` in +> `index.html` or an `@fontsource/*` package import), or the browser silently falls back to +> the system font. + ### 2. Plain CSS Set the variables yourself on any selector. `:root` themes the whole document; a class scopes it. diff --git a/skills/bestax-theming/references/themeable-components.md b/skills/bestax-theming/references/themeable-components.md index 42cdda3a7..971cfdad4 100644 --- a/skills/bestax-theming/references/themeable-components.md +++ b/skills/bestax-theming/references/themeable-components.md @@ -8,13 +8,20 @@ This is the self-contained inventory of the color/size/variant props that matter 1. **Component `color` modifier** β†’ emits `is-<color>` (the filled Bulma variant). The accepted values are component-specific (see the table). Example: `<Button color="primary">` β†’ `is-primary`. -2. **Helper color props** (available on virtually every component, applied as utility classes): + ⚠️ Some unions are **typed wider than the CSS Bulma ships** β€” the class is emitted but no rule + matches. No component ships `is-grey*`/`is-*-bis`/`is-*-ter` rules at all: those `validColors` + members typecheck on `Progress`/`Notification`/`Hero` but style nothing (the `has-text-*`/ + `has-background-*` **helpers** do cover all 17). Before relying on an unusual value, grep the + shipped CSS: `node_modules/@allxsmith/bestax-bulma/dist/bestax.css` for e.g. `.progress.is-grey`. +2. **Helper color props** (on most components, applied as utility classes): - `color` / `textColor` β†’ `has-text-<color>` (text color) - `backgroundColor` / `bgColor` β†’ `has-background-<color>` (background) - `colorShade` / `backgroundColorShade` β†’ adds a shade suffix, e.g. `has-text-primary-30` - When a component has its own `color` modifier (Button, Tag, Input, …), use **`textColor`** / - **`bgColor`** for utility coloring so the two don't collide. + Components with their own `color` modifier drop the `color` helper to avoid a collision. + Button/Box/Card/Hero/Section re-expose it as **`textColor`** / **`bgColor`**; Tag, Input, + and `Td`/`Th` have **no text-color prop at all** β€” wrap content in `<Span textColor="…">` + there (raw `backgroundColor` still works on Tag and Input). `<color>` for the helper props is one of **`validColors`**: @@ -28,43 +35,43 @@ Shades (`colorShade` / `backgroundColorShade`): `00, 05, 10, … 95, invert, lig ## Component `color` / `size` props (verbatim unions) -| Component | `color` accepts | `size` accepts | Notes | -| ------------------ | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | -| `Button` | `primary \| link \| info \| success \| warning \| danger \| white \| light \| dark \| black \| text \| ghost` | `small \| normal \| medium \| large` | adds `text`, `ghost`; also `isLight`, `isOutlined`, `isInverted`, `isRounded` | -| `Notification` | the 17 `validColors` | β€” | also `isLight` | -| `Tag` | `primary \| link \| info \| success \| warning \| danger \| black \| dark \| light \| white` | `normal \| medium \| large` | also `isRounded`, `isDelete`, `isHoverable` | -| `Box` | `primary \| link \| info \| success \| warning \| danger` | β€” | the 6 only; also `hasShadow` | -| `Message` | `primary \| link \| info \| success \| warning \| danger` | β€” | the 6 only | -| `Input` | `primary \| link \| info \| success \| warning \| danger \| black \| dark \| light \| white` | `small \| medium \| large` | also `isRounded`, `isStatic` | -| `Avatar` | `primary \| link \| info \| success \| warning \| danger \| black \| dark \| light \| white` | `16x16 \| 24x24 \| 32x32 \| 48x48 \| 64x64 \| 96x96 \| 128x128 \| number` | initials/icon background (auto-derived from `name` when unset); also `shape` | -| `Badge` | `primary \| link \| info \| success \| warning \| danger \| black \| dark \| light \| white` | β€” | pill background; default `danger` | -| `Title` | β€” (no `color`; use `textColor`) | `1 \| 2 \| 3 \| 4 \| 5 \| 6` | also `isSpaced` | -| `SubTitle` | β€” (no `color`; use `textColor`) | `1 \| 2 \| 3 \| 4 \| 5 \| 6` | β€” | -| `Autocomplete` | `primary \| link \| info \| success \| warning \| danger` | `small \| medium \| large` | the 6 only | -| `Checkbox` | `primary \| link \| info \| success \| warning \| danger` | `small \| normal \| medium \| large` | the 6 only | -| `DateInput` | `primary \| link \| info \| success \| warning \| danger` | `small \| medium \| large` | also `isRounded` | -| `DateTimeInput` | `primary \| link \| info \| success \| warning \| danger` | `small \| medium \| large` | also `isRounded` | -| `File` | `primary \| link \| info \| success \| warning \| danger \| black \| dark \| light \| white` | `small \| medium \| large` | also `isBoxed`, `isFullwidth` | -| `Hero` | the 17 `validColors` | `small \| medium \| large \| fullheight \| fullheight-with-navbar` | section background | -| `LinkButton` | `primary \| link \| info \| success \| warning \| danger \| white \| light \| dark \| black` | β€” | button-styled link; emits `link-button-<color>` | -| `Loading` | `primary \| link \| info \| success \| warning \| danger` | `small \| medium \| large` | spinner color; default light grey | -| `Navbar` | `primary \| link \| info \| success \| warning \| danger \| black \| dark \| light \| white` | β€” | β€” | -| `Numberinput` | `primary \| link \| info \| success \| warning \| danger \| light \| dark` | `small \| medium \| large` | also `inputColor` (the 6) for the inner input | -| `Pagination` | `primary \| link \| info \| success \| warning \| danger \| black \| dark \| light \| white` | `small \| medium \| large` | β€” | -| `Panel` | `primary \| link \| info \| success \| warning \| danger \| black \| dark \| light \| white` | β€” | β€” | -| `Progress` | the 17 `validColors` | `small \| medium \| large` | β€” | -| `Radio` | `primary \| link \| info \| success \| warning \| danger` | `small \| normal \| medium \| large` | the 6 only | -| `Rate` | `primary \| link \| info \| success \| warning \| danger` | `small \| medium \| large` | the 6 only | -| `Select` | `primary \| link \| info \| success \| warning \| danger \| black \| dark \| light \| white` | `small \| medium \| large` | also `isRounded` | -| `Slider` | `primary \| link \| info \| success \| warning \| danger` | `small \| medium \| large` | also `isRounded`, `isCircle` | -| `Steps` | `primary \| link \| info \| success \| warning \| danger` | `small \| medium \| large` | the 6 only | -| `Switch` | `primary \| link \| info \| success \| warning \| danger` | `small \| normal \| medium \| large` | also `isRounded`, `isThin`, `isOutlined` | -| `Tabs` | `primary \| link \| info \| success \| warning \| danger \| black \| dark \| light \| white` | `small \| medium \| large` | β€” | -| `Taginput` | `primary \| link \| info \| success \| warning \| danger` | `small \| medium \| large` | also `tagColor` (the 6 + `dark \| light`) for the tags | -| `TextArea` | `primary \| link \| info \| success \| warning \| danger \| black \| dark \| light \| white` | `small \| medium \| large` | also `isRounded`, `isStatic` | -| `TimeInput` | `primary \| link \| info \| success \| warning \| danger` | `small \| medium \| large` | β€” | -| `Tooltip` | `primary \| link \| info \| success \| warning \| danger \| dark \| light` | `small \| medium \| large` | β€” | -| `Tr` / `Td` / `Th` | `primary \| link \| info \| success \| warning \| danger \| black \| dark \| light \| white` | β€” | table row/cell background | +| Component | `color` accepts | `size` accepts | Notes | +| ------------------ | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | +| `Button` | `primary \| link \| info \| success \| warning \| danger \| white \| light \| dark \| black \| text \| ghost` | `small \| normal \| medium \| large` | adds `text`, `ghost`; also `isLight`, `isOutlined`, `isInverted`, `isRounded` | +| `Notification` | the 17 `validColors` (greys typecheck, no CSS β€” see ⚠️) | β€” | also `isLight` | +| `Tag` | `primary \| link \| info \| success \| warning \| danger \| black \| dark \| light \| white` | `normal \| medium \| large` | also `isRounded`, `isDelete`, `isHoverable` β€” **no `isLight`** | +| `Box` | `primary \| link \| info \| success \| warning \| danger` | β€” | `color` renders `has-text-<color>` (no `.box.is-*` ships β€” tint via `bgColor`); also `hasShadow` | +| `Message` | `primary \| link \| info \| success \| warning \| danger` | β€” | the 6 only | +| `Input` | `primary \| link \| info \| success \| warning \| danger \| black \| dark \| light \| white` | `small \| medium \| large` | also `isRounded`, `isStatic` | +| `Avatar` | `primary \| link \| info \| success \| warning \| danger \| black \| dark \| light \| white` | `16x16 \| 24x24 \| 32x32 \| 48x48 \| 64x64 \| 96x96 \| 128x128 \| number` | initials/icon background (auto-derived from `name` when unset); also `shape` | +| `Badge` | `primary \| link \| info \| success \| warning \| danger \| black \| dark \| light \| white` | β€” | pill background; default `danger` | +| `Title` | β€” (no `color`; use `textColor`) | `1 \| 2 \| 3 \| 4 \| 5 \| 6` | also `isSpaced` | +| `SubTitle` | β€” (no `color`; use `textColor`) | `1 \| 2 \| 3 \| 4 \| 5 \| 6` | β€” | +| `Autocomplete` | `primary \| link \| info \| success \| warning \| danger` | `small \| medium \| large` | the 6 only | +| `Checkbox` | `primary \| link \| info \| success \| warning \| danger` | `small \| normal \| medium \| large` | the 6 only | +| `DateInput` | `primary \| link \| info \| success \| warning \| danger` | `small \| medium \| large` | also `isRounded` | +| `DateTimeInput` | `primary \| link \| info \| success \| warning \| danger` | `small \| medium \| large` | also `isRounded` | +| `File` | `primary \| link \| info \| success \| warning \| danger \| black \| dark \| light \| white` | `small \| medium \| large` | also `isBoxed`, `isFullwidth` | +| `Hero` | the 17 `validColors` (greys typecheck, no CSS β€” see ⚠️) | `small \| medium \| large \| fullheight \| fullheight-with-navbar` | section background | +| `LinkButton` | `primary \| link \| info \| success \| warning \| danger \| white \| light \| dark \| black` | β€” | button-styled link; emits `link-button-<color>` | +| `Loading` | `primary \| link \| info \| success \| warning \| danger` | `small \| medium \| large` | spinner color; default light grey | +| `Navbar` | `primary \| link \| info \| success \| warning \| danger \| black \| dark \| light \| white` | β€” | β€” | +| `Numberinput` | `primary \| link \| info \| success \| warning \| danger \| light \| dark` | `small \| medium \| large` | also `inputColor` (the 6) for the inner input | +| `Pagination` | `primary \| link \| info \| success \| warning \| danger \| black \| dark \| light \| white` | `small \| medium \| large` | β€” | +| `Panel` | `primary \| link \| info \| success \| warning \| danger \| black \| dark \| light \| white` | β€” | β€” | +| `Progress` | the 17 `validColors` (greys typecheck, no CSS β€” see ⚠️) | `small \| medium \| large` | β€” | +| `Radio` | `primary \| link \| info \| success \| warning \| danger` | `small \| normal \| medium \| large` | the 6 only | +| `Rate` | `primary \| link \| info \| success \| warning \| danger` | `small \| medium \| large` | the 6 only | +| `Select` | `primary \| link \| info \| success \| warning \| danger \| black \| dark \| light \| white` | `small \| medium \| large` | also `isRounded` | +| `Slider` | `primary \| link \| info \| success \| warning \| danger` | `small \| medium \| large` | also `isRounded`, `isCircle` | +| `Steps` | `primary \| link \| info \| success \| warning \| danger` | `small \| medium \| large` | the 6 only | +| `Switch` | `primary \| link \| info \| success \| warning \| danger` | `small \| normal \| medium \| large` | also `isRounded`, `isThin`, `isOutlined` | +| `Tabs` | `primary \| link \| info \| success \| warning \| danger \| black \| dark \| light \| white` | `small \| medium \| large` | β€” | +| `Taginput` | `primary \| link \| info \| success \| warning \| danger` | `small \| medium \| large` | also `tagColor` (the 6 + `dark \| light`) for the tags | +| `TextArea` | `primary \| link \| info \| success \| warning \| danger \| black \| dark \| light \| white` | `small \| medium \| large` | also `isRounded`, `isStatic` | +| `TimeInput` | `primary \| link \| info \| success \| warning \| danger` | `small \| medium \| large` | β€” | +| `Tooltip` | `primary \| link \| info \| success \| warning \| danger \| dark \| light` | `small \| medium \| large` | β€” | +| `Tr` / `Td` / `Th` | `primary \| link \| info \| success \| warning \| danger \| black \| dark \| light \| white` | β€” | cell background; cells take `textAlign`/`textWeight`/`textSize` directly | The 6 brand colors (`primary, link, info, success, warning, danger`) are the ones a custom theme recolors via the HSL trios (see `css-variables.md`). The greyscale and `white`/`light`/`dark` From 71c03bcd1963245b25a4abc96b89bfdda6443afe Mon Sep 17 00:00:00 2001 From: Alex Smith <asmith62378@gmail.com> Date: Fri, 24 Jul 2026 16:37:22 -0400 Subject: [PATCH 2/5] fix(create-bestax): correct the featured-card ring recipe to override --bulma-shadow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The recipe scoped --bulma-box-shadow (and named --bulma-card-shadow as equally reachable), but .box/.card re-declare those on their own selector, so an ancestor Theme never wins β€” verified in a browser: the ring did not render for either component. Overriding the upstream --bulma-shadow token does work for both. This also restores consistency with the rule already stated in bestax-theming/references/css-variables.md. πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --- skills/bestax-layout-scaffold/SKILL.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/skills/bestax-layout-scaffold/SKILL.md b/skills/bestax-layout-scaffold/SKILL.md index b70d64b60..6bbeb0d00 100644 --- a/skills/bestax-layout-scaffold/SKILL.md +++ b/skills/bestax-layout-scaffold/SKILL.md @@ -85,10 +85,12 @@ Centered; a collection of items β†’ Card grid. For mixed requests, pick the domi } ``` - A highlighted/"featured" card needs **no third rule**: wrap that one card in a scoped - `<Theme bulmaVars={{ '--bulma-box-shadow': '0 0 0 2px var(--bulma-primary), var(--bulma-shadow)' }}>` - β€” component `--bulma-*` vars (`box-shadow`, `card-shadow`, radius, …) are reachable - per-subtree with zero CSS, and stay theme- and dark-mode-aware. + A highlighted/"featured" `Card`/`Box` needs **no third rule**: wrap that one element in + `<Theme bulmaVars={{ '--bulma-shadow': '0 0 0 2px var(--bulma-primary)' }}>`. Override the + **upstream token**, not the component's own var: `.card`/`.box` re-declare + `--bulma-card-shadow`/`--bulma-box-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). 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 From 2fa78206fb1d73d3b22161bfe5ec7c22ae0ee89a Mon Sep 17 00:00:00 2001 From: Alex Smith <asmith62378@gmail.com> Date: Tue, 28 Jul 2026 19:39:31 -0400 Subject: [PATCH 3/5] fix(create-bestax): correct spine, Field, isLight, and Box/Input color guidance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the CodeRabbit review on #365. Each finding re-verified against source: - Component spine: the shorthand said "spread `...rest`" without naming `useBulmaClasses`, leaving `rest` undefined for a reader β€” spreading the raw props instead leaks helper props onto the DOM and emits none of their classes. Now names the hook at all three sites (scaffold CLAUDE.md, catalog generator, SKILL.md prose) to match the working template. - `Field` does accept `color` (Field.tsx:42); only `message`/`messageColor` are absent. Corrected the claim. - `LinkButtonProps` explicitly omits `isLight` (LinkButton.tsx:13), so drop LinkButton from the isLight claim; note it on the reference row too. - Box/Card/Section emit no `is-<color>` rule β€” their `color` flows into `useBulmaClasses` as the text helper, so lumping them with Button/Hero as "modifier displaced the helper" was wrong. (CodeRabbit flagged Box; Card and Section have the same shape.) - `Input` renders a native `<input>`, so the `<Span textColor>` wrapper cannot color its value. Bulma declares `--bulma-input-*` on `.input` itself, so an ancestor `<Theme>` can't reach them either β€” point at the upstream `--bulma-text-strong-l`, same rule as the shadow fix in 71c03bc. - stat-card's icon is decorative next to a visible label; `ariaLabel` made it announce twice. Now `aria-hidden="true"`, matching bestax-icons' own guidance. Not changed: the decorative-CSS example is exactly 10 lines, within the stated "≀10 lines" budget β€” the reported 11-line overrun does not reproduce. --- create-bestax/src/constants.ts | 5 +++-- scripts/gen-component-catalog.mjs | 6 ++++-- skills/bestax-custom-component/SKILL.md | 4 +++- .../examples/stat-card.tsx | 7 +++++-- .../references/component-catalog.md | 6 ++++-- skills/bestax-form/SKILL.md | 9 +++++---- skills/bestax-theming/SKILL.md | 3 ++- .../references/themeable-components.md | 15 ++++++++++----- 8 files changed, 36 insertions(+), 19 deletions(-) diff --git a/create-bestax/src/constants.ts b/create-bestax/src/constants.ts index 69ab50b2a..b007c755a 100644 --- a/create-bestax/src/constants.ts +++ b/create-bestax/src/constants.ts @@ -147,8 +147,9 @@ ${setupLines.join('\n')} - Compose existing components before writing custom CSS; theme via \`Theme\` and \`--bulma-*\` variables, never hardcoded colors. - Reusable components you write get the library's spine so helper props work on them too: - extend \`BulmaClassesProps\`, merge \`className\`, spread \`...rest\` β€” the - bestax-custom-component skill has the full template. + extend \`BulmaClassesProps\`, run your props through \`useBulmaClasses\`, merge the + \`bulmaHelperClasses\` it returns into \`className\`, and spread **its** \`rest\` (not the raw + props) β€” the bestax-custom-component skill has the full template. - There is no test runner or Storybook in this app β€” don't assume one. - \`index.html\`'s \`<title>\` starts as the project name and \`README.md\` is stock template boilerplate β€” once this app has a real identity, set the title (and any meta tags) to match diff --git a/scripts/gen-component-catalog.mjs b/scripts/gen-component-catalog.mjs index 62cff3a5b..e739e7606 100644 --- a/scripts/gen-component-catalog.mjs +++ b/scripts/gen-component-catalog.mjs @@ -251,8 +251,10 @@ instead of hand-writing markup. \`className\` + HTML attributes β€” put helper props on the parent or on an element inside (\`Span\`, \`Paragraph\`, …) instead. - **Composing these into your own reusable component?** Use the spine in this - skill's \`SKILL.md\`: extend \`BulmaClassesProps\`, merge \`className\`, spread - \`...rest\` β€” so it takes the same helper props as the library components. + skill's \`SKILL.md\`: extend \`BulmaClassesProps\`, run your props through + \`useBulmaClasses\`, merge its \`bulmaHelperClasses\` into \`className\`, and spread + the \`rest\` it returns β€” so it takes the same helper props as the library + components. - Raw \`*Base\` form exports (\`InputBase\`, \`SelectBase\`, \`TextAreaBase\`, …) are escape-hatch variants of the convenience wrappers above them; see the Form docs. diff --git a/skills/bestax-custom-component/SKILL.md b/skills/bestax-custom-component/SKILL.md index 2824f2a85..5c9e75736 100644 --- a/skills/bestax-custom-component/SKILL.md +++ b/skills/bestax-custom-component/SKILL.md @@ -60,7 +60,9 @@ See `examples/stat-card.tsx` for a complete worked example. 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`, merge `className`, spread `...rest`. The +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`: diff --git a/skills/bestax-custom-component/examples/stat-card.tsx b/skills/bestax-custom-component/examples/stat-card.tsx index a58eecb7f..cbf1ec4d5 100644 --- a/skills/bestax-custom-component/examples/stat-card.tsx +++ b/skills/bestax-custom-component/examples/stat-card.tsx @@ -64,8 +64,11 @@ export function StatCard({ size="large" textColor={color} mr="4" - // ariaLabel is Icon's own prop; most components take plain aria-label. - ariaLabel={`${label} icon`} + // Decorative: the label below already says it, so hide it from AT β€” + // Icon otherwise emits its default aria-label="icon". (To *label* an + // icon, use Icon's own camelCase `ariaLabel`; most components take + // the plain aria-label attribute.) + aria-hidden="true" /> )} {/* No `gap` helper exists β€” space siblings with margin props (mr above). */} diff --git a/skills/bestax-custom-component/references/component-catalog.md b/skills/bestax-custom-component/references/component-catalog.md index 317187f2e..14db08509 100644 --- a/skills/bestax-custom-component/references/component-catalog.md +++ b/skills/bestax-custom-component/references/component-catalog.md @@ -29,8 +29,10 @@ instead of hand-writing markup. `className` + HTML attributes β€” put helper props on the parent or on an element inside (`Span`, `Paragraph`, …) instead. - **Composing these into your own reusable component?** Use the spine in this - skill's `SKILL.md`: extend `BulmaClassesProps`, merge `className`, spread - `...rest` β€” so it takes the same helper props as the library components. + skill's `SKILL.md`: extend `BulmaClassesProps`, run your props through + `useBulmaClasses`, merge its `bulmaHelperClasses` into `className`, and spread + the `rest` it returns β€” so it takes the same helper props as the library + components. - Raw `*Base` form exports (`InputBase`, `SelectBase`, `TextAreaBase`, …) are escape-hatch variants of the convenience wrappers above them; see the Form docs. diff --git a/skills/bestax-form/SKILL.md b/skills/bestax-form/SKILL.md index c64991e74..f3b22e3e0 100644 --- a/skills/bestax-form/SKILL.md +++ b/skills/bestax-form/SKILL.md @@ -11,9 +11,9 @@ This skill covers the form components in `@allxsmith/bestax-bulma` and how to co **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 β€” they live on the -convenience inputs (`Input`, `Select`, `TextArea`, …), **not** on `Field` (which takes `label` -but has no `message`). See **Validation without a library** below. +back via each input's own `color`, `message`, and `messageColor` props on the convenience inputs +(`Input`, `Select`, `TextArea`, …). `Field` accepts `color` too, but has **no** `message` / +`messageColor` β€” put the validation state on the input. See **Validation without a library** below. ## Use when @@ -217,4 +217,5 @@ for the expected classes/states, and say plainly that the visual pass is still o - [ ] Grouped/addon layouts use explicit `Field` + `Control` composition. - [ ] No assumption of a built-in validation/form library β€” state is owned by the app. - [ ] **Rendered and visually inspected in a browser** β€” layout and the error/validation states - look right, not just green tests. + 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. diff --git a/skills/bestax-theming/SKILL.md b/skills/bestax-theming/SKILL.md index 3f0ac2fba..0bcc6fd47 100644 --- a/skills/bestax-theming/SKILL.md +++ b/skills/bestax-theming/SKILL.md @@ -54,7 +54,8 @@ color tokens or fixed-color surfaces exist: 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`, `LinkButton`, and `Notification` **only** (`Tag` has none); +`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`. diff --git a/skills/bestax-theming/references/themeable-components.md b/skills/bestax-theming/references/themeable-components.md index 971cfdad4..5f1f75634 100644 --- a/skills/bestax-theming/references/themeable-components.md +++ b/skills/bestax-theming/references/themeable-components.md @@ -18,10 +18,15 @@ This is the self-contained inventory of the color/size/variant props that matter - `backgroundColor` / `bgColor` β†’ `has-background-<color>` (background) - `colorShade` / `backgroundColorShade` β†’ adds a shade suffix, e.g. `has-text-primary-30` - Components with their own `color` modifier drop the `color` helper to avoid a collision. - Button/Box/Card/Hero/Section re-expose it as **`textColor`** / **`bgColor`**; Tag, Input, - and `Td`/`Th` have **no text-color prop at all** β€” wrap content in `<Span textColor="…">` - there (raw `backgroundColor` still works on Tag and Input). + Components with a real `is-<color>` modifier (`Button`, `Hero`) drop the `color` helper and + re-expose it as **`textColor`** / **`bgColor`**. `Box`/`Card`/`Section` ship no `is-<color>` + rule β€” their `color` _is_ the text helper (`has-text-<color>`; narrowed to the 6 on + `Box`/`Card`), so `color` and `textColor` are the same lever there. `Tag` and `Td`/`Th` have + **no text-color prop** β€” wrap content in `<Span textColor="…">`. `Input` has none either and + the wrapper trick can't work (it renders a native `<input>`; a child can't color its value): + recolor via the upstream `--bulma-text-strong-l`, since Bulma re-declares `--bulma-input-*` on + `.input` itself and an ancestor `<Theme>` can't reach those. Raw `backgroundColor` still works + on `Tag` and `Input`. `<color>` for the helper props is one of **`validColors`**: @@ -53,7 +58,7 @@ Shades (`colorShade` / `backgroundColorShade`): `00, 05, 10, … 95, invert, lig | `DateTimeInput` | `primary \| link \| info \| success \| warning \| danger` | `small \| medium \| large` | also `isRounded` | | `File` | `primary \| link \| info \| success \| warning \| danger \| black \| dark \| light \| white` | `small \| medium \| large` | also `isBoxed`, `isFullwidth` | | `Hero` | the 17 `validColors` (greys typecheck, no CSS β€” see ⚠️) | `small \| medium \| large \| fullheight \| fullheight-with-navbar` | section background | -| `LinkButton` | `primary \| link \| info \| success \| warning \| danger \| white \| light \| dark \| black` | β€” | button-styled link; emits `link-button-<color>` | +| `LinkButton` | `primary \| link \| info \| success \| warning \| danger \| white \| light \| dark \| black` | β€” | button-styled link; emits `link-button-<color>` β€” **no `isLight`/`isOutlined`/`isInverted`** | | `Loading` | `primary \| link \| info \| success \| warning \| danger` | `small \| medium \| large` | spinner color; default light grey | | `Navbar` | `primary \| link \| info \| success \| warning \| danger \| black \| dark \| light \| white` | β€” | β€” | | `Numberinput` | `primary \| link \| info \| success \| warning \| danger \| light \| dark` | `small \| medium \| large` | also `inputColor` (the 6) for the inner input | From 819f1f40e5fd7d07da72171da2e147e08a230bfb Mon Sep 17 00:00:00 2001 From: Alex Smith <asmith62378@gmail.com> Date: Tue, 28 Jul 2026 19:43:04 -0400 Subject: [PATCH 4/5] =?UTF-8?q?fix(create-bestax):=20state=20sub-part=20pr?= =?UTF-8?q?ops=20precisely=20=E2=80=94=20Tabs.Tab=20takes=20more=20than=20?= =?UTF-8?q?className?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses deep-review advisory #2 on #365. `TabProps` (Tabs.tsx:282-294) takes `index`, `disabled`, `icon`, `iconLibrary`, `iconVariant`, `iconSize`, and `iconFeatures` on top of `className` + HTML attributes, so "take only `className` + HTML attributes" was imprecise at all four sites. The load-bearing claim β€” no Bulma helper props on `Card.*`/`Modal.*`/`Tabs.*`/`Message.*` β€” is unchanged. Also surfaces `Tabs.Tab`'s built-in icon props, which the old wording implied did not exist and would have led an agent to nest an `<Icon>` there. --- create-bestax/src/constants.ts | 7 ++++--- scripts/gen-component-catalog.mjs | 7 ++++--- skills/bestax-custom-component/SKILL.md | 7 ++++--- skills/bestax-custom-component/references/api.md | 3 ++- .../references/component-catalog.md | 7 ++++--- 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/create-bestax/src/constants.ts b/create-bestax/src/constants.ts index b007c755a..aadc912a8 100644 --- a/create-bestax/src/constants.ts +++ b/create-bestax/src/constants.ts @@ -141,9 +141,10 @@ ${setupLines.join('\n')} \`Navbar fixed="top"\`) are hand-added in \`index.html\` β€” no component renders those elements. Flex layouts have no \`gap\` helper β€” space children with margins (\`Grid\` and \`Columns\` take a \`gap\` prop, so prefer that there). -- Compound sub-parts (\`Card.*\`, \`Modal.*\`, \`Tabs.*\`, \`Message.*\`) take only \`className\` + - HTML attributes β€” no helper props, no \`as\`/\`href\`: nest a \`Link\`/\`Span\` inside instead. - One required addition: \`Tabs.Tab\` and \`Tabs.Content.Item\` each need \`index={i}\`. +- Compound sub-parts (\`Card.*\`, \`Modal.*\`, \`Tabs.*\`, \`Message.*\`) take \`className\` + HTML + attributes and their own few props β€” no Bulma helper props, no \`as\`/\`href\`: nest a + \`Link\`/\`Span\` inside instead. \`Tabs.Tab\` and \`Tabs.Content.Item\` each require \`index={i}\`, + and \`Tabs.Tab\` has built-in \`icon\`/\`disabled\` props β€” no nested \`Icon\` needed. - Compose existing components before writing custom CSS; theme via \`Theme\` and \`--bulma-*\` variables, never hardcoded colors. - Reusable components you write get the library's spine so helper props work on them too: diff --git a/scripts/gen-component-catalog.mjs b/scripts/gen-component-catalog.mjs index e739e7606..f393eb951 100644 --- a/scripts/gen-component-catalog.mjs +++ b/scripts/gen-component-catalog.mjs @@ -247,9 +247,10 @@ instead of hand-writing markup. \`Navbar.Item\`, \`Tabs.Tab\`, \`Hero.Body\`, \`Columns.Column\`, \`Table.Tr\`); see the component's linked page for the full set. Sub-parts do **not** all take helper props: the \`Table.*\`, \`Menu.*\`, and \`Hero.*\` families do (most \`Navbar.*\` - too), but \`Card.*\`, \`Modal.*\`, \`Tabs.*\`, and \`Message.*\` sub-parts take only - \`className\` + HTML attributes β€” put helper props on the parent or on an - element inside (\`Span\`, \`Paragraph\`, …) instead. + too), but \`Card.*\`, \`Modal.*\`, \`Tabs.*\`, and \`Message.*\` sub-parts take none β€” + just \`className\`, HTML attributes, and their own few (\`Tabs.Tab\` requires + \`index\` and has built-in \`icon\`/\`disabled\` props). Put helper props on the + parent or on an element inside (\`Span\`, \`Paragraph\`, …) instead. - **Composing these into your own reusable component?** Use the spine in this skill's \`SKILL.md\`: extend \`BulmaClassesProps\`, run your props through \`useBulmaClasses\`, merge its \`bulmaHelperClasses\` into \`className\`, and spread diff --git a/skills/bestax-custom-component/SKILL.md b/skills/bestax-custom-component/SKILL.md index 5c9e75736..756cbe811 100644 --- a/skills/bestax-custom-component/SKILL.md +++ b/skills/bestax-custom-component/SKILL.md @@ -51,9 +51,10 @@ label β€” use that instead"_ or _"No `ProfileCard` exists; I'll build one compos 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). Compound sub-parts are the exception β€” `Card.Content`, `Modal.Card`, -`Tabs.Tab`, `Message.Body` take only `className` + HTML attributes β€” `Tabs.Tab` additionally -requires `index={i}` β€” so put helper props on the parent or on an element inside them, never -invent them there. Most "custom components" are a composition function β€” zero new styles. +`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. Most "custom components" are a composition function β€” zero new styles. See `examples/stat-card.tsx` for a complete worked example. ## The component spine diff --git a/skills/bestax-custom-component/references/api.md b/skills/bestax-custom-component/references/api.md index ea99d1f6c..8081a5f5e 100644 --- a/skills/bestax-custom-component/references/api.md +++ b/skills/bestax-custom-component/references/api.md @@ -32,7 +32,8 @@ that can also be used on their own: Because the component destructures these into `bulmaHelperClasses`, callers get the full Bulma helper surface for free on every component built this way, and `rest` stays clean for DOM spreading. (Library compound sub-parts β€” `Card.Content`, `Modal.Card`, `Tabs.Tab`, -`Message.Body` β€” do **not** take helper props: only `className` + HTML attributes.) +`Message.Body` β€” do **not** take helper props: just `className`, HTML attributes, and their own +few, e.g. `Tabs.Tab`'s required `index` and its built-in `icon`/`disabled` props.) ## `classNames(...)` and friends β€” `helpers/classNames.ts` diff --git a/skills/bestax-custom-component/references/component-catalog.md b/skills/bestax-custom-component/references/component-catalog.md index 14db08509..e2d58b388 100644 --- a/skills/bestax-custom-component/references/component-catalog.md +++ b/skills/bestax-custom-component/references/component-catalog.md @@ -25,9 +25,10 @@ instead of hand-writing markup. `Navbar.Item`, `Tabs.Tab`, `Hero.Body`, `Columns.Column`, `Table.Tr`); see the component's linked page for the full set. Sub-parts do **not** all take helper props: the `Table.*`, `Menu.*`, and `Hero.*` families do (most `Navbar.*` - too), but `Card.*`, `Modal.*`, `Tabs.*`, and `Message.*` sub-parts take only - `className` + HTML attributes β€” put helper props on the parent or on an - element inside (`Span`, `Paragraph`, …) instead. + too), but `Card.*`, `Modal.*`, `Tabs.*`, and `Message.*` sub-parts take none β€” + just `className`, HTML attributes, and their own few (`Tabs.Tab` requires + `index` and has built-in `icon`/`disabled` props). Put helper props on the + parent or on an element inside (`Span`, `Paragraph`, …) instead. - **Composing these into your own reusable component?** Use the spine in this skill's `SKILL.md`: extend `BulmaClassesProps`, run your props through `useBulmaClasses`, merge its `bulmaHelperClasses` into `className`, and spread From f09bf025346adb210b8762b0e1b917b23e89ef51 Mon Sep 17 00:00:00 2001 From: Alex Smith <asmith62378@gmail.com> Date: Tue, 28 Jul 2026 20:05:53 -0400 Subject: [PATCH 5/5] fix(create-bestax): note that Field's color prop is typed but inert MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the deep-review advisory on #365. The previous wording ("Field accepts `color` too") was type-true but behavior-false: `FieldComponent` destructures `color: _fieldColor` and never uses it (Field.tsx:189), so it reaches neither `useBulmaClasses` nor the DOM and renders no class. Note this cuts against the CodeRabbit suggestion that prompted the earlier edit β€” `FieldProps` does declare `color`, but declaring it is not honoring it. The doc now states the practical truth (put validation state on the input) and the API truth (the prop exists and does nothing). Also verified the neighbors, which differ: `FieldLabel` and `FieldBody` do *not* destructure `color`, so it flows through `...props` into `useBulmaClasses` and lands as the `has-text-*` helper. Called out so the three aren't assumed alike. --- skills/bestax-form/SKILL.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/skills/bestax-form/SKILL.md b/skills/bestax-form/SKILL.md index f3b22e3e0..835905eba 100644 --- a/skills/bestax-form/SKILL.md +++ b/skills/bestax-form/SKILL.md @@ -12,8 +12,10 @@ This skill covers the form components in `@allxsmith/bestax-bulma` and how to co 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`, …). `Field` accepts `color` too, but has **no** `message` / -`messageColor` β€” put the validation state on the input. See **Validation without a library** below. +(`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