create-bestax 4.2.16 → 4.2.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +13 -5
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +1 -1
  4. package/dist/constants.d.ts +2 -0
  5. package/dist/constants.d.ts.map +1 -1
  6. package/dist/constants.js +7 -2
  7. package/dist/display.d.ts +9 -0
  8. package/dist/display.d.ts.map +1 -1
  9. package/dist/display.js +19 -1
  10. package/dist/file-system.d.ts +16 -3
  11. package/dist/file-system.d.ts.map +1 -1
  12. package/dist/file-system.js +41 -5
  13. package/dist/project-creator.d.ts +16 -1
  14. package/dist/project-creator.d.ts.map +1 -1
  15. package/dist/project-creator.js +35 -6
  16. package/dist/prompts.d.ts +1 -0
  17. package/dist/prompts.d.ts.map +1 -1
  18. package/dist/prompts.js +3 -2
  19. package/dist/validators.d.ts +8 -0
  20. package/dist/validators.d.ts.map +1 -1
  21. package/dist/validators.js +18 -1
  22. package/package.json +7 -7
  23. package/templates/skills/bestax-custom-component/SKILL.md +36 -1
  24. package/templates/skills/bestax-custom-component/references/api.md +36 -14
  25. package/templates/skills/bestax-custom-component/references/component-catalog.md +10 -2
  26. package/templates/skills/bestax-custom-component/references/library-contributor.md +4 -1
  27. package/templates/skills/bestax-form/SKILL.md +46 -30
  28. package/templates/skills/bestax-form/references/api.md +15 -10
  29. package/templates/skills/bestax-form/references/patterns.md +11 -6
  30. package/templates/skills/bestax-icons/SKILL.md +8 -1
  31. package/templates/skills/bestax-icons/references/icon-libraries.md +35 -2
  32. package/templates/skills/bestax-layout-scaffold/SKILL.md +54 -4
  33. package/templates/skills/bestax-layout-scaffold/references/archetypes.md +57 -0
  34. package/templates/skills/bestax-layout-scaffold/references/layout-components.md +3 -3
  35. package/templates/skills/bestax-migrate/SKILL.md +23 -2
  36. package/templates/skills/bestax-migrate/references/bloomer/prop-map.md +34 -8
  37. package/templates/skills/bestax-migrate/references/bloomer/unmappables.md +18 -0
  38. package/templates/skills/bestax-migrate/references/bulma-classes/component-map.md +9 -4
  39. package/templates/skills/bestax-migrate/references/bulma-classes/prop-map.md +1 -1
  40. package/templates/skills/bestax-migrate/references/bulma-classes/unmappables.md +317 -4
  41. package/templates/skills/bestax-migrate/references/css-migration.md +5 -1
  42. package/templates/skills/bestax-migrate/references/rbx/prop-map.md +31 -7
  43. package/templates/skills/bestax-migrate/references/rbx/unmappables.md +41 -14
  44. package/templates/skills/bestax-migrate/references/react-bulma-components/component-map.md +3 -3
  45. package/templates/skills/bestax-migrate/references/react-bulma-components/prop-map.md +22 -0
  46. package/templates/skills/bestax-migrate/references/react-bulma-components/unmappables.md +33 -8
  47. package/templates/skills/bestax-optimize/SKILL.md +2 -0
  48. package/templates/skills/bestax-optimize/references/modular-build.md +23 -12
  49. package/templates/skills/bestax-theming/SKILL.md +4 -1
  50. package/templates/skills/bestax-theming/references/css-variables.md +130 -2
  51. package/templates/skills/bestax-theming/references/themeable-components.md +8 -2
  52. package/templates/vite-ts/package.json +1 -0
  53. package/templates/vite-ts/tsconfig.app.json +10 -9
  54. package/templates/vite-ts/tsconfig.node.json +15 -5
  55. package/templates/vite-ts/src/vite-env.d.ts +0 -1
@@ -63,10 +63,45 @@ Wrap it in the library's `ClientOnly` rather than checking `typeof window`: it r
63
63
  until the page has hydrated, so the server and client markup match. `references/api.md` has the
64
64
  details.
65
65
 
66
+ A button that turns something on or off and stays that way (Bold, Mute, Show archived) is
67
+ `ToggleButton`: use it rather than `Button isActive`, which only draws a shade and sets no
68
+ `aria-pressed`. It's always `type="button"`, holds its own state (`defaultPressed`) or takes
69
+ `pressed` with `onPressedChange`, and fills with `color` (default `link`) only while pressed.
70
+ Its label stays the same in both states, and an icon-only one needs an `aria-label`. A set goes
71
+ in `Buttons hasAddons role="group" aria-label="…"`, where it's also `Buttons.ToggleButton`. A row
72
+ where only one can be on is a radio group, so it's `SegmentedControl` (below), never toggles that
73
+ switch each other off. A setting saved with a form is a `Switch` or a `Checkbox`.
74
+
75
+ A row of buttons where only one can be on (text alignment, a list or grid view, monthly or yearly
76
+ billing) is `SegmentedControl`: use it rather than `Buttons hasAddons` with `isActive` or a fill
77
+ you move yourself. Give it an `aria-label`, put one `SegmentedControl.Segment value="…"` inside per
78
+ option, and hold the choice with `defaultValue`, or `value` with `onChange(value)`. A screen reader
79
+ hears a radio group, and the single tab stop, the arrow keys, Home and End come built in, so add
80
+ no `onKeyDown` or `role` of your own. An icon-only segment needs its own `aria-label`, as an
81
+ icon-only `ToggleButton` does. The arrow keys check the segment they land on, so `onChange` fires
82
+ on each, and pressing the checked segment keeps it checked. It carries no form value: a choice
83
+ the form submits is `Radios`.
84
+
66
85
  A panel opened from a button (a filter form, share options, an inline edit) is `Popover`: use
67
86
  it rather than building one. It anchors the panel, moves focus in and back, and closes on
68
87
  Escape and outside presses.
69
88
 
