Skip to content
Merged
17 changes: 17 additions & 0 deletions create-bestax/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,15 +158,31 @@ cases. Before writing \`style\`, translate each declaration with this table:
take a \`gap\` prop, so prefer that there).
- No helper matches (e.g. \`maxWidth\`, a one-off gradient)? Add a named class to
\`src/App.css\` and pass it via \`className\` — still never inline \`style\`.
- Don't hand-write Bulma utility classes either — bare text/markup has wrapper elements that
take the same helper props: \`Span\`, \`Paragraph\`, \`Strong\`, not \`<span className="has-text-…">\`.
The one exception: companion classes Bulma requires on \`<html>\`/\`<body>\` (e.g.
\`has-navbar-fixed-top\` with \`Navbar fixed="top"\`) are hand-added in \`index.html\` — no
component renders those elements.
- 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.
- \`Navbar.Burger\`/\`Navbar.Menu\` are controlled — wire \`active\` via state on both, and pair
\`Navbar fixed="top"\` with the \`has-navbar-fixed-top\` class on \`<html>\` (never an inline
padding offset).
- Reusable components you write get the library's spine so helper props work on them too:
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
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

Expand All @@ -182,6 +198,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
Expand Down
24 changes: 21 additions & 3 deletions scripts/gen-component-catalog.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -232,12 +232,30 @@ 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 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
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.

Expand Down
24 changes: 18 additions & 6 deletions skills/bestax-custom-component/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,23 @@ 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 **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

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`, 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
Expand Down Expand Up @@ -163,7 +173,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

Expand All @@ -176,7 +188,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 — 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).
Expand Down
6 changes: 5 additions & 1 deletion skills/bestax-custom-component/examples/stat-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,11 @@ export function StatCard({
size="large"
textColor={color}
mr="4"
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). */}
Expand Down
5 changes: 4 additions & 1 deletion skills/bestax-custom-component/references/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ 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: 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`

Expand Down
24 changes: 21 additions & 3 deletions skills/bestax-custom-component/references/component-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,30 @@ 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 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
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.

Expand Down
24 changes: 19 additions & 5 deletions skills/bestax-form/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ 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 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

Expand Down Expand Up @@ -113,6 +116,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
Expand Down Expand Up @@ -195,15 +205,19 @@ 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.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

## 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.
- [ ] 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.
5 changes: 3 additions & 2 deletions skills/bestax-form/references/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion skills/bestax-icons/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"`.
Expand Down
Loading
Loading