create-bestax 4.2.16 → 4.2.17

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 (29) hide show
  1. package/dist/constants.js +1 -1
  2. package/package.json +6 -6
  3. package/templates/skills/bestax-custom-component/SKILL.md +36 -1
  4. package/templates/skills/bestax-custom-component/references/api.md +36 -14
  5. package/templates/skills/bestax-custom-component/references/component-catalog.md +8 -2
  6. package/templates/skills/bestax-custom-component/references/library-contributor.md +4 -1
  7. package/templates/skills/bestax-form/SKILL.md +41 -29
  8. package/templates/skills/bestax-form/references/api.md +12 -10
  9. package/templates/skills/bestax-form/references/patterns.md +4 -3
  10. package/templates/skills/bestax-icons/SKILL.md +8 -1
  11. package/templates/skills/bestax-icons/references/icon-libraries.md +35 -2
  12. package/templates/skills/bestax-layout-scaffold/SKILL.md +34 -1
  13. package/templates/skills/bestax-layout-scaffold/references/archetypes.md +57 -0
  14. package/templates/skills/bestax-migrate/references/bloomer/prop-map.md +34 -8
  15. package/templates/skills/bestax-migrate/references/bloomer/unmappables.md +18 -0
  16. package/templates/skills/bestax-migrate/references/rbx/prop-map.md +31 -7
  17. package/templates/skills/bestax-migrate/references/rbx/unmappables.md +41 -14
  18. package/templates/skills/bestax-migrate/references/react-bulma-components/component-map.md +3 -3
  19. package/templates/skills/bestax-migrate/references/react-bulma-components/prop-map.md +22 -0
  20. package/templates/skills/bestax-migrate/references/react-bulma-components/unmappables.md +33 -8
  21. package/templates/skills/bestax-optimize/SKILL.md +2 -0
  22. package/templates/skills/bestax-optimize/references/modular-build.md +19 -10
  23. package/templates/skills/bestax-theming/SKILL.md +4 -1
  24. package/templates/skills/bestax-theming/references/css-variables.md +87 -2
  25. package/templates/skills/bestax-theming/references/themeable-components.md +6 -2
  26. package/templates/vite-ts/package.json +1 -0
  27. package/templates/vite-ts/tsconfig.app.json +10 -9
  28. package/templates/vite-ts/tsconfig.node.json +15 -5
  29. package/templates/vite-ts/src/vite-env.d.ts +0 -1
package/dist/constants.js CHANGED
@@ -98,7 +98,7 @@ cases. Before writing \`style\`, translate each declaration with this table:
98
98
  | \`textAlign: 'center'\` | \`textAlign="centered"\` (also \`left\`, \`right\`, \`justified\`) |
99
99
  | \`color: '#…'\` | \`textColor\` with the nearest Bulma color: \`primary\`, \`link\`, \`info\`, \`success\`, \`warning\`, \`danger\`, \`white\`, \`black\`, \`grey\` (+ \`grey-light\`, \`grey-dark\`, …) |
100
100
  | \`backgroundColor: '#…'\` | \`bgColor\` (same palette) |
101
- | \`fontSize: …\` | \`textSize="1"\`…\`"7"\` (\`1\` largest) — for headings use \`Title\`/\`SubTitle\` \`size\` |
101
+ | \`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"\` |
102
102
  | \`fontWeight: …\` | \`textWeight\`: \`light\`, \`normal\`, \`medium\`, \`semibold\`, \`bold\` |
103
103
  | \`textTransform\`, italics | \`textTransform\`: \`uppercase\`, \`lowercase\`, \`capitalized\`, \`italic\` |
104
104
  | \`display: 'flex'\` + flex properties | same-named props: \`display="flex"\`, \`flexDirection\`, \`justifyContent\`, \`alignItems\`, \`flexWrap\` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-bestax",
3
- "version": "4.2.16",
3
+ "version": "4.2.17",
4
4
  "description": "Create a new bestax-bulma project",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,21 +41,21 @@
41
41
  "prompts": "^2.4.2",
42
42
  "react": "^18.0.0 || ^19.0.0",
43
43
  "react-dom": "^18.0.0 || ^19.0.0",
44
- "@allxsmith/bestax-bulma": "^5.27.5"
44
+ "@allxsmith/bestax-bulma": "^5.34.3"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@jest/globals": "^30.0.0",
48
- "@playwright/test": "^1.62.1",
48
+ "@playwright/test": "^1.63.0",
49
49
  "@types/fs-extra": "^11.0.4",
50
50
  "@types/jest": "^30.0.0",
51
- "@types/node": "^26.3.0",
51
+ "@types/node": "^26.6.4",
52
52
  "@types/prompts": "^2.4.9",
53
53
  "@types/wait-on": "^5.3.4",
54
54
  "jest": "^30.2.0",
55
55
  "semver": "^7.8.5",
56
- "ts-jest": "^29.4.12",
56
+ "ts-jest": "^29.4.14",
57
57
  "typescript": "^6.0.3",
58
- "wait-on": "^9.1.0"
58
+ "wait-on": "^9.5.1"
59
59
  },
60
60
  "publishConfig": {
61
61
  "access": "public"
@@ -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
+ 100 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,6 +100,7 @@ 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.
@@ -147,7 +153,7 @@ component is guaranteed to appear (the generator fails if one lacks an API page)
147
153
 
148
154
  - [ClientOnly](https://bestax.io/docs/api/helpers/clientonly) — `ClientOnly` renders its children only in the browser, after hydration, and a `fallback` until then…
149
155
  - [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…
156
+ - [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
157
  - [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
158
  - [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
159
  - [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
@@ -125,30 +125,41 @@ Full-width is `isFullwidth` on every component that supports it (`Button`, `Link
125
125
  spellings compile only where they historically existed: `isFullWidth` everywhere except
