diff --git a/bulma-ui/CLAUDE.md b/bulma-ui/CLAUDE.md index f4fc1dbfb..674f27995 100644 --- a/bulma-ui/CLAUDE.md +++ b/bulma-ui/CLAUDE.md @@ -29,7 +29,8 @@ A new or changed component is **five artifacts, not one**. Touch all of: If the change invalidates guidance in `skills/`, update the skill in the same PR. The full worked walkthrough (including the SCSS side for extras) is -`skills/bestax-custom-component/SKILL.md` — follow it rather than improvising. +`skills/bestax-custom-component/references/library-contributor.md` — follow it rather than +improvising. ## Conventions diff --git a/docs/docs/api/helpers/classnames.md b/docs/docs/api/helpers/classnames.md index 47dc88b0c..f35044ba9 100644 --- a/docs/docs/api/helpers/classnames.md +++ b/docs/docs/api/helpers/classnames.md @@ -14,7 +14,7 @@ sidebar_label: classNames ## Import ```tsx -import { classNames } from '@allxsmith/bestax-bulma; +import { classNames } from '@allxsmith/bestax-bulma'; ``` --- diff --git a/docs/docs/api/helpers/useprefixedclassnames.md b/docs/docs/api/helpers/useprefixedclassnames.md new file mode 100644 index 000000000..0f493e701 --- /dev/null +++ b/docs/docs/api/helpers/useprefixedclassnames.md @@ -0,0 +1,164 @@ +--- +title: usePrefixedClassNames +sidebar_label: usePrefixedClassNames +--- + +# usePrefixedClassNames + +## Overview + +`usePrefixedClassNames` builds a component class string that honors the `classPrefix` from `ConfigProvider` — the hook every bestax component uses for its own classes. It accepts the same arguments as [`classNames`](./classnames.md) (strings, numbers, arrays, objects — falsy values ignored, duplicates removed), reads the current `classPrefix` from context, and prepends it to **every** emitted class name. Two non-hook variants, `prefixedClassNames` and `createPrefixedClassNames`, do the same when you already have the prefix in hand or are outside a React component. + +--- + +## Import + +```tsx +import { + usePrefixedClassNames, + prefixedClassNames, + createPrefixedClassNames, +} from '@allxsmith/bestax-bulma'; +``` + +--- + +## API + +```ts +// Hook: reads classPrefix from the nearest ConfigProvider +function usePrefixedClassNames( + ...args: ( + | string + | number + | undefined + | null + | false + | Record + | unknown[] + )[] +): string; + +// Plain function: pass the prefix explicitly (undefined ⇒ plain classNames) +function prefixedClassNames( + prefix: string | undefined, + ...args: ( + | string + | number + | undefined + | null + | false + | Record + | unknown[] + )[] +): string; + +// Factory: returns a classNames function bound to a fixed prefix +function createPrefixedClassNames(classPrefix: string): (...args) => string; // args: same union as classNames +``` + +### Parameters + +| Function | Parameter | Type | Description | +| -------------------------- | ------------- | --------------------- | -------------------------------------------------------------------------------------------------- | +| `usePrefixedClassNames` | `...args` | same as `classNames` | Class values to join. The `classPrefix` from `ConfigProvider` is applied to every resulting class. | +| `prefixedClassNames` | `prefix` | `string \| undefined` | Prefix to apply. When `undefined` (or empty), behaves exactly like `classNames`. | +| `prefixedClassNames` | `...args` | same as `classNames` | Class values to join. | +| `createPrefixedClassNames` | `classPrefix` | `string` | Prefix baked into the returned function. | + +All three return a space-separated string of unique class names. With no `ConfigProvider` (or no `classPrefix` set), `usePrefixedClassNames` produces the same output as `classNames`. + +--- + +## Usage + +### In a custom component (honors `ConfigProvider`) + +This is the pattern every bestax component follows: build the component's **own** Bulma classes with `usePrefixedClassNames`, then merge in the (already-prefixed) helper classes from `useBulmaClasses` and the consumer's `className` with plain `classNames` — the consumer's `className` must **not** be prefixed. + +```tsx +import { + usePrefixedClassNames, + useBulmaClasses, + classNames, + type BulmaClassesProps, +} from '@allxsmith/bestax-bulma'; + +interface ChipProps + extends React.HTMLAttributes, BulmaClassesProps { + color?: 'primary' | 'link' | 'info' | 'success' | 'warning' | 'danger'; + isRounded?: boolean; +} + +function Chip({ color, isRounded, className, children, ...props }: ChipProps) { + const { bulmaHelperClasses, rest } = useBulmaClasses(props); + + // 'tag is-rounded is-primary' — or 'bestax-tag bestax-is-rounded …' + // inside + const chipClasses = usePrefixedClassNames('tag', { + [`is-${color}`]: !!color, + 'is-rounded': isRounded, + }); + + return ( + + {children} + + ); +} +``` + +Wrapped in a provider, the component emits prefixed classes automatically: + +```tsx +import { ConfigProvider } from '@allxsmith/bestax-bulma'; + + + + Prefixed + + {/* renders class="bestax-tag bestax-is-primary bestax-is-rounded" */} +; +``` + +### Outside a component: `prefixedClassNames` + +When you already know the prefix (or might not have one), pass it as the first argument: + +```ts +prefixedClassNames('bulma-', 'button', { 'is-primary': true }); +// => 'bulma-button bulma-is-primary' + +prefixedClassNames(undefined, 'button', { 'is-primary': true }); +// => 'button is-primary' +``` + +### Reusable prefixer: `createPrefixedClassNames` + +Bind the prefix once and reuse the returned function: + +```ts +const cx = createPrefixedClassNames('bulma-'); + +cx('card', ['has-shadow', { 'is-active': true }]); +// => 'bulma-card bulma-has-shadow bulma-is-active' +``` + +--- + +## Tips + +- The prefix is applied to **every** class, including modifier classes from object keys (`'is-primary'` → `bestax-is-primary`) — this matches the `bestax-prefixed` CSS flavor. +- Never route a consumer-supplied `className` through these helpers; combine it afterwards with plain `classNames` so user classes stay untouched. +- `usePrefixedClassNames` is a React hook — call it unconditionally at the top level of a component. Use `prefixedClassNames`/`createPrefixedClassNames` everywhere else. + +--- + +## See Also + +- [`classNames`](./classnames.md): The unprefixed class-string builder these helpers wrap. +- [`ConfigProvider`](./config.md): Where `classPrefix` comes from. +- [`useBulmaClasses`](./usebulmaclasses.md): Helper-prop classes (already prefix-aware). diff --git a/docs/docs/api/helpers/valid-values.md b/docs/docs/api/helpers/valid-values.md new file mode 100644 index 000000000..2e9d18e0b --- /dev/null +++ b/docs/docs/api/helpers/valid-values.md @@ -0,0 +1,88 @@ +--- +title: Valid value constants +sidebar_label: Valid value constants +--- + +# Valid value constants + +## Overview + +The `valid*` constant arrays enumerate every accepted value for the shared Bulma helper props — public API you can import to build prop types and validation. Each is a readonly `as const` tuple, so `(typeof validColors)[number]` gives you the exact string-literal union. `useBulmaClasses` (and the per-concern hooks) silently ignore values outside these lists, so validating against them tells you exactly what will produce a class. + +--- + +## Import + +```tsx +import { + validColors, + validSizes, + validViewports, +} from '@allxsmith/bestax-bulma'; +``` + +All 18 constants are importable the same way. + +--- + +## Constants + +| Constant | Values | Used by prop family | +| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | +| `validColors` | `'primary'`, `'link'`, `'info'`, `'success'`, `'warning'`, `'danger'`, `'black'` … `'white'`, `'light'`, `'dark'` (17 values) | `color`, `backgroundColor` (also component `color` props) | +| `validColorShades` | `'00'`, `'05'` … `'95'`, `'invert'`, `'light'`, `'dark'`, `'soft'`, `'bold'`, `'on-scheme'` | `colorShade`, `backgroundColorShade` | +| `validSizes` | `'0'`–`'6'`, `'auto'` | Spacing: `m`, `mt`, `mr`, `mb`, `ml`, `mx`, `my`, `p`, `pt`, … `px`, `py` | +| `validTextSizes` | `'1'`–`'7'` | `textSize` (+ `textSizeMobile` … `textSizeFullhd`) | +| `validAlignments` | `'centered'`, `'justified'`, `'left'`, `'right'` | `textAlign` (+ viewport variants) | +| `validTextTransforms` | `'capitalized'`, `'lowercase'`, `'uppercase'`, `'italic'` | `textTransform` | +| `validTextWeights` | `'light'`, `'normal'`, `'medium'`, `'semibold'`, `'bold'` | `textWeight` | +| `validFontFamilies` | `'sans-serif'`, `'monospace'`, `'primary'`, `'secondary'`, `'code'` | `fontFamily` | +| `validDisplays` | `'block'`, `'flex'`, `'inline'`, `'inline-block'`, `'inline-flex'` | `display` (+ `displayMobile` … `displayFullhd`) | +| `validVisibilities` | `'hidden'`, `'sr-only'`, `'invisible'` | `visibility` (+ viewport variants) | +| `validFlexDirections` | `'row'`, `'row-reverse'`, `'column'`, `'column-reverse'` | `flexDirection` | +| `validFlexWraps` | `'nowrap'`, `'wrap'`, `'wrap-reverse'` | `flexWrap` | +| `validJustifyContents` | `'flex-start'`, `'flex-end'`, `'center'`, `'space-between'`, `'space-around'`, `'space-evenly'`, `'start'`, `'end'`, `'left'`, `'right'` | `justifyContent` | +| `validAlignContents` | `'flex-start'`, `'flex-end'`, `'center'`, `'space-between'`, `'space-around'`, `'space-evenly'`, `'stretch'` | `alignContent` | +| `validAlignItems` | `'stretch'`, `'flex-start'`, `'flex-end'`, `'center'`, `'baseline'`, `'start'`, `'end'` | `alignItems` | +| `validAlignSelfs` | `'auto'`, `'flex-start'`, `'flex-end'`, `'center'`, `'baseline'`, `'stretch'` | `alignSelf` | +| `validFlexGrowShrink` | `'0'`–`'5'` | `flexGrow`, `flexShrink` | +| `validViewports` | `'mobile'`, `'tablet'`, `'desktop'`, `'widescreen'`, `'fullhd'` | `viewport` (responsive modifier) | + +--- + +## Typing with `(typeof …)[number]` + +Because the arrays are `as const`, indexing them with `number` yields the union of their literal values — the same idiom the library uses internally for its prop types: + +```ts +import { validColors, validSizes } from '@allxsmith/bestax-bulma'; + +type Color = (typeof validColors)[number]; // 'primary' | 'link' | … | 'dark' +type Spacing = (typeof validSizes)[number]; // '0' | '1' | … | '6' | 'auto' + +interface MyComponentProps { + color?: Color; + m?: Spacing; +} +``` + +They also work as runtime validators: + +```ts +function isColor(value: string): value is (typeof validColors)[number] { + return (validColors as readonly string[]).includes(value); +} +``` + +:::warning `validSizes` is for spacing, not element sizes + +`validSizes` (`'0'`–`'6'` | `'auto'`) enumerates the **spacing helper** scale — the values for `m`/`p` props like `m="4"` (→ `m-4`). Element `size` props are a different axis: `Button`, `Icon`, and friends declare an inline union such as `'small' | 'medium' | 'large'` (Button adds `'normal'`), **not** `validSizes`. Don't use `validSizes` to type a component's `size` prop. + +::: + +--- + +## See Also + +- [`useBulmaClasses`](./usebulmaclasses.md): The hook that consumes these values and generates the helper classes. +- [Helper guides](../../guides/helpers/color.md): Task-oriented walkthroughs of the helper-prop system (color, spacing, typography, flex, visibility). diff --git a/docs/docs/skills/custom-component.mdx b/docs/docs/skills/custom-component.mdx index ab79060aa..2056cd4d7 100644 --- a/docs/docs/skills/custom-component.mdx +++ b/docs/docs/skills/custom-component.mdx @@ -12,8 +12,10 @@ import { ExampleMeta } from '@site/src/components/SkillExamples'; The [`bestax-custom-component`](https://github.com/allxsmith/bestax/blob/main/skills/bestax-custom-component/SKILL.md) skill teaches an agent to build a new custom component the bestax way — starting by -**checking whether a matching component already exists**, then the helper hooks and the Bulma v1 -SCSS pattern, plus stories, tests, and docs. +**checking whether a matching component already exists** — in either of two contexts. In an app +using `@allxsmith/bestax-bulma`, it composes existing components, helper props, the public hooks, +and `--bulma-*` CSS variables; inside the bestax monorepo, it covers the full library pipeline +(helper hooks, the Bulma v1 SCSS pattern, stories, tests, and docs). ## Install diff --git a/skills/README.md b/skills/README.md index d53af00ac..a3de00107 100644 --- a/skills/README.md +++ b/skills/README.md @@ -9,12 +9,12 @@ directory the agent reads on demand. ## Skills -| Skill | Use it when… | -| --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [`bestax-custom-component`](./bestax-custom-component/SKILL.md) | Building a **new custom component** beyond stock Bulma — React + TS, the Bulma v1 SCSS CSS-variable pattern, stories, tests, docs, and export/build wiring. | -| [`bestax-form`](./bestax-form/SKILL.md) | Building **forms** — Field/Control composition, the full input inventory, and the validate-it-yourself error pattern (there is no form library). | -| [`bestax-theming`](./bestax-theming/SKILL.md) | **Theming** — customize colors, branding, fonts/radius tokens, and dark mode by overriding Bulma's `--bulma-*` variables with the `Theme` component. | -| [`bestax-layout-scaffold`](./bestax-layout-scaffold/SKILL.md) | **Layout scaffolding** — turn a high-level request (dashboard, landing page, auth page, catalog) into a complete responsive page from named layout archetypes. | +| Skill | Use it when… | +| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [`bestax-custom-component`](./bestax-custom-component/SKILL.md) | Building a **custom component** beyond stock Bulma — in an app: composition, helper props, public hooks, `--bulma-*` CSS vars; in the monorepo: the full SCSS/stories/tests/docs pipeline. | +| [`bestax-form`](./bestax-form/SKILL.md) | Building **forms** — Field/Control composition, the full input inventory, and the validate-it-yourself error pattern (there is no form library). | +| [`bestax-theming`](./bestax-theming/SKILL.md) | **Theming** — customize colors, branding, fonts/radius tokens, and dark mode by overriding Bulma's `--bulma-*` variables with the `Theme` component. | +| [`bestax-layout-scaffold`](./bestax-layout-scaffold/SKILL.md) | **Layout scaffolding** — turn a high-level request (dashboard, landing page, auth page, catalog) into a complete responsive page from named layout archetypes. | ## Install @@ -32,11 +32,14 @@ npx skills add https://github.com/allxsmith/bestax --skill bestax-layout-scaffol ``` skills/ bestax-custom-component/ - SKILL.md + SKILL.md # app context (compose + public hooks + --bulma-* vars) references/ api.md # helper hooks, classNames, valid-value constants, SCSS utilities - patterns.md # Dialog — the canonical worked example + patterns.md # Dialog — the canonical library-contributor worked example component-catalog.md # generated: every exported component + one-line purpose + library-contributor.md # monorepo pipeline: SCSS partial, stories, tests, docs, wiring + examples/ + stat-card.tsx # app-side worked example (composition + helper props + scoped CSS) bestax-form/ SKILL.md references/ diff --git a/skills/bestax-custom-component/SKILL.md b/skills/bestax-custom-component/SKILL.md index 0d052625a..283a2a247 100644 --- a/skills/bestax-custom-component/SKILL.md +++ b/skills/bestax-custom-component/SKILL.md @@ -1,21 +1,21 @@ --- name: bestax-custom-component -description: Build a new custom Bulma "extra" component for @allxsmith/bestax-bulma — a React + TypeScript component with Bulma v1 SCSS (the CSS-variable pattern), Storybook stories, tests, and docs. Use when adding a component that goes beyond stock Bulma (like Dialog, Carousel, Switch, Slider, Rate, Taginput), or when extending an existing one to match the library's conventions. +description: 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. license: MIT --- -# Building a custom bestax-bulma component +# Building a custom component the bestax way -This skill teaches the end-to-end pattern the library uses for its custom "extra" -components — the ones that aren't part of stock Bulma. Follow it whenever you add a new -component to `@allxsmith/bestax-bulma`, or refactor a component to match house style. +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. -## Use when +## Which context are you in? -- Creating a new component beyond stock Bulma (an interactive widget, a composed element). -- Writing the component's SCSS and you need it to follow the **Bulma v1 CSS-variable pattern** - (`register-vars` / `getVar`, `--bulma-*` custom properties, the class prefix). -- Wiring a component into the package exports, the SCSS bundle, Storybook, tests, and docs. +- **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. @@ -30,359 +30,130 @@ call. Where to look: - `references/component-catalog.md` — **start here.** Every documented component with a one-line - purpose, grouped by category (generated from the API docs). Scan it for the name and its - synonyms before anything else. -- `bulma-ui/src/index.ts` — the full export list; check here for anything not yet documented (e.g. - raw `*Base` form variants) that the catalog omits. -- `docs/docs/api/{elements,components,form}/` — one doc page per shipped component (full props). -- Storybook titles — `Elements/*`, `Components/*`, `Form/*`. + 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 or extending** 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.) +- **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."_ -## File layout +## Composition first -Every custom component has five files. Mirror the existing names exactly (PascalCase TSX, -`_kebab.scss` partial): +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. -``` -bulma-ui/src/components/MyComponent.tsx # React + TS component -bulma-ui/src/components/MyComponent.stories.tsx # Storybook stories -bulma-ui/src/components/__tests__/MyComponent.test.tsx # Jest + RTL tests -bulma-ui/src/scss/components/_mycomponent.scss # SCSS partial -docs/docs/api/components/mycomponent.md # Docusaurus docs page -``` - -Then wire two index files (see **Wiring & build**). +## The component spine -## Component template - -Components use `forwardRef`, accept Bulma helper props via `BulmaClassesProps`, run them -through `useBulmaClasses`, build their own classes with `usePrefixedClassNames`, and merge -everything with `classNames`. Spread `rest` (the non-helper props) onto the DOM node. +Same shape the library itself uses, with all imports from the package. File at +`src/components/MyComponent.tsx`: ```tsx -import React, { forwardRef } from 'react'; -import { classNames, usePrefixedClassNames } from '../helpers/classNames'; -import { useBulmaClasses, BulmaClassesProps } from '../helpers/useBulmaClasses'; - -export type MyComponentColor = - 'primary' | 'link' | 'info' | 'success' | 'warning' | 'danger'; - -/** - * Props for the MyComponent component. - * - * @property {MyComponentColor} [color] - Bulma color modifier. - * @property {'small' | 'medium' | 'large'} [size] - Size modifier. - * @property {boolean} [isActive] - Whether the component is active. - */ +import type React from 'react'; +import { + classNames, + usePrefixedClassNames, + useBulmaClasses, + type BulmaClassesProps, +} from '@allxsmith/bestax-bulma'; + export interface MyComponentProps extends Omit, 'color'>, Omit { - color?: MyComponentColor; - size?: 'small' | 'medium' | 'large'; - isActive?: boolean; + color?: 'primary' | 'link' | 'info' | 'success' | 'warning' | 'danger'; } -/** - * MyComponent — short description of what it does. - * - * @example - * Hello - */ -export const MyComponent = forwardRef( - ({ color, size, isActive, className, children, ...props }, ref) => { - // 1. Pull Bulma helper classes (m/p, text*, display, etc.) out of props. - const { bulmaHelperClasses, rest } = useBulmaClasses(props); - - // 2. Build this component's own classes (respects the ConfigProvider classPrefix). - const mainClasses = usePrefixedClassNames('mycomponent', { - [`is-${color}`]: !!color, - [`is-${size}`]: !!size, - 'is-active': !!isActive, - }); - - // 3. Merge: own classes + helper classes + caller className. - const combined = classNames(mainClasses, bulmaHelperClasses, className); - - return ( -
- {children} -
- ); - } -); - -MyComponent.displayName = 'MyComponent'; - -export default MyComponent; -``` - -Rules that keep components consistent: - -- **Always `Omit<…, 'color'>`** from both `HTMLAttributes` and `BulmaClassesProps` when the - component exposes its own typed `color`, so the native/helper `color` doesn't collide. -- **Never hand-build class strings.** Use `usePrefixedClassNames(base, conditionalMap)` so the - optional `classPrefix` from `ConfigProvider` is honored, then `classNames(...)` to merge. -- **Spread `rest`, not `props`**, onto the DOM node — `useBulmaClasses` has already stripped the - helper props out of `rest`, so they don't leak to the DOM as invalid attributes. -- **Set `displayName`** on `forwardRef` components (needed for tests and Storybook autodocs). -- **Element sizing uses an inline `'small' | 'medium' | 'large'` union**, mapped to `is-small` / - `is-medium` / `is-large` (see `Tabs.tsx`, `Control.tsx`). Do **not** reach for the `validSizes` - constant — that one is `'0'…'6' | 'auto'` and exists for **spacing** helpers, not element size. -- **Format before you lint.** The repo enforces Prettier and ESLint fails on unformatted code. - Run `pnpm exec prettier --write` on your new files (or `pnpm format` from the repo root) before - `pnpm lint`. Copy snippets as a starting point, then let Prettier normalize them. - -See `references/api.md` for the full helper API and `references/patterns.md` for the complete -Dialog walkthrough. - -## SCSS pattern (required) - -This is the library's house convention — **the Bulma v1 CSS-variable pattern**. Do not write -plain hard-coded CSS or homebrew `--mycomponent-*` variables. Import Bulma's utilities, declare -SCSS vars with `!default`, register them as `--bulma-*` custom properties on the root selector -with `cv.register-vars`, then consume them with `cv.getVar`. Prefix every selector with -`iv.$class-prefix`. - -```scss -// bulma-ui/src/scss/components/_mycomponent.scss -@use 'bulma/sass/utilities/initial-variables' as iv; -@use 'bulma/sass/utilities/css-variables' as cv; - -// 1. SCSS variables, overridable, referencing Bulma vars via cv.getVar. -$mycomponent-radius: cv.getVar('radius') !default; -$mycomponent-background: cv.getVar('scheme-main') !default; -$mycomponent-color: cv.getVar('text') !default; -$mycomponent-padding: 1rem !default; - -// 2. Register them as runtime --bulma-* custom properties on the root selector. -.#{iv.$class-prefix}mycomponent { - @include cv.register-vars( - ( - 'mycomponent-radius': #{$mycomponent-radius}, - 'mycomponent-background': #{$mycomponent-background}, - 'mycomponent-color': #{$mycomponent-color}, - 'mycomponent-padding': #{$mycomponent-padding}, - ) +export function MyComponent({ + color, + className, + children, + ...props +}: MyComponentProps) { + const { bulmaHelperClasses, rest } = useBulmaClasses(props); + const mainClasses = usePrefixedClassNames('mycomponent', { + [`is-${color}`]: !!color, + }); + return ( +
+ {children} +
); } +``` -// 3. Consume via cv.getVar. Prefix every selector with iv.$class-prefix. -.#{iv.$class-prefix}mycomponent { - background-color: cv.getVar('mycomponent-background'); - border-radius: cv.getVar('mycomponent-radius'); - color: cv.getVar('mycomponent-color'); - padding: cv.getVar('mycomponent-padding'); -} - -// Color variants reuse Bulma's registered color vars. -.#{iv.$class-prefix}mycomponent.#{iv.$class-prefix}is-primary { - background-color: cv.getVar('primary'); - color: cv.getVar('primary-invert'); +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`. There is +**no `gap` helper** — space children with `m*`/`p*` margins instead. + +**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); } - -// Respect reduced-motion if you animate. -@media (prefers-reduced-motion: reduce) { - .#{iv.$class-prefix}mycomponent { - animation: none; - } +.mycomponent .mycomponent-value { + color: var(--mycomponent-accent); } ``` -Why this matters: registering vars makes the component themeable at runtime (the docs site and -`Theme`/`ConfigProvider` providers override `--bulma-*` properties), and the `iv.$class-prefix` keeps -the component working when consumers opt into a class prefix to avoid collisions. - -The canonical reference file is `bulma-ui/src/scss/components/_dialog.scss`. - -## Stories - -`MyComponent.stories.tsx` beside the component. Use `tags: ['autodocs']` so the JSDoc becomes -the docs page, declare `argTypes`, and write one named `function`-style render per variant. - -```tsx -import type { Meta, StoryObj } from '@storybook/react'; -import { MyComponent } from './MyComponent'; - -const meta: Meta = { - title: 'Components/MyComponent', - component: MyComponent, - tags: ['autodocs'], - argTypes: { - color: { - control: 'select', - options: ['primary', 'link', 'info', 'success', 'warning', 'danger'], - }, - isActive: { control: 'boolean' }, - }, -}; -export default meta; -type Story = StoryObj; - -export const Default: Story = { - render: function DefaultExample() { - return Default; - }, -}; - -export const Colors: Story = { - render: function ColorsExample() { - return ( - <> - Primary - Danger - - ); - }, -}; -``` - -## Tests - -`__tests__/MyComponent.test.tsx`, Jest + `@testing-library/react`. Cover render, each prop → -class mapping, the helper-prop passthrough, ref forwarding, and any interaction/a11y. - -```tsx -import { render, screen } from '@testing-library/react'; -import { createRef } from 'react'; -import { MyComponent } from '../MyComponent'; - -describe('MyComponent', () => { - it('renders children', () => { - render(Hello); - expect(screen.getByText('Hello')).toBeInTheDocument(); - }); - - it('applies the color modifier', () => { - render(x); - expect(screen.getByText('x')).toHaveClass('mycomponent', 'is-primary'); - }); - - it('passes Bulma helper props through', () => { - render(x); - expect(screen.getByText('x')).toHaveClass('m-3'); - }); - - it('forwards the ref', () => { - const ref = createRef(); - render(x); - expect(ref.current).toBeInstanceOf(HTMLDivElement); - }); -}); -``` - -## Docs page - -`docs/docs/api/components/mycomponent.md` — Overview, Import, a Props table, and `Usage` with -live examples. Live code blocks use the ` ```tsx live ` fence (Docusaurus live-codeblock). - -````md ---- -title: MyComponent -sidebar_label: MyComponent ---- - -# MyComponent - -## Overview - -Short description of the component. - -## Import - -```tsx -import { MyComponent } from '@allxsmith/bestax-bulma'; -``` - -## Props - -| Prop | Type | Default | Description | -| ---------- | ---------------------------- | ------- | --------------------- | -| `color` | `'primary' \| 'link' \| ...` | — | Bulma color modifier. | -| `isActive` | `boolean` | `false` | Active state. | - -## Usage +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). -### Default - -```tsx live -Hello -``` -```` - -> Note: the Docusaurus docs load the **built** dist CSS. After SCSS changes, run -> `cd bulma-ui && pnpm build` before the new styles show up in the docs site (Storybook -> compiles SCSS live and does not need this). - -## Wiring & build - -Two index files must be updated or the component won't ship: - -1. **Package export** — add to `bulma-ui/src/index.ts`, in the **components** group (the file - groups exports by directory — keep yours next to the other `./components/*` lines): - ```ts - export * from './components/MyComponent'; - ``` -2. **SCSS bundle** — add to `bulma-ui/src/scss/components/_index.scss`: - ```scss - @use 'mycomponent'; - ``` - -Then build and verify: - -```sh -cd bulma-ui -pnpm exec prettier --write src/components/MyComponent.tsx src/scss/components/_mycomponent.scss -pnpm lint -pnpm test -pnpm build # compiles JS + the bestax/extras CSS bundles -``` +**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-')`. -## Visually inspect it in a browser +## Verify in the browser -Types and unit tests don't see layout. **Render the component and actually look at it** before -you call it done — spacing, padding, vertical centering, alignment, and every variant/state -(colors, sizes, hover/active, dark mode). Visual bugs hide from `tsc` and `@testing-library`. +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. -1. Run a surface that renders it: `pnpm storybook` (compiles SCSS live) or the docs dev server. -2. Open the component and inspect it. If a browser-automation tool (claude-in-chrome, Playwright) - is available, drive the browser and screenshot each variant; otherwise open it yourself and - eyeball it. -3. Check the usual offenders: - - **Vertical centering of inline text** — `display: inline-block` + `line-height: 1` makes - text sit low. For chips/labels/buttons use `display: inline-flex; align-items: center; -justify-content: center;` with a normal `line-height` (Bulma's `Tag` is the reference). - - Padding/gaps look balanced; nothing clips or overflows. - - Every color/size variant renders; dark mode is legible. +## Tests and stories in an app -Fix what you see, then re-inspect. A green test suite with a misaligned component is not done. +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 -- [ ] **Checked the inventory first** — searched `src/index.ts` / docs / Storybook for an existing - match or synonym, and told the user (reuse/extend it, or confirm there's a genuine gap). -- [ ] `MyComponent.tsx` — `forwardRef`, `Omit<…, 'color'>`, `useBulmaClasses`, - `usePrefixedClassNames`, `classNames`, spread `rest`, `displayName` set. -- [ ] `_mycomponent.scss` — `@use` Bulma utilities, `$vars !default`, `cv.register-vars`, - `cv.getVar`, every selector prefixed with `iv.$class-prefix`. -- [ ] `MyComponent.stories.tsx` — `tags: ['autodocs']`, `argTypes`, one story per variant. -- [ ] `__tests__/MyComponent.test.tsx` — render, prop→class, helper passthrough, ref. -- [ ] `docs/docs/api/components/mycomponent.md` — Overview / Import / Props / `tsx live`. -- [ ] `src/index.ts` exports the component (in the `./components/*` group). -- [ ] `scss/components/_index.scss` `@use`s the partial. -- [ ] Prettier-formatted, then `pnpm lint && pnpm test && pnpm build` all pass. -- [ ] **Rendered and visually inspected in a browser** — centering/spacing/variants all look - right (not just green tests). +- [ ] 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. +- [ ] No inline `style={{}}` anywhere. +- [ ] 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. diff --git a/skills/bestax-custom-component/examples/stat-card.tsx b/skills/bestax-custom-component/examples/stat-card.tsx new file mode 100644 index 000000000..d27eee088 --- /dev/null +++ b/skills/bestax-custom-component/examples/stat-card.tsx @@ -0,0 +1,101 @@ +// StatCard — the app-side worked example for the bestax-custom-component skill. +// (The library-contributor counterpart is Dialog, in references/patterns.md.) +// +// Context: an app depending on @allxsmith/bestax-bulma (e.g. `npm create bestax`). +// Everything imports from the package; no monorepo wiring, no Sass pipeline. +// +// It demonstrates the two lowest rungs of the styling ladder: +// Rung 1 — composition + helper props only (StatCard): Box/Title/Icon plus +// flexbox and spacing helper props. No CSS written at all. +// Rung 2 — a small scoped CSS file consuming --bulma-* variables (the +// commented block at the bottom) for the one thing helper props +// can't do (an accent border), keeping Theme + dark mode working. +import type React from 'react'; +import { + Box, + Title, + Icon, + classNames, + usePrefixedClassNames, + useBulmaClasses, + type BulmaClassesProps, +} from '@allxsmith/bestax-bulma'; + +export interface StatCardProps + extends + Omit, 'color'>, + Omit { + /** Metric label, e.g. "Active users". */ + label: string; + /** The headline value, e.g. "12,481". */ + value: string; + /** Icon name (Font Awesome by default), e.g. "users". */ + icon?: string; + /** Bulma color for the icon + accent. */ + color?: 'primary' | 'link' | 'info' | 'success' | 'warning' | 'danger'; +} + +export function StatCard({ + label, + value, + icon, + color = 'primary', + className, + ...props +}: StatCardProps) { + // The library's own spine, via public exports: helper props in, classes out. + const { bulmaHelperClasses, rest } = useBulmaClasses(props); + const mainClasses = usePrefixedClassNames('statcard', { + [`is-${color}`]: !!color, + }); + + return ( + + {icon && ( + + )} + {/* No `gap` helper exists — space siblings with margin props (mr above). */} +
+ {/* as="p": the sizes are visual scale, not document structure — a bare + renders a heading and breaks the page outline. */} + <Title as="p" size="6" textColor="grey" mb="1"> + {label} + + + {value} + +
+
+ ); +} + +// Rung 2 (optional) — src/components/StatCard.css, imported from this file: +// +// .statcard { +// /* Component-scoped custom props initialized from Bulma tokens, so any +// ancestor (or ) can re-theme the card by overriding them. */ +// --statcard-accent: var(--bulma-primary); +// --statcard-radius: var(--bulma-radius); +// border-left: 0.25rem solid var(--statcard-accent); +// border-radius: var(--statcard-radius); +// } +// .statcard.is-success { --statcard-accent: var(--bulma-success); } +// .statcard.is-danger { --statcard-accent: var(--bulma-danger); } +// +// Only --bulma-*-derived values — never literal colors — so dark mode and +// Theme overrides keep working. Caveat: if the app uses the prefixed CSS +// flavor / ConfigProvider classPrefix, usePrefixedClassNames renders +// `bestax-statcard`; adjust the selectors (or use plain classNames). diff --git a/skills/bestax-custom-component/references/api.md b/skills/bestax-custom-component/references/api.md index 75a078603..3729b0d95 100644 --- a/skills/bestax-custom-component/references/api.md +++ b/skills/bestax-custom-component/references/api.md @@ -1,6 +1,11 @@ # Reference: helper APIs for building components -The shared helpers live in `bulma-ui/src/helpers/`. Import them from there in components. +Everything below is public API. Where to import from depends on your context: + +| Context | Import from | +| --------------------------------------------- | ------------------------------------------------ | +| An app depending on `@allxsmith/bestax-bulma` | `'@allxsmith/bestax-bulma'` | +| Inside the bestax monorepo (`bulma-ui/src/`) | Relative paths — `'../helpers/classNames'`, etc. | ## `useBulmaClasses(props)` — `helpers/useBulmaClasses.tsx` @@ -72,6 +77,10 @@ hard-coding values. @use 'bulma/sass/utilities/css-variables' as cv; // cv.getVar, cv.register-vars ``` +In an app these work too (styling-ladder rung 3 in `SKILL.md`): `npm i -D sass` and Vite +compiles imported `.scss` zero-config — `bulma` resolves because it's a runtime dependency of +bestax-bulma. + - `iv.$class-prefix` — the configurable class prefix; prepend to every selector. - `cv.getVar("name")` — emits `var(--bulma-name)`; use for both Bulma vars (`"primary"`, `"radius"`, `"scheme-main"`, `"text"`) and your own registered vars. diff --git a/skills/bestax-custom-component/references/component-catalog.md b/skills/bestax-custom-component/references/component-catalog.md index 612335a77..53da7d059 100644 --- a/skills/bestax-custom-component/references/component-catalog.md +++ b/skills/bestax-custom-component/references/component-catalog.md @@ -19,7 +19,7 @@ instead of hand-writing markup. - Raw `*Base` form exports (`InputBase`, `SelectBase`, `TextAreaBase`, …) are escape-hatch variants of the convenience wrappers above them; see the Form docs. -85 documented components. Generated from the API docs — every exported +87 documented components. Generated from the API docs — every exported component is guaranteed to appear (the generator fails if one lacks an API page). ## Elements @@ -125,5 +125,7 @@ component is guaranteed to appear (the generator fails if one lacks an API page) - [ConfigProvider](https://bestax.io/docs/api/helpers/config) — The `ConfigProvider` component provides a React context for configuring global settings across all Bulma UI components. - [Theme](https://bestax.io/docs/api/helpers/theme) — The `Theme` component provides a powerful way to customize Bulma's appearance using CSS custom properties (CSS variables). +- [Valid value constants](https://bestax.io/docs/api/helpers/valid-values) — The `valid*` constant arrays enumerate every accepted value for the shared Bulma helper props — public API you can import to build prop types and validation. - [classNames](https://bestax.io/docs/api/helpers/classnames) — `classNames` is a utility function for conditionally joining class names together. - [useBulmaClasses](https://bestax.io/docs/api/helpers/usebulmaclasses) — `useBulmaClasses` is a custom React hook that generates Bulma helper class strings from a set of props. +- [usePrefixedClassNames](https://bestax.io/docs/api/helpers/useprefixedclassnames) — `usePrefixedClassNames` builds a component class string that honors the `classPrefix` from `ConfigProvider` — the hook every bestax component uses for its own… diff --git a/skills/bestax-custom-component/references/library-contributor.md b/skills/bestax-custom-component/references/library-contributor.md new file mode 100644 index 000000000..3b06b18cd --- /dev/null +++ b/skills/bestax-custom-component/references/library-contributor.md @@ -0,0 +1,395 @@ +# Reference: building a component inside the bestax monorepo + +You are inside the bestax monorepo — import helpers from relative paths +(`../helpers/classNames`), not from the package. This is the full contributor pipeline for a +custom "extra" component: React + TS, the Bulma v1 SCSS pattern, stories, tests, docs, and +wiring. For **form** components (Field/Control/Input/etc.) use the `bestax-form` skill instead. + +## File layout + +Every custom component has five files. Mirror the existing names exactly (PascalCase TSX, +`_kebab.scss` partial): + +``` +bulma-ui/src/components/MyComponent.tsx # React + TS component +bulma-ui/src/components/MyComponent.stories.tsx # Storybook stories +bulma-ui/src/components/__tests__/MyComponent.test.tsx # Jest + RTL tests +bulma-ui/src/scss/components/_mycomponent.scss # SCSS partial +docs/docs/api/components/mycomponent.md # Docusaurus docs page +``` + +Then wire two index files (see **Wiring & build**). + +## Component template + +Components accept Bulma helper props via `BulmaClassesProps`, run them through +`useBulmaClasses`, build their own classes with `usePrefixedClassNames`, and merge everything +with `classNames`. Spread `rest` (the non-helper props) onto the DOM node. + +Use `forwardRef` when consumers need the DOM node (focus, measurement, observers) — typical +for interactive extras, so this template uses it. Simpler wrappers in the library are plain +function components; match the siblings in the target folder. + +```tsx +import React, { forwardRef } from 'react'; +import { classNames, usePrefixedClassNames } from '../helpers/classNames'; +import { useBulmaClasses, BulmaClassesProps } from '../helpers/useBulmaClasses'; + +export type MyComponentColor = + 'primary' | 'link' | 'info' | 'success' | 'warning' | 'danger'; + +/** + * Props for the MyComponent component. + * + * @property {MyComponentColor} [color] - Bulma color modifier. + * @property {'small' | 'medium' | 'large'} [size] - Size modifier. + * @property {boolean} [isActive] - Whether the component is active. + */ +export interface MyComponentProps + extends + Omit, 'color'>, + Omit { + color?: MyComponentColor; + size?: 'small' | 'medium' | 'large'; // element size union — never the spacing 'validSizes' constant ('0'…'6'|'auto') + isActive?: boolean; +} + +/** + * MyComponent — short description of what it does. + * + * @example + * Hello + */ +export const MyComponent = forwardRef( + ({ color, size, isActive, className, children, ...props }, ref) => { + // 1. Pull Bulma helper classes (m/p, text*, display, etc.) out of props. + const { bulmaHelperClasses, rest } = useBulmaClasses(props); + + // 2. Build this component's own classes (respects the ConfigProvider classPrefix). + const mainClasses = usePrefixedClassNames('mycomponent', { + [`is-${color}`]: !!color, + [`is-${size}`]: !!size, + 'is-active': !!isActive, + }); + + // 3. Merge: own classes + helper classes + caller className. + const combined = classNames(mainClasses, bulmaHelperClasses, className); + + return ( +
+ {children} +
+ ); + } +); + +MyComponent.displayName = 'MyComponent'; + +export default MyComponent; +``` + +Rules that keep components consistent: + +- **Always `Omit<…, 'color'>`** from both `HTMLAttributes` and `BulmaClassesProps` when the + component exposes its own typed `color`, so the native/helper `color` doesn't collide. +- **Never hand-build class strings.** Use `usePrefixedClassNames(base, conditionalMap)` so the + optional `classPrefix` from `ConfigProvider` is honored, then `classNames(...)` to merge. +- **Spread `rest`, not `props`**, onto the DOM node — `useBulmaClasses` has already stripped the + helper props out of `rest`, so they don't leak to the DOM as invalid attributes. +- **Set `displayName`** on `forwardRef` components (needed for tests and Storybook autodocs). +- **Element sizing uses an inline `'small' | 'medium' | 'large'` union**, mapped to `is-small` / + `is-medium` / `is-large` (see `Tabs.tsx`, `Control.tsx`). Do **not** reach for the `validSizes` + constant — that one is `'0'…'6' | 'auto'` and exists for **spacing** helpers, not element size. +- **Format before you lint.** The repo enforces Prettier and ESLint fails on unformatted code. + Run `pnpm exec prettier --write` on your new files (or `pnpm format` from the repo root) before + `pnpm lint`. Copy snippets as a starting point, then let Prettier normalize them. + +See `api.md` for the full helper API and `patterns.md` for the complete Dialog walkthrough. + +## SCSS pattern (required) + +This is the library's house convention — **the Bulma v1 CSS-variable pattern**. Do not write +plain hard-coded CSS or homebrew `--mycomponent-*` variables. Import Bulma's utilities, declare +SCSS vars with `!default`, register them as `--bulma-*` custom properties on the root selector +with `cv.register-vars`, then consume them with `cv.getVar`. Prefix every selector with +`iv.$class-prefix`. + +```scss +// bulma-ui/src/scss/components/_mycomponent.scss +@use 'bulma/sass/utilities/initial-variables' as iv; +@use 'bulma/sass/utilities/css-variables' as cv; + +// 1. SCSS variables, overridable, referencing Bulma vars via cv.getVar. +$mycomponent-radius: cv.getVar('radius') !default; +$mycomponent-background: cv.getVar('scheme-main') !default; +$mycomponent-color: cv.getVar('text') !default; +$mycomponent-padding: 1rem !default; + +// 2. Register them as runtime --bulma-* custom properties on the root selector. +.#{iv.$class-prefix}mycomponent { + @include cv.register-vars( + ( + 'mycomponent-radius': #{$mycomponent-radius}, + 'mycomponent-background': #{$mycomponent-background}, + 'mycomponent-color': #{$mycomponent-color}, + 'mycomponent-padding': #{$mycomponent-padding}, + ) + ); +} + +// 3. Consume via cv.getVar. Prefix every selector with iv.$class-prefix. +.#{iv.$class-prefix}mycomponent { + background-color: cv.getVar('mycomponent-background'); + border-radius: cv.getVar('mycomponent-radius'); + color: cv.getVar('mycomponent-color'); + padding: cv.getVar('mycomponent-padding'); +} + +// Color variants reuse Bulma's registered color vars. +.#{iv.$class-prefix}mycomponent.#{iv.$class-prefix}is-primary { + background-color: cv.getVar('primary'); + color: cv.getVar('primary-invert'); +} + +// Respect reduced-motion if you animate. +@media (prefers-reduced-motion: reduce) { + .#{iv.$class-prefix}mycomponent { + animation: none; + } +} +``` + +Why this matters: registering vars makes the component themeable at runtime (the docs site and +`Theme`/`ConfigProvider` providers override `--bulma-*` properties), and the `iv.$class-prefix` keeps +the component working when consumers opt into a class prefix to avoid collisions. + +Register **all** themable values — durations and offsets included — and prefer Bulma tokens +(`cv.getVar('radius-rounded')`, never `9999px`); derive dark-mode-affected surfaces from scheme +tokens (`scheme-main`, `text`, `border`). When the component is themeable, add rows to +`skills/bestax-theming/references/themeable-components.md` and `css-variables.md` in the same PR. + +The canonical reference file is `bulma-ui/src/scss/components/_dialog.scss`. + +## Stories + +`MyComponent.stories.tsx` beside the component. Use `tags: ['autodocs']` so the JSDoc becomes +the docs page, declare `argTypes`, and write one named `function`-style render per variant. +Give every argType a `description` — enforced by a jest meta-test. + +```tsx +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { MyComponent } from './MyComponent'; + +const meta: Meta = { + title: 'Components/MyComponent', + component: MyComponent, + tags: ['autodocs'], + argTypes: { + color: { + control: 'select', + options: ['primary', 'link', 'info', 'success', 'warning', 'danger'], + description: 'Bulma color modifier applied to the component.', + }, + isActive: { + control: 'boolean', + description: 'Whether the component renders in its active state.', + }, + }, +}; +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: function DefaultExample() { + return Default; + }, +}; + +export const Colors: Story = { + render: function ColorsExample() { + return ( + <> + Primary + Danger + + ); + }, +}; +``` + +## Tests + +`__tests__/MyComponent.test.tsx`, Jest + `@testing-library/react`. Cover render, each prop → +class mapping, the helper-prop passthrough, ref forwarding, the ConfigProvider prefix, and any +interaction/a11y. + +```tsx +import { render, screen } from '@testing-library/react'; +import { createRef } from 'react'; +import { MyComponent } from '../MyComponent'; +import { ConfigProvider } from '../../helpers/Config'; + +describe('MyComponent', () => { + it('renders children', () => { + render(Hello); + expect(screen.getByText('Hello')).toBeInTheDocument(); + }); + + it('applies the color modifier', () => { + render(x); + expect(screen.getByText('x')).toHaveClass('mycomponent', 'is-primary'); + }); + + it('passes Bulma helper props through', () => { + render(x); + expect(screen.getByText('x')).toHaveClass('m-3'); + }); + + it('forwards the ref', () => { + const ref = createRef(); + render(x); + expect(ref.current).toBeInstanceOf(HTMLDivElement); + }); + + it('applies classPrefix from ConfigProvider', () => { + const { container } = render( + + x + + ); + const el = container.querySelector('.bestax-mycomponent'); + expect(el).toBeInTheDocument(); + expect(el).not.toHaveClass('mycomponent'); + }); +}); +``` + +## Docs page + +`docs/docs/api/components/mycomponent.md` — Overview, Import, a Props table, `Usage` with +live examples, then Accessibility, Related Components, and Additional Resources. Live code +blocks use the ` ```tsx live ` fence (Docusaurus live-codeblock). House rules: + +- Frontmatter `title:` **must equal the exported component name** — `gen-component-catalog.mjs` + parses it to build the skill catalog. +- Headings are Title Case. +- Every example gets one prose sentence explaining what it shows. +- No inline `style={{}}` in examples — use helper props. + +````md +--- +title: MyComponent +sidebar_label: MyComponent +--- + +# MyComponent + +## Overview + +Short description of the component. + +## Import + +```tsx +import { MyComponent } from '@allxsmith/bestax-bulma'; +``` + +## Props + +| Prop | Type | Default | Description | +| ---------- | ---------------------------- | ------- | --------------------- | +| `color` | `'primary' \| 'link' \| ...` | — | Bulma color modifier. | +| `isActive` | `boolean` | `false` | Active state. | + +## Usage + +### Default + +A basic MyComponent with default styling. + +```tsx live +Hello +``` + +## Accessibility + +Note roles, keyboard behavior, and reduced-motion handling. + +## Related Components + +- [`Tag`](../elements/tag.md) — for a small colored label instead. + +## Additional Resources + +- [Bulma documentation](https://bulma.io/documentation/) +```` + +> Note: the Docusaurus docs load the **built** dist CSS. After SCSS changes, run +> `cd bulma-ui && pnpm build` before the new styles show up in the docs site (Storybook +> compiles SCSS live and does not need this). + +## Wiring & build + +Two index files must be updated or the component won't ship: + +1. **Package export** — add to `bulma-ui/src/index.ts`, in the **components** group (the file + groups exports by directory — keep yours next to the other `./components/*` lines): + ```ts + export * from './components/MyComponent'; + ``` +2. **SCSS bundle** — add to `bulma-ui/src/scss/components/_index.scss`: + ```scss + @use 'mycomponent'; + ``` + +Then build and verify: + +```sh +cd bulma-ui +pnpm exec prettier --write src/components/MyComponent.tsx src/scss/components/_mycomponent.scss +pnpm lint +pnpm test +pnpm build # compiles JS + the bestax/extras CSS bundles +``` + +Finally run `pnpm gen:catalog` from the repo root — CI's `gen:catalog:check` fails if the skill +component catalog is stale. + +## Visually inspect it in a browser + +Types and unit tests don't see layout. **Render the component and actually look at it** before +you call it done — spacing, padding, vertical centering, alignment, and every variant/state +(colors, sizes, hover/active, dark mode). Visual bugs hide from `tsc` and `@testing-library`. + +1. Run a surface that renders it: `pnpm storybook` (compiles SCSS live) or the docs dev server. +2. Open the component and inspect it. If a browser-automation tool (claude-in-chrome, Playwright) + is available, drive the browser and screenshot each variant; otherwise open it yourself and + eyeball it. +3. Check the usual offenders: + - **Vertical centering of inline text** — `display: inline-block` + `line-height: 1` makes + text sit low. For chips/labels/buttons use `display: inline-flex; align-items: center; +justify-content: center;` with a normal `line-height` (Bulma's `Tag` is the reference). + - Padding/gaps look balanced; nothing clips or overflows. + - Every color/size variant renders; dark mode is legible. + +Fix what you see, then re-inspect. A green test suite with a misaligned component is not done. + +## Checklist + +- [ ] **Checked the inventory first** — searched `src/index.ts` / docs / Storybook for an existing + match or synonym, and told the user (reuse/extend it, or confirm there's a genuine gap). +- [ ] `MyComponent.tsx` — `Omit<…, 'color'>`, `useBulmaClasses`, `usePrefixedClassNames`, + `classNames`, spread `rest`; `forwardRef` + `displayName` when consumers need the node. +- [ ] `_mycomponent.scss` — `@use` Bulma utilities, `$vars !default`, `cv.register-vars`, + `cv.getVar`, every selector prefixed with `iv.$class-prefix`. +- [ ] `MyComponent.stories.tsx` — `tags: ['autodocs']`, `argTypes` (each with a `description`), + one story per variant. +- [ ] `__tests__/MyComponent.test.tsx` — render, prop→class, helper passthrough, ref + ConfigProvider prefix test (required). +- [ ] `docs/docs/api/components/mycomponent.md` — Overview / Import / Props / `tsx live` / + Accessibility / Related Components / Additional Resources; frontmatter `title:` = export name. +- [ ] `src/index.ts` exports the component (in the `./components/*` group). +- [ ] `scss/components/_index.scss` `@use`s the partial. +- [ ] Themeable values registered; theming skill references updated in the same PR if applicable. +- [ ] Prettier-formatted, then `pnpm lint && pnpm test && pnpm build` pass; `pnpm gen:catalog` run. +- [ ] **Rendered and visually inspected in a browser** — centering/spacing/variants all look + right (not just green tests). diff --git a/skills/bestax-custom-component/references/patterns.md b/skills/bestax-custom-component/references/patterns.md index a3ff29caf..0a73651e5 100644 --- a/skills/bestax-custom-component/references/patterns.md +++ b/skills/bestax-custom-component/references/patterns.md @@ -1,5 +1,8 @@ # Reference: Dialog, the canonical worked example +**Library-contributor worked example** — the in-monorepo counterpart to `examples/stat-card.tsx`; +follow it together with `library-contributor.md`. + `Dialog` is the library's reference implementation of the custom-component pattern. Read the real files alongside this: @@ -121,8 +124,8 @@ Dialog also shows optional patterns you can borrow when relevant: - **Accessibility**: `role="alertdialog"`, Escape-to-cancel, and focus management on open. - **Body scroll lock** via a module-level ref count so chained/overlapping dialogs behave. -These are not required for every component — start from the simple template in `SKILL.md` and -add only what your component needs. +These are not required for every component — start from the simple template in +`library-contributor.md` and add only what your component needs. ## Other components worth reading for variety diff --git a/skills/bestax-theming/references/css-variables.md b/skills/bestax-theming/references/css-variables.md index 3258c5971..2711593b3 100644 --- a/skills/bestax-theming/references/css-variables.md +++ b/skills/bestax-theming/references/css-variables.md @@ -98,10 +98,15 @@ and numeric shades `--bulma--00` … `--bulma--95`. ## Extras component variables (Avatar / Avatars / Badge) -These are registered on the component's own selector (`.avatar`, `.avatars`, `.badge`), not on -`:root`, so override them with a scoped rule, the component's `style`/`className`, or `Theme`'s -`bulmaVars` on a wrapping `Theme`. Several default to core theme vars above, so they already flow -through a custom theme. +These are registered on the component's **own selector** (`.avatar`, `.avatars`, `.badge` — +`.bestax-avatar` etc. with the prefixed CSS flavor), not on `:root`. A value set on a wrapping +ancestor — including `Theme`'s `bulmaVars` on a wrapping `Theme` — is only _inherited_ and +always loses to the component-level declaration, so it will NOT take effect. Working overrides +target the component's own element instead: redeclare on the component's own class in your CSS +(mind the class prefix), e.g. `.avatar { --bulma-avatar-size: 3.5rem; }`, or pass a +`className` and scope the override under it +(`.avatar.big-avatar { --bulma-avatar-size: 3.5rem; }`), or set it via the component's `style` +prop. Several default to core theme vars above, so they already flow through a custom theme. | Variable | Default | | ----------------------------------------------------------------- | -------------------------------- |