89
+ A preview that shows up on hover (a profile card on a username, a summary on a link) is
90
+ `HoverCard`: use it rather than wiring pointer events to a `Popover`. It opens on hover and
91
+ keyboard focus after a delay, lets the pointer move onto the card, closes on Escape, and ignores
92
+ touch unless `openOnTap` is on, which suits only a trigger with no tap action of its own (a
93
+ `Button` with no `onClick`, never a link). Its `trigger` has to be a link or a button, and a trigger in running text takes
94
+ `appendToBody`. A form never goes in one; that's `Popover`.
95
+
96
+ A menu of actions that opens on a right-click (Rename and Delete on a table row, Archive on a
97
+ card) is `ContextMenu`: use it rather than an `onContextMenu` handler that positions a
98
+ `Dropdown`. It opens at the point, also on Shift+F10, the context menu key and a long press,
99
+ and its items are `ContextMenu.Item` and `ContextMenu.Divider`, the same components as
100
+ `Dropdown.Item` and `Dropdown.Divider`. Its `trigger` is cloned, not wrapped, so a `Table.Tr`
101
+ works, and the menu needs `aria-label`. Put the same items in a visible `Dropdown` as well:
102
+ nothing tells a screen reader user a context menu is there, and on macOS only VoiceOver opens
103
+ it from the keyboard.
104
+
70
105
  Building something else that floats (a command palette)? Render it through the library's
71
106
  `Portal` rather than `createPortal`: it renders nothing on the server and during hydration, so
72
107
  the server and client markup match. `references/api.md` has the details.
@@ -139,7 +174,7 @@ the children with `gap`. Before writing `style={{ … }}` anywhere, translate ea
139
174
  | `textAlign: 'center'` | `textAlign="centered"` (also `left`, `right`, `justified`) |
140
175
  | `color: '#…'` | `textColor` with the nearest Bulma color: `primary`, `link`, `info`, `success`, `warning`, `danger`, `white`, `black`, `grey` (+ `grey-light`, `grey-dark`, …) |
141
176
  | `backgroundColor: '#…'` | `bgColor` (same palette) |
142
- | `fontSize: …` | `textSize="1"`…`"7"` (`1` largest) — for headings use `Title`/`SubTitle` `size` |
177
+ | `fontSize: …` | `textSize="1"`…`"7"` (`1` largest). On `Title`/`SubTitle`, `as` sets the level and `size` picks it too, so an `<h3>` at size 4 is `as="h3" textSize="4"` |
143
178
  | `fontWeight: …` | `textWeight`: `light`, `normal`, `medium`, `semibold`, `bold` |
144
179
  | `textTransform`, italics | `textTransform`: `uppercase`, `lowercase`, `capitalized`, `italic` |
145
180
  | `display: 'flex'` + flex properties | same-named props: `display="flex"`, `flexDirection`, `justifyContent`, `alignItems`, `flexWrap` |
@@ -88,7 +88,8 @@ union (a compile error beats a silent no-op).
88
88
 
89
89
  ## `ConfigProvider` / `Theme` — `helpers/Config.tsx`, `helpers/Theme.tsx`
90
90
 
91
- `ConfigProvider` provides the runtime `classPrefix` (and `iconLibrary`) consumed via `useConfig`;
91
+ `ConfigProvider` provides the runtime `classPrefix` (and `iconLibrary` and `portalContainer`)
92
+ consumed via `useConfig`;
92
93
  `classPrefix` feeds `usePrefixedClassNames` (opt-in class prefixing to avoid collisions). `Theme`
93
94
  overrides `--bulma-*` custom properties at runtime —
94
95
  which is exactly why component SCSS must register its vars via `cv.register-vars` rather than
@@ -114,17 +115,23 @@ hard-coding values.
114
115
  ## Floating content: `Portal`
115
116
 
116
117
  A panel anchored to the button that opens it is `Popover`, which already portals
117
- (`appendToBody`), traps focus and dismisses itself. `Portal` is for the overlays it doesn't
118
- cover.
119
-
120
- `<Portal container?>` (`helpers/portal.tsx`) renders its children into `document.body`, or into
121
- `container` (an element or a selector), so floating content escapes an ancestor's `overflow`,
118
+ (`appendToBody`), traps focus and dismisses itself, a preview that opens on hover is
119
+ `HoverCard`, which portals the same way, and a menu that opens at a right-click or a long press
120
+ is `ContextMenu`, which always portals and places itself at the point. `Portal` is for the
121
+ overlays they don't cover.
122
+
123
+ `<Portal container?>` (`helpers/portal.tsx`) renders its children into `container` (an element or
124
+ a selector), or without one into the `portalContainer` a `ConfigProvider` sets, and
125
+ `document.body` when none is set, so floating content escapes an ancestor's `overflow`,
122
126
  `transform` or stacking context. It renders nothing on the server and during hydration, so the
123
127
  first client render matches; `disabled` renders in place instead. Use it rather than calling
124
128
  `createPortal` yourself, which React's server renderer can't render.
125
129
 
126
- Focus follows the DOM, not the React tree: portaled content comes last in the Tab order, so move
127
- focus into it when it opens and back to its trigger when it closes. To trap portaled content,
130
+ Focus follows the DOM, not the React tree: portaled content sits in the Tab order where its
131
+ container is, which for the default `document.body` is last, so an overlay you build that takes
132
+ focus moves it in when it opens and back to its trigger when it closes. A `HoverCard` moves no
133
+ focus when it opens, and a `ContextMenu` moves it to its first item and closes on Tab, handing
134
+ focus back. To trap portaled content,
128
135
  put the trap's ref on the element inside the `Portal`, as below with
129
136
  `useFocusTrap(panelRef, { active: open, restoreFocus: buttonRef })`. A focus trap doesn't cover
130
137
  what a `Portal` inside it renders, so render nested overlays inside the trapped element.
@@ -141,17 +148,28 @@ what a `Portal` inside it renders, so render nested overlays inside the trapped
141
148
  }
142
149
  ```
143
150
 
151
+ Content in `document.body` loses a scoped `Theme`'s variables and a shadow root's styles. In a web
152
+ component or a themed section, wrap the area in a `ConfigProvider` whose `portalContainer` is an
153
+ element inside it, which also covers what the library's components portal, such as `Popover`'s
154
+ `appendToBody` panel or a `Modal` with `portal`. In a shadow root, pass the
155
+ element itself, held in state from a ref callback, since a selector can't reach inside one. What
156
+ moves there is `position: fixed`, so the element must not sit under an ancestor with a
157
+ `transform`, `filter`, `backdrop-filter`, `perspective`, `will-change: transform`, or a `contain`
158
+ that includes `paint` or `layout`: that ancestor becomes the containing block, and overlays and
159
+ anchored panels land against it instead of the viewport.
160
+
144
161
  ## Holding focus: `useFocusTrap`
145
162
 
146
163
  `useFocusTrap(ref, { active, initialFocusRef, restoreFocus })` (`helpers/useFocusTrap.ts`) moves
147
164
  focus into `ref` when `active` turns on, wraps Tab at the first and last tab stops (the ones the
148
165
  browser visits, so hidden, disabled, inert and `tabIndex={-1}` elements are skipped and a radio
149
166
  group counts once) and restores focus when it turns off. It handles Tab only: wire Escape to
150
- close. Pass the trigger's ref as `restoreFocus` for a panel opened from a button. It looks for
151
- the container again each time the component calling it renders, so `active` can follow `open`
152
- alone even when the panel mounts a render later, as portaled content can. The example below
153
- shows the hook's wiring; for a real filter panel opened from a button, use `Popover`, which does
154
- all of this.
167
+ close, and call `preventDefault()` on the Escape it closes on, so a `Modal` or `Sidebar` around
168
+ the panel stays open for the next one. Pass the trigger's ref as `restoreFocus` for a panel
169
+ opened from a button. It looks for the container again each time the component calling it
170
+ renders, so `active` can follow `open` alone even when the panel mounts a render later, as
171
+ portaled content can. The example below shows the hook's wiring; for a real filter panel opened
172
+ from a button, use `Popover`, which does all of this.
155
173
 
156
174
  ```tsx