126
126
  `Sidebar`, `fullwidth` on `Tabs` only, `fullWidth` on `Sidebar` only.
127
127
 
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.
128
+ **Labels.** Default to the convenience `label` prop, with `horizontal` for a side-by-side row.
129
+ `Input`, `Select`, `TextArea`, `File`, `Numberinput`, `Slider`, `DateInput`, `TimeInput`,
130
+ `DateTimeInput`, `Autocomplete` and `Taginput` then render the `Field` and the label and wire
131
+ `htmlFor`/`id`, to your `id` when you pass one (`<Input label="Email" id="email" horizontal />`);
132
+ an explicit `labelProps={{ htmlFor }}` wins. The group inputs (`Checkboxes`, `Radios`, `Rate`,
133
+ `DateRangeInput`) name their `role="group"`/`"radiogroup"` wrapper through `aria-labelledby`
134
+ instead. Inside a `Field` a convenience input drops its `label`, and an `inline` date/time picker
135
+ or a `Taginput` at `maxTags` has no visible input to wire.
136
+
137
+ Composed, a labeled `Field` names the one control it holds: a composed `InputBase`/`SelectBase`/
138
+ `TextAreaBase`, a composed `DateInputBase`/`TimeInputBase`/`DateTimeInputBase` that is not
139
+ `inline`, or any input above takes its generated id, and a group (a composed `DateRangeInputBase`
140
+ too, `inline` or not) points `aria-labelledby` at the label's own id. **A control with an `id` of
141
+ its own never takes that id**, so `<Field label="Email"><Input id="email" /></Field>` renders a
142
+ label that names nothing: leave the `id` off, or pass the same value as
143
+ `labelProps={{ htmlFor: 'email' }}`. Wire it that way too where the association is skipped, in a
144
+ `grouped`/`hasAddons` row and in a horizontal `Field` whose body holds an inner `Field` (a nested
145
+ `Field` starts its own scope). For a group there, put `labelProps={{ id, htmlFor: undefined }}` on
146
+ the outer `Field` and a matching `aria-labelledby` on the group; the `htmlFor: undefined` keeps an
147
+ unmatched `for` out of server-rendered HTML.
148
+
149
+ A `range` `Slider` points the `for` at its low thumb and starts both thumbs' names with the label
150
+ ("Price range Minimum value"); its `ariaLabel={[low, high]}` replaces those names outright, so
151
+ leave it off when a label already names the Slider. Those thumbs and an `Autocomplete`'s or
152
+ `Taginput`'s suggestion list point `aria-labelledby` at the label's `id`, which a label wired by
153
+ hand has only when you pass one, `labelProps={{ htmlFor, id }}`; without it they keep their
154
+ fallback names ("Minimum value"/"Maximum value", "Suggestions"). A `<label htmlFor>` you put in
155
+ `Field.Label` yourself (`references/patterns.md`) names its control through the `for` alone, so
156
+ label a row holding either with the `Field`'s `label` prop. An `aria-label` or `aria-labelledby`
157
+ you give a group wins over any label, and a `Checkbox`, `Radio` or `Switch` takes nothing from a
158
+ `Field` on its own: its children name it, and a `Field`'s label names one only when you wire it as
159
+ you would any control with an `id` of its own, `labelProps={{ htmlFor }}` plus that control's
160
+ `id`. A labeled `Field` whose `for` nothing takes (a group, a `Checkbox`, `Radio` or `Switch`, an
161
+ input with an `id` of its own, your own markup) drops it once mounted, though server-rendered HTML
162
+ keeps it. `labelProps={{ htmlFor: undefined }}` opts out of the association entirely.
152
163
 