157
175
  function FilterPanel({ children }: { children: React.ReactNode }) {
@@ -176,7 +194,11 @@ function FilterPanel({ children }: { children: React.ReactNode }) {
176
194
  role="dialog"
177
195
  aria-label="Filters"
178
196
  tabIndex={-1}
179
- onKeyDown={e => e.key === 'Escape' && setOpen(false)}
197
+ onKeyDown={e => {
198
+ if (e.key !== 'Escape') return;
199
+ e.preventDefault();
200
+ setOpen(false);
201
+ }}
180
202
  >
181
203
  {children}
182
204
  </div>
@@ -37,7 +37,7 @@ instead of hand-writing markup.
37
37
  - Raw `*Base` form exports (`InputBase`, `SelectBase`, `TextAreaBase`, …) are
38
38
  escape-hatch variants of the convenience wrappers above them; see the Form docs.
39
39
 
40
- 94 documented components. Generated from the API docs — every exported
40
+ 102 documented components. Generated from the API docs — every exported
41
41
  component is guaranteed to appear (the generator fails if one lacks an API page).
42
42
 
43
43
  ## Elements
@@ -55,6 +55,7 @@ component is guaranteed to appear (the generator fails if one lacks an API page)
55
55
  - [Icon](https://bestax.io/docs/api/elements/icon) — The `Icon` component is a Bulma-styled wrapper for displaying icons from various libraries (Font Awesome, Material Design Icons, Ionicons…
56
56
  - [IconText](https://bestax.io/docs/api/elements/icontext) — The `IconText` component provides a Bulma-styled horizontal arrangement of one or more `Icon` components and optional text.
57
57
  - [Image](https://bestax.io/docs/api/elements/image) — The `Image` component wraps images, iframes, or custom content in a Bulma-styled container, supporting fixed sizes, aspect ratios, rounded corners…
58
+ - [Kbd](https://bestax.io/docs/api/elements/kbd) — The `Kbd` component renders a `<kbd>` drawn as a keycap, for a key the reader presses.
58
59
  - [Link](https://bestax.io/docs/api/elements/link) — The `Link` component renders a styled anchor (`<a>`) element with Bulma helper class integration.
59
60
  - [LinkButton](https://bestax.io/docs/api/elements/linkbutton) — The `LinkButton` component renders a `<button>` that visually looks like text or a link.
60
61
  - [ListItem](https://bestax.io/docs/api/elements/listitem) — The `ListItem` component renders a styled list item (`<li>`) element with Bulma helper class integration.
@@ -72,6 +73,7 @@ component is guaranteed to appear (the generator fails if one lacks an API page)
72
73
  - [Tag](https://bestax.io/docs/api/elements/tag) — The `Tag` component renders a Bulma-styled label or badge.
73
74
  - [Tags](https://bestax.io/docs/api/elements/tags) — The `Tags` component groups multiple `Tag` components together in a Bulma-styled container that wraps onto multiple lines by default.
74
75
  - [Title](https://bestax.io/docs/api/elements/title) — The `Title` component renders a Bulma-styled title (heading), supporting sizes `1`-`6`, spacing, and rendering as any heading or paragraph element (`h1`-`h6`…
76
+ - [ToggleButton](https://bestax.io/docs/api/elements/togglebutton) — The `ToggleButton` component renders a button that stays pressed until it's pressed again, and says whether it's on through `aria-pressed`.
75
77
  - [UnorderedList](https://bestax.io/docs/api/elements/unorderedlist) — The `UnorderedList` component renders a styled unordered list (`<ul>`) element with Bulma helper class integration.
76
78
 
77
79
  ## Components
@@ -84,8 +86,11 @@ component is guaranteed to appear (the generator fails if one lacks an API page)
84
86
  - [Carousel](https://bestax.io/docs/api/components/carousel) — The `Carousel` component provides an image/content slider with navigation arrows and indicators.
85
87
  - [Collapse](https://bestax.io/docs/api/components/collapse) — The `Collapse` component provides an expandable/collapsible content panel.
86
88
  - [Collapses](https://bestax.io/docs/api/components/collapses) — The `Collapses` component groups `Collapse` items into an accordion that keeps one item open at a time, or any number with `multiple`.
89
+ - [ContextMenu](https://bestax.io/docs/api/components/contextmenu) — The `ContextMenu` component opens a menu of actions where you right-click an element, such as a table row or a card, or long press it on a touch screen.
87
90
  - [Dialog](https://bestax.io/docs/api/components/dialog) — The `Dialog` component provides ready-made confirm and alert dialogs, so a destructive action stays one `await dialog.confirm()` call away.
88
91
  - [Dropdown](https://bestax.io/docs/api/components/dropdown) — The `Dropdown` component provides Bulma's versatile dropdown menu for your Bulma React UI.
92
+ - [EmptyState](https://bestax.io/docs/api/components/emptystate) — The `EmptyState` component renders the block an app shows where content would be: an empty list, a search with no results, or a first-run screen.
93
+ - [HoverCard](https://bestax.io/docs/api/components/hovercard) — The `HoverCard` component shows a card previewing what's behind a link or a button while the pointer rests on it or keyboard focus is on it.
89
94
  - [Loading](https://bestax.io/docs/api/components/loading) — The `Loading` component provides a loading overlay with a spinner animation.
90
95
  - [Menu](https://bestax.io/docs/api/components/menu) — The `Menu` component provides Bulma's vertical navigation menu: a simple, accessible sidebar or section menu for your Bulma React UI.
91
96
  - [Message](https://bestax.io/docs/api/components/message) — The `Message` component provides Bulma's flexible notice/message box for your Bulma React UI.
@@ -95,11 +100,14 @@ component is guaranteed to appear (the generator fails if one lacks an API page)
95
100
  - [Panel](https://bestax.io/docs/api/components/panel) — The `Panel` component implements Bulma's versatile panel block for React.
96
101
  - [Popover](https://bestax.io/docs/api/components/popover) — The `Popover` component opens a panel of interactive content, such as a filter form or a set of share options, anchored to the element that toggles it.
97
102
  - [Reveal](https://bestax.io/docs/api/components/reveal) — The `Reveal` component animates its content into view as it scrolls into the viewport, backed by `IntersectionObserver`.
103
+ - [SegmentedControl](https://bestax.io/docs/api/components/segmentedcontrol) — The `SegmentedControl` component renders a row of buttons where only one can be on, and tells a screen reader it's a radio group.
98
104
  - [Sidebar](https://bestax.io/docs/api/components/sidebar) — The `Sidebar` component provides a slide-out navigation panel that appears from the left or right side of the screen.
99
105
  - [Steps](https://bestax.io/docs/api/components/steps) — The `Steps` component provides a multi-step progress indicator for wizard flows, checkout processes, or any multi-step workflow.
100
106
  - [Tabs](https://bestax.io/docs/api/components/tabs) — The `Tabs` component provides flexible and fully-featured Bulma tab navigation for your Bulma React UI.
107
+ - [Timeline](https://bestax.io/docs/api/components/timeline) — The `Timeline` component renders a list of events in the order they happened, each with a marker on a line down the side.
101
108
  - [Toast](https://bestax.io/docs/api/components/toast) — The `Toast` component provides brief notification messages with optional action and cancel buttons.
102
109
  - [Tooltip](https://bestax.io/docs/api/components/tooltip) — The `Tooltip` component displays helpful information when users hover over or focus on an element.
110
+ - [TreeView](https://bestax.io/docs/api/components/treeview) — The `TreeView` component renders a tree of items you browse and pick from, the way a file explorer works: one tab stop, the arrow keys to move…
103
111
 
104
112
  ## Form
105
113
 
@@ -147,7 +155,7 @@ component is guaranteed to appear (the generator fails if one lacks an API page)
147
155
 
148
156
  - [ClientOnly](https://bestax.io/docs/api/helpers/clientonly) — `ClientOnly` renders its children only in the browser, after hydration, and a `fallback` until then…
149
157
  - [ConfigProvider](https://bestax.io/docs/api/helpers/config) — The `ConfigProvider` component provides a React context for configuring global settings across all Bulma UI components.
150
- - [Portal](https://bestax.io/docs/api/helpers/portal) — `Portal` renders its children into another part of the page, `document.body` unless `container` says otherwise…
158
+ - [Portal](https://bestax.io/docs/api/helpers/portal) — `Portal` renders its children into another part of the page, `document.body` unless `container` or a `ConfigProvider` says otherwise…
151
159
  - [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).
152
160
  - [Valid value constants](https://bestax.io/docs/api/helpers/valid-values) — The helper tuples enumerate the core accepted values for the shared Bulma helper props — public API for building prop types and validation.
153
161
  - [classNames](https://bestax.io/docs/api/helpers/classnames) — `classNames` is a utility function for conditionally joining class names together.
@@ -168,7 +168,10 @@ Rules that keep components consistent:
168
168
  place in the DOM uses `Portal` (`helpers/portal.tsx`) rather than its own `createPortal` and
169
169
  `typeof document` check. `Portal` renders nothing on the server or during hydration, so the
170
170
  first client render matches the server markup. Test that with `renderToString` plus
171
- `hydrateRoot` and a `console.error` spy, as `portal.test.tsx` does.
171
+ `hydrateRoot` and a `console.error` spy, as `portal.test.tsx` does. A component that has to
172
+ call `createPortal` itself, as `Modal` does to render in place until hydration, resolves its
173
+ target with `usePortalContainer` from the same module, so the `portalContainer` a
174
+ `ConfigProvider` sets reaches it too. Add it to `src/__tests__/portal-container.test.tsx`.
172
175
 
173
176
  - **Focus management builds on `useFocusTrap`.** A new component that holds focus while it is
174
177
  open uses `useFocusTrap` (`helpers/useFocusTrap.ts`) rather than its own Tab handler, so it
@@ -65,7 +65,7 @@ using context (`useInsideField` / `useInsideControl`) to detect their surroundin
65
65
  ```tsx
66
66
  <Field hasAddons>
67
67
  <Control isExpanded>
68
- <InputBase placeholder="Search" />
68
+ <InputBase placeholder="Search" aria-label="Search" />
69
69
  </Control>
70
70
  <Control>
71
71
  <Button color="primary">Go</Button>
@@ -73,6 +73,10 @@ using context (`useInsideField` / `useInsideControl`) to detect their surroundin
73
73
  </Field>
74
74
  ```
75
75
 
76
+ A `grouped` or `hasAddons` Field names none of its controls, and a placeholder disappears once
77
+ the user types, so give each input in the row an `aria-label`, or a visible label on an outer
78
+ `Field` (`references/patterns.md`, "Grouped controls and addons").
79
+
76
80
  ## Component inventory
77
81
 
78
82
  All import from `@allxsmith/bestax-bulma`. Convenience components auto-wrap Field+Control;
@@ -125,30 +129,41 @@ Full-width is `isFullwidth` on every component that supports it (`Button`, `Link
125
129
  spellings compile only where they historically existed: `isFullWidth` everywhere except
126
130
  `Sidebar`, `fullwidth` on `Tabs` only, `fullWidth` on `Sidebar` only.
127
131
 
128
- The `label` prop on the single-control convenience inputs (`Input`, `Select`, `TextArea`,
129
- `File`, `Numberinput`, `Slider`, `DateInput`, `TimeInput`, `DateTimeInput`, `Autocomplete`,
130
- `Taginput`) wires `htmlFor`/`id` automatically — your `id` is used when provided, a
131
- generated one otherwise, and an explicit `labelProps={{ htmlFor }}` wins. The wiring only
132
- happens when the component renders its own `Field` (nested inside one, the `label` prop is
133
- dropped); the date/time pickers skip it in `inline` mode and `Taginput` skips it at
134
- `maxTags` (no visible input to label). A `range` `Slider` points the `for` at its low thumb
135
- and also puts the label at the start of both thumbs' names through `aria-labelledby`
136
- ("Price range Minimum value"). Its `ariaLabel={[low, high]}` replaces those names outright, so
137
- leave it off when a label already names the Slider. The group inputs (`Checkboxes`, `Radios`, `Rate`,
138
- `DateRangeInput`) associate their `label` too, but group-style: the wrapper gets
139
- `role="group"`/`"radiogroup"` and `aria-labelledby` pointing at the label. Composing yourself
140
- also associates: a labeled `Field` names the one control it holds, whether a composed
141
- `InputBase`/`SelectBase`/`TextAreaBase`, a composed `DateInputBase`/`TimeInputBase`/
142
- `DateTimeInputBase` that is not `inline`, or any input above (through the id), or a group
143
- (through `aria-labelledby`). Either way, an `aria-label` or `aria-labelledby` you give a
144
- group wins over the label. A `Checkbox`, `Radio` or `Switch` takes nothing from a `Field`:
145
- each is named by its own children, so put the text there. The association is skipped for
146
- `grouped`/`hasAddons`, and a nested `Field` starts its own scope, so a
147
- horizontal `Field` whose body holds an inner `Field` needs `labelProps={{ htmlFor }}` plus the
148
- control's `id` (for a group, `labelProps={{ id, htmlFor: undefined }}` plus its
149
- `aria-labelledby`, since nothing takes the label's `for` there). Pass
150
- `labelProps={{ htmlFor }}` plus a matching `id` only when you want a stable id, or
151
- `labelProps={{ htmlFor: undefined }}` to opt out.
132
+ **Labels.** Default to the convenience `label` prop, with `horizontal` for a side-by-side row.
133
+ `Input`, `Select`, `TextArea`, `File`, `Numberinput`, `Slider`, `DateInput`, `TimeInput`,
134
+ `DateTimeInput`, `Autocomplete` and `Taginput` then render the `Field` and the label and wire
135
+ `htmlFor`/`id`, to your `id` when you pass one (`<Input label="Email" id="email" horizontal />`);
136
+ an explicit `labelProps={{ htmlFor }}` wins. The group inputs (`Checkboxes`, `Radios`, `Rate`,
137
+ `DateRangeInput`) name their `role="group"`/`"radiogroup"` wrapper through `aria-labelledby`
138
+ instead. Inside a `Field` a convenience input drops its `label`, and an `inline` date/time picker
139
+ has no visible input to wire.
140
+
141
+ Composed, a labeled `Field` names the one control it holds: a composed `InputBase`/`SelectBase`/
142
+ `TextAreaBase`, a composed `DateInputBase`/`TimeInputBase`/`DateTimeInputBase` that is not
143
+ `inline`, or any input above takes its generated id, and a group (a composed `DateRangeInputBase`
144
+ too, `inline` or not) points `aria-labelledby` at the label's own id. **A control with an `id` of
145
+ its own never takes that id**, so `<Field label="Email"><Input id="email" /></Field>` renders a
146
+ label that names nothing: leave the `id` off, or pass the same value as
147
+ `labelProps={{ htmlFor: 'email' }}`. Wire it that way too where the association is skipped, in a
148
+ `grouped`/`hasAddons` row and in a horizontal `Field` whose body holds an inner `Field` (a nested
149
+ `Field` starts its own scope). For a group there, put `labelProps={{ id, htmlFor: undefined }}` on
150
+ the outer `Field` and a matching `aria-labelledby` on the group; the `htmlFor: undefined` keeps an
151
+ unmatched `for` out of server-rendered HTML.
152
+
153
+ A `range` `Slider` points the `for` at its low thumb and starts both thumbs' names with the label
154
+ ("Price range Minimum value"); its `ariaLabel={[low, high]}` replaces those names outright, so
155
+ leave it off when a label already names the Slider. Those thumbs and an `Autocomplete`'s or
156
+ `Taginput`'s suggestion list point `aria-labelledby` at the label's `id`, which a label wired by
157
+ hand has only when you pass one, `labelProps={{ htmlFor, id }}`; without it they keep their
158
+ fallback names ("Minimum value"/"Maximum value", "Suggestions"). A `<label htmlFor>` you put in
159
+ `Field.Label` yourself (`references/patterns.md`) names its control through the `for` alone, so
160
+ label a row holding either with the `Field`'s `label` prop. An `aria-label` or `aria-labelledby`
161
+ you give a group wins over any label, and a `Checkbox`, `Radio` or `Switch` takes nothing from a
162
+ `Field` on its own: its children name it, and a `Field`'s label names one only when you wire it as
163
+ you would any control with an `id` of its own, `labelProps={{ htmlFor }}` plus that control's
164
+ `id`. A labeled `Field` whose `for` nothing takes (a group, a `Checkbox`, `Radio` or `Switch`, an
165
+ input with an `id` of its own, your own markup) drops it once mounted, though server-rendered HTML
166
+ keeps it. `labelProps={{ htmlFor: undefined }}` opts out of the association entirely.
152
167
 
153
168
  ## Convenience vs composed
154
169
 
@@ -265,11 +280,12 @@ for the expected classes/states, and say plainly that the visual pass is still o
265
280
  ## Checklist
266
281
 
267
282
  - [ ] Built from the shipped form components (no hand-rolled inputs / reinvented controls).
268
- - [ ] Every label is programmatically associated — the convenience `label` prop, the group
269
- inputs, and `Field` + single-base composition all do this automatically; pass
270
- `labelProps={{ htmlFor }}` plus a matching `id` only for a stable id, and label a
271
- multi-control `Field`'s controls individually (`aria-label`, `aria-labelledby`, or
272
- a `<label htmlFor>` matching each control's `id`).
283
+ - [ ] Every label is programmatically associated: the convenience `label` prop, the group
284
+ inputs, and `Field` + single-control composition do it on their own. A composed labeled
285
+ `Field` names a control with an `id` of its own, or one in an inner `Field`, only when its
286
+ `labelProps={{ htmlFor }}` matches that `id`, and a multi-control `Field`'s controls are
287
+ labeled individually (`aria-label`, `aria-labelledby`, or a `<label htmlFor>` matching
288
+ each control's `id`).
273
289
  - [ ] Controlled inputs have both `value` and `onChange` (or use `defaultValue` uncontrolled).
274
290
  - [ ] Error state shows via `color="danger"` + `message` + `messageColor="danger"`.
275
291
  - [ ] Grouped/addon layouts use explicit `Field` + `Control` composition.
@@ -8,16 +8,18 @@ via `useBulmaClasses`.
8
8
 
9
9
  Container and layout. Compound parts: `Field.Label`, `Field.Body`.
10
10
 
11
- | Prop | Type | Notes |
12
- | ----------------------- | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
- | `horizontal` | `boolean` | Label + control side by side. Auto-wraps children in `Field.Body`. |
14
- | `grouped` | `boolean \| 'centered' \| 'right' \| 'multiline'` | Controls in a row. |
15
- | `hasAddons` | `boolean \| 'centered' \| 'right'` | Attached controls. |
16
- | `narrow` | `boolean` | Constrain to content width (inside horizontal bodies). |
17
- | `label` | `ReactNode` | Names the one control it holds: a composed base or convenience input through its id, a group through `aria-labelledby` (skipped for `grouped`/`hasAddons`; a nested `Field` starts its own scope). |
18
- | `labelSize` | `'small' \| 'normal' \| 'medium' \| 'large'` | Label size. |
19
- | `labelProps` | label attributes | Props for the `<label>`. An explicit `htmlFor` (even `undefined`) takes over the association; otherwise it is set automatically. An `id` here is what a group's `aria-labelledby` points at. |
20
- | `textColor` / `bgColor` | Bulma color | Helper colors for the field. |
11
+ | Prop | Type | Notes |
12
+ | ----------------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
+ | `horizontal` | `boolean` | Label + control side by side. Auto-wraps children in `Field.Body`. |
14
+ | `grouped` | `boolean \| 'centered' \| 'right' \| 'multiline'` | Controls in a row. |
15
+ | `hasAddons` | `boolean \| 'centered' \| 'right'` | Attached controls. |
16
+ | `narrow` | `boolean` | Constrain to content width (inside horizontal bodies). |
17
+ | `label` | `ReactNode` | Names the one control it holds: a composed base or convenience input with no `id` of its own through a generated id, a group through `aria-labelledby` (skipped for `grouped`/`hasAddons`; a nested `Field` starts its own scope). |
18
+ | `labelSize` | `'small' \| 'normal' \| 'medium' \| 'large'` | Label size. |
19
+ | `labelProps` | label attributes | Props for the `<label>`. An explicit `htmlFor` (even `undefined`) takes over the association; otherwise it is set automatically. For its `id`, see below. |
20
+ | `textColor` / `bgColor` | Bulma color | Helper colors for the field. |
21
+
22
+ An `id` in `labelProps` is what a group's, a range `Slider`'s and an `Autocomplete` or `Taginput` suggestion list's `aria-labelledby` points at. A label wired by hand with `htmlFor` gets no generated id, so give it one, `labelProps={{ htmlFor, id }}`, for a range `Slider`, an `Autocomplete` or a `Taginput`. No group points at a label wired by hand.
21
23
 
22
24
  ## Control — `form/Control.tsx`
23
25
 
@@ -68,6 +70,9 @@ Field/Control/message props. `TextArea` adds `rows` and `hasFixedSize`.
68
70
 
69
71
  - `Checkbox` / `Radio` — single controls with `color`, `size`, `checked`/`defaultChecked`,
70
72
  `onChange`, `disabled`.
73
+ - `Checkbox` also takes `indeterminate`, the mixed state of a "Select all" box over a partly
74
+ checked list. It changes the look and the announcement, not the value: `checked` stays the
75
+ value, so set both from the list.
71
76
  - `Checkboxes` — group wrapper managing an **array** value.
72
77
  - `Radios` — group wrapper managing a **single** selected value.
73
78
 
@@ -131,7 +131,7 @@ Notes:
131
131
  // Search box: input + button attached.
132
132
  <Field hasAddons>
133
133
  <Control isExpanded>
134
- <InputBase placeholder="Search" />
134
+ <InputBase placeholder="Search" aria-label="Search" />
135
135
  </Control>
136
136
  <Control>
137
137
  <Button color="primary">Search</Button>
@@ -141,14 +141,18 @@ Notes:
141
141
  // Two controls on one row.
142
142
  <Field grouped>
143
143
  <Control>
144
- <InputBase placeholder="First" />
144
+ <InputBase placeholder="First" aria-label="First name" />
145
145
  </Control>
146
146
  <Control>
147
- <InputBase placeholder="Last" />
147
+ <InputBase placeholder="Last" aria-label="Last name" />
148
148
  </Control>
149
149
  </Field>
150
150
  ```
151
151
 
152
+ A `grouped` or `hasAddons` Field names none of the controls it holds, and a placeholder
153
+ disappears once the user types, so each input above carries an `aria-label`. For a visible
154
+ label instead, see the last item below.
155
+
152
156
  Recipes built on addons, each written out in full in the docs:
153
157
 
154
158
  - **Password reveal** ([docs](https://bestax.io/docs/api/form/input#password-reveal)): the
@@ -164,9 +168,10 @@ Recipes built on addons, each written out in full in the docs:
164
168
  - **Split button** ([docs](https://bestax.io/docs/api/components/dropdown#split-button)): a
165
169
  `Button` and a `Dropdown` in `Field hasAddons`, one `Control` each. `Buttons hasAddons` does
166
170
  not join the Dropdown's trigger. Keep the default color, since the trigger takes no `color`.
167
- - To label an addon row, put the label on an outer `Field` with `labelProps={{ htmlFor }}` and a
168
- matching `id` on the input. A label inside the `hasAddons` field sits in the row beside the
169
- controls.
171
+ - To label an addon row, put the label on an outer `Field` with `labelProps={{ htmlFor, id }}`
172
+ and a matching `id` on the input. The label's `id` names an `Autocomplete`'s or `Taginput`'s
173
+ suggestion list or a range `Slider`'s thumbs, which point `aria-labelledby` at it. A label
174
+ inside the `hasAddons` field sits in the row beside the controls.
170
175
 
171
176
  ## Horizontal field with explicit label/body
172
177
 
@@ -46,6 +46,12 @@ or `library` silently renders nothing.
46
46
  first thing to check. A redundant `fa-`/`mdi-` prefix in `name` is tolerated (stripped), but
47
47
  don't rely on it.
48
48
 
49
+ **Material Symbols' font is several megabytes however few icons the app shows.** Keep the
50
+ default `material-symbols/outlined.css` import. When the user cares about download size,
51
+ offer a smaller font as an optimisation (a Google Fonts `icon_names` subset, a self-hosted
52
+ subset, or a single-weight package): `references/icon-libraries.md` has each one and its
53
+ trade-offs.
54
+
49
55
  ## Styling
50
56
 
51
57
  - `size` — `'small' | 'medium' | 'large'` sizes the Bulma **container** (`is-small` ≈ 1rem,
@@ -105,6 +111,7 @@ everything else takes the standard `aria-label` attribute, e.g.
105
111
  ## References
106
112
 
107
113
  - `references/icon-libraries.md` — per-library setup (install/import/CDN), the full
108
- name-format and variant tables, `features` values, and the blank-icon troubleshooting list.
114
+ name-format and variant tables, `features` values, making the Material Symbols font
115
+ smaller, and the blank-icon troubleshooting list.
109
116
  - `examples/icon-usage.tsx` — runnable example: ConfigProvider setup, sizes, variants,
110
117
  colors, IconText, and decorative-vs-labeled patterns.
@@ -44,7 +44,7 @@ Set it once on `ConfigProvider` and omit `library` everywhere else.
44
44
 
45
45
  ## Ionicons — `'ion'` ⚠️ value is `ion`, not `ionicons`
46
46
 
47
- - **Setup:** a CDN script pair in `index.html` (no npm package — it registers the
47
+ - **Setup:** a CDN script in `index.html` (no npm package — it registers the
48
48
  `<ion-icon>` web component):
49
49
 
50
50
  ```html
@@ -89,6 +89,39 @@ Set it once on `ConfigProvider` and omit `library` everywhere else.
89
89
  Material Icons' `round`.
90
90
  - **Features:** Bulma classes like `'is-size-1'`.
91
91
 
92
+ ### Making the font smaller
93
+
94
+ `outlined.css` loads the whole variable font (every symbol at every fill, weight, grade and
95
+ optical size), several megabytes however few icons the app shows. Keep that import as the
96
+ default: it is one line, works offline, and any name renders. Offer a smaller font only when
97
+ the user cares about download size (a size budget, a Lighthouse flag, slow networks), and
98
+ present it as an optimisation with the trade-offs below:
99
+
100
+ - **One stylesheet per style rendered.** If the app has the bare `import 'material-symbols';`,
101
+ replace it with `outlined.css`, adding `rounded.css` or `sharp.css` only where a `variant`
102
+ uses them. The bare import ships every style's font in the build (a browser fetches each one
103
+ only when a page renders it). This is where the scaffold already stands, and the style left
104
+ is still the whole variable font, which the options below shrink.
105
+ - **Google Fonts subset.** Replace the import with a stylesheet link in `index.html`:
106
+ `<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined&icon_names=home,palette,settings&display=block" />`.
107
+ `icon_names` takes the ligature names comma-separated and sorted alphabetically, and keep
108
+ `&display=block` (it stops the names flashing as text). The returned CSS defines the same
109
+ `material-symbols-outlined` class, so components don't change. Costs: the browser fetches
110
+ the icons from Google (privacy, no offline use), and the list must hold every name the app
111
+ renders, since a missing one shows as plain text. `variant="rounded"`/`"sharp"` need
112
+ `Material+Symbols+Rounded`/`Material+Symbols+Sharp` requested too, and an app that sets
113
+ `font-variation-settings` must request those axes in the URL.
114
+ - **Self-hosted subset.** Cut a subset from the package's `.woff2` with a font subsetting tool
115
+ (fontTools' subsetter, for one), keeping the glyphs for the names the app renders, and load
116
+ it with the app's own `@font-face` plus the `.material-symbols-outlined` rules. No request to
117
+ Google, same list upkeep.
118
+ - **Fewer variations.** `@material-symbols/font-400`, from the same project, keeps every
119
+ symbol with weight, grade and optical size fixed, at a fraction of the size. Import
120
+ `@material-symbols/font-400/outlined.css` in place of `material-symbols/outlined.css`; it
121
+ defines the same class, and there is no list to maintain.
122
+
123
+ Docs: https://bestax.io/docs/guides/getting-started/alternative-icons#making-the-font-smaller
124
+
92
125
  ## One glyph, five names
93
126
 
94
127
  | Glyph | fa | mdi | ion | material-icons / material-symbols |
@@ -105,7 +138,7 @@ Set it once on `ConfigProvider` and omit `library` everywhere else.
105
138
  2. **Name format for that library** — kebab vs snake_case (see the table above); for
106
139
  material-* a wrong name renders as literal text instead of a glyph.
107
140
  3. **The library's CSS/script is actually loaded** — the import in `main.tsx` (or the
108
- Ionicons scripts in `index.html`) must exist; bestax ships none of them.
141
+ Ionicons script in `index.html`) must exist; bestax ships none of them.
109
142
  4. **Variant availability** — Font Awesome free has no `light`/`duotone`/`thin`; brand
110
143
  glyphs need `variant="brands"`.
111
144
 
@@ -59,13 +59,59 @@ Centered; a collection of items → Card grid. For mixed requests, pick the domi
59
59
  `aria-controls`/`aria-labelledby` links come built in, so add no `onKeyDown` or ARIA
60
60
  of your own; `onChange` fires when a tab is activated, never on
61
61
  arrow-key focus), `Dropdown active/onActiveChange`,
62
+ `ToggleButton pressed/defaultPressed onPressedChange` for a button that stays on or off (not
63
+ `Button isActive`, which sets no `aria-pressed`; the label stays the same in both states; a
64
+ set goes in `Buttons hasAddons role="group"` with an `aria-label`, where it's also
65
+ `Buttons.ToggleButton`, and a row where only one can be on is `SegmentedControl`),
66
+ `SegmentedControl aria-label="…" value/defaultValue onChange` for a row of buttons where
67
+ only one can be on, such as a list or grid view (one `SegmentedControl.Segment value="…"` per
68
+ option; a radio group with the single tab stop and the arrow keys built in, so add no
69
+ `onKeyDown` of your own; an icon-only segment needs its own `aria-label`; the arrows check as
70
+ they move, so `onChange(value)` fires on each, and it can't be cleared; it carries no form
71
+ value, so a choice a form submits is `Radios`),
62
72
  `Popover trigger={<Button>…</Button>} open/defaultOpen onOpenChange` (`trigger` is one
63
73
  element, not a node: Popover clones it to add the click and ARIA, so never wrap it in a
64
74
  clickable of your own; build the panel from `Popover.Header`/`Body`/`Footer`/`Close`, and a
65
75
  panel without a `Popover.Header` needs `ariaLabel`),
76
+ `HoverCard trigger={<Link href="…">…</Link>}` for a preview on hover or keyboard focus
77
+ (`trigger` is one link or button, cloned the same way; the card's content goes straight in as
78
+ children with no sub-components; a trigger inside a `Paragraph` needs `appendToBody`; hover
79
+ and focus only unless `openOnTap` is set, which suits only a trigger with no tap action of its
80
+ own, such as a `Button` with no `onClick`; on touch a link trigger just follows the link, so
81
+ what the card shows must be on that page too, and a form is a `Popover`),
82
+ `ContextMenu aria-label="…" trigger={<Table.Tr>…</Table.Tr>}` for actions on a right-click
83
+ or a long press (`trigger` is one element, a row or a card, cloned rather than wrapped; the
84
+ items are `ContextMenu.Item`/`ContextMenu.Divider`, which are `Dropdown`'s; no `open` prop,
85
+ `onOpenChange` reports it; always put the same items in a visible `Dropdown` on the row too,
86
+ since a context menu is only a shortcut),
66
87
  `Steps value={i}/onStepClick items={[{label, icon?}]}` (child form is `Steps.Step`, not
67
88
  `Steps.Item`). `Reveal cascade` staggers only its **direct children** — to stagger a grid,
68
89
  put `<Reveal delay={i * 80}>` inside each `Cell`, not around the container.
90
+ - **An empty list, table or search with no results is `EmptyState`**, not a hand-built `Box`
91
+ of `Icon`, `Title` and `Buttons isCentered`:
92
+ `<EmptyState icon="folder-open" title="No projects yet" titleAs="h3" description="…">` with
93
+ its one `<Button>` as a child (two or more go in `EmptyState.Actions`). Its `title` is always a
94
+ heading, so set `titleAs` to the level under the list's own heading. It has no live region:
95
+ after a search or a filter, keep a `Paragraph` with `role="status"`, `aria-atomic="true"` and
96
+ `visibility="sr-only"` beside the search field the whole time, saying the count, and have
97
+ "Clear search" put focus back in the field. While data loads show a `Skeleton` or a `Loader`,
98
+ and a failed load is a `Message` or a `Notification`, never an EmptyState. The archetypes
99
+ reference has the full pattern under Card grid.
100
+ - **An order's history, a ticket's activity, a changelog or an activity feed is `Timeline`**,
101
+ not a hand-built list with a border down the side: `<Timeline titleAs="h3">` with a
102
+ `<Timeline.Item title="Shipped" time="14 October 2026" dateTime="2026-10-14">` per event, any
103
+ detail as its children, and the whole date in `time`. `titleAs` is the level under the
104
+ section's heading, or `"p"` in a busy feed. For something that moves along on its own, like a
105
+ parcel or a deployment, `active={i}` marks progress and `pending="Out for delivery"` ends it on
106
+ what's under way, oldest first. A journey the user moves through, like a wizard or a checkout,
107
+ stays `Steps`.
108
+ - **A file browser, a checkbox tree (permissions, filters) or an outline you browse is
109
+ `TreeView`**, with `<TreeView.Item value="…" label="…" icon="folder">` items nested inside each
110
+ other: `<TreeView aria-label="Files" selectionMode="single">` with `selected`/`onSelectedChange`
111
+ picks one, and `selectionMode="checkbox"` with `checked`/`onCheckedChange` draws boxes with
112
+ mixed parents, where a parent's value checks its whole branch. Map the data with a recursive
113
+ function, and render an item's children whether it's open or not. Its items aren't links, so
114
+ the app's sidebar and any list of pages stay on `Menu` with `Menu.Item href`.
69
115
  - Link lists (footer nav, sidebars): a bare `UnorderedList` of `ListItem`s is already
70
116
  marker-less and flush — Bulma's reset unstyles `ul` — so no prop or CSS is needed;
71
117
  bullets appear only inside `Content`.
@@ -160,7 +206,7 @@ background comes from the semantic `color` prop, not `bgColor`.
160
206
  | `textAlign: 'center'` | `textAlign="centered"` (also `left`, `right`, `justified`) |
161
207
  | `color: '#…'` | `textColor` with the nearest Bulma color: `primary`, `link`, `info`, `success`, `warning`, `danger`, `white`, `black`, `grey` (+ `grey-light`, `grey-dark`, …) |
162
208
  | `backgroundColor: '#…'` | `bgColor` (same palette) |
163
- | `fontSize: …` | `textSize="1"`…`"7"` (`1` largest) — for headings use `Title`/`SubTitle` `size` |
209
+ | `fontSize: …` | `textSize="1"`…`"7"` (`1` largest). On `Title`/`SubTitle`, `as` sets the level and `size` picks it too, so an `<h3>` at size 4 is `as="h3" textSize="4"` |
164
210
  | `fontWeight: …` | `textWeight`: `light`, `normal`, `medium`, `semibold`, `bold` |
165
211
  | `textTransform`, italics | `textTransform`: `uppercase`, `lowercase`, `capitalized`, `italic` |
166
212
  | `display: 'flex'` + flex properties | same-named props: `display="flex"`, `flexDirection`, `justifyContent`, `alignItems`, `flexWrap` |
@@ -168,9 +214,11 @@ background comes from the semantic `color` prop, not `bgColor`.
168
214
  | `display: 'none'` | `visibility="hidden"`, or responsive `display*` props (`displayMobile`, `displayTablet`, …) |
169
215
  | `gap: '1rem'` (flex or grid) | `gap="2"`, or one axis: `columnGap`, `rowGap`; gap scale: `1`=0.5rem, `2`=1rem, `4`=2rem, half steps (`"1.5"`) |
170
216
 
171
- No helper matches (e.g. `maxWidth`, a one-off gradient)? Add a named class to the project
172
- stylesheet (`src/App.css` in a scaffolded app) and pass it via `className` — still never
173
- inline `style`.
217
+ No helper matches (e.g. `maxWidth`, a one-off gradient)? Add a named class to the project's
218
+ global stylesheet and pass it via `className` — still never inline `style`. In a create-bestax
219
+ app that is `src/index.css`, which its entry file imports after bestax's stylesheet, so the
220
+ class wins over a Bulma rule of the same weight. The starter page's `src/App.css` loads after
221
+ bestax's too, but only while the starter's `App` imports it.
174
222
 
175
223
  ## References
176
224
 
@@ -200,6 +248,8 @@ inline `style`.
200
248
  - [ ] Do not use `Tile` — it is not shipped.
201
249
  - [ ] Action feedback goes through `Toast`, a confirmation through `Dialog`, a text-styled
202
250
  action through `LinkButton` — not `Notification`, `Modal` or a bare `<a>`.
251
+ - [ ] An empty list or a search with no results renders `EmptyState`, and a search or a filter
252
+ keeps a `role="status"` region beside its field that says the count.
203
253
  - [ ] Style with helper props, not inline `style` — translate via the mapping table; values
204
254
  with no helper get a named class in the stylesheet, never `style={{}}`. No raw Bulma
205
255
  `className`s either (`Span`/`Paragraph` wrap bare text; `Th`/`Td` take `textAlign`/`textWeight`).