153
164
  ## Convenience vs composed
154
165
 
@@ -265,11 +276,12 @@ for the expected classes/states, and say plainly that the visual pass is still o
265
276
  ## Checklist
266
277
 
267
278
  - [ ] 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`).
279
+ - [ ] Every label is programmatically associated: the convenience `label` prop, the group
280
+ inputs, and `Field` + single-control composition do it on their own. A composed labeled
281
+ `Field` names a control with an `id` of its own, or one in an inner `Field`, only when its
282
+ `labelProps={{ htmlFor }}` matches that `id`, and a multi-control `Field`'s controls are
283
+ labeled individually (`aria-label`, `aria-labelledby`, or a `<label htmlFor>` matching
284
+ each control's `id`).
273
285
  - [ ] Controlled inputs have both `value` and `onChange` (or use `defaultValue` uncontrolled).
274
286
  - [ ] Error state shows via `color="danger"` + `message` + `messageColor="danger"`.
275
287
  - [ ] 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
 
@@ -164,9 +164,10 @@ Recipes built on addons, each written out in full in the docs:
164
164
  - **Split button** ([docs](https://bestax.io/docs/api/components/dropdown#split-button)): a
165
165
  `Button` and a `Dropdown` in `Field hasAddons`, one `Control` each. `Buttons hasAddons` does
166
166
  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.
167
+ - To label an addon row, put the label on an outer `Field` with `labelProps={{ htmlFor, id }}`
168
+ and a matching `id` on the input. The label's `id` names an `Autocomplete`'s or `Taginput`'s
169
+ suggestion list or a range `Slider`'s thumbs, which point `aria-labelledby` at it. A label
170
+ inside the `hasAddons` field sits in the row beside the controls.
170
171
 
171
172
  ## Horizontal field with explicit label/body
172
173
 
@@ -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,44 @@ 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.
69
100
  - Link lists (footer nav, sidebars): a bare `UnorderedList` of `ListItem`s is already
70
101
  marker-less and flush — Bulma's reset unstyles `ul` — so no prop or CSS is needed;
71
102
  bullets appear only inside `Content`.
@@ -160,7 +191,7 @@ background comes from the semantic `color` prop, not `bgColor`.
160
191
  | `textAlign: 'center'` | `textAlign="centered"` (also `left`, `right`, `justified`) |
161
192
  | `color: '#…'` | `textColor` with the nearest Bulma color: `primary`, `link`, `info`, `success`, `warning`, `danger`, `white`, `black`, `grey` (+ `grey-light`, `grey-dark`, …) |
162
193
  | `backgroundColor: '#…'` | `bgColor` (same palette) |
163
- | `fontSize: …` | `textSize="1"`…`"7"` (`1` largest) — for headings use `Title`/`SubTitle` `size` |
194
+ | `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
195
  | `fontWeight: …` | `textWeight`: `light`, `normal`, `medium`, `semibold`, `bold` |
165
196
  | `textTransform`, italics | `textTransform`: `uppercase`, `lowercase`, `capitalized`, `italic` |
166
197
  | `display: 'flex'` + flex properties | same-named props: `display="flex"`, `flexDirection`, `justifyContent`, `alignItems`, `flexWrap` |
@@ -200,6 +231,8 @@ inline `style`.
200
231
  - [ ] Do not use `Tile` — it is not shipped.
201
232
  - [ ] Action feedback goes through `Toast`, a confirmation through `Dialog`, a text-styled
202
233
  action through `LinkButton` — not `Notification`, `Modal` or a bare `<a>`.
234
+ - [ ] An empty list or a search with no results renders `EmptyState`, and a search or a filter
235
+ keeps a `role="status"` region beside its field that says the count.
203
236
  - [ ] Style with helper props, not inline `style` — translate via the mapping table; values
204
237
  with no helper get a named class in the stylesheet, never `style={{}}`. No raw Bulma
205
238
  `className`s either (`Span`/`Paragraph` wrap bare text; `Th`/`Td` take `textAlign`/`textWeight`).