bestax-mcp 1.16.2 → 1.16.4
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.
- package/data/bulma-classes.json +62 -18
- package/data/catalog.json +14 -5
- package/data/components/Image.json +1 -1
- package/data/components/Menu.json +99 -3
- package/data/components/TreeView.json +77 -3
- package/data/components/useFocusTrap.json +2 -2
- package/data/skills/bestax-layout-scaffold/SKILL.md +12 -2
- package/data/skills/bestax-layout-scaffold/references/layout-components.md +34 -4
- package/data/skills/bestax-migrate/references/bulma-classes/component-map.md +2 -1
- package/data/skills/bestax-migrate/references/bulma-classes/prop-map.md +17 -6
- package/data/skills/bestax-migrate/references/bulma-classes/unmappables.md +19 -4
- package/data/skills/bestax-optimize/references/modular-build.md +3 -2
- package/data/skills/bestax-theming/references/css-variables.md +11 -0
- package/data/skills.json +6 -6
- package/dist/bulma-classes.d.ts +6 -0
- package/dist/bulma-classes.d.ts.map +1 -1
- package/dist/bulma-classes.js +34 -7
- package/package.json +2 -2
package/data/bulma-classes.json
CHANGED
|
@@ -3615,25 +3615,69 @@
|
|
|
3615
3615
|
"is-64x64": { "writes": [{ "prop": "size", "value": "64x64" }] },
|
|
3616
3616
|
"is-96x96": { "writes": [{ "prop": "size", "value": "96x96" }] },
|
|
3617
3617
|
"is-128x128": { "writes": [{ "prop": "size", "value": "128x128" }] },
|
|
3618
|
-
"is-square": { "writes": [{ "prop": "size", "value": "square" }] }
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
"is-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
"is-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
"is-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
"is-
|
|
3618
|
+
"is-square": { "writes": [{ "prop": "size", "value": "square" }] },
|
|
3619
|
+
"is-1by1": {
|
|
3620
|
+
"writes": [{ "prop": "size", "value": "1by1" }],
|
|
3621
|
+
"rendersTag": "figure"
|
|
3622
|
+
},
|
|
3623
|
+
"is-5by4": {
|
|
3624
|
+
"writes": [{ "prop": "size", "value": "5by4" }],
|
|
3625
|
+
"rendersTag": "figure"
|
|
3626
|
+
},
|
|
3627
|
+
"is-4by3": {
|
|
3628
|
+
"writes": [{ "prop": "size", "value": "4by3" }],
|
|
3629
|
+
"rendersTag": "figure"
|
|
3630
|
+
},
|
|
3631
|
+
"is-3by2": {
|
|
3632
|
+
"writes": [{ "prop": "size", "value": "3by2" }],
|
|
3633
|
+
"rendersTag": "figure"
|
|
3634
|
+
},
|
|
3635
|
+
"is-5by3": {
|
|
3636
|
+
"writes": [{ "prop": "size", "value": "5by3" }],
|
|
3637
|
+
"rendersTag": "figure"
|
|
3638
|
+
},
|
|
3639
|
+
"is-16by9": {
|
|
3640
|
+
"writes": [{ "prop": "size", "value": "16by9" }],
|
|
3641
|
+
"rendersTag": "figure"
|
|
3642
|
+
},
|
|
3643
|
+
"is-2by1": {
|
|
3644
|
+
"writes": [{ "prop": "size", "value": "2by1" }],
|
|
3645
|
+
"rendersTag": "figure"
|
|
3646
|
+
},
|
|
3647
|
+
"is-3by1": {
|
|
3648
|
+
"writes": [{ "prop": "size", "value": "3by1" }],
|
|
3649
|
+
"rendersTag": "figure"
|
|
3650
|
+
},
|
|
3651
|
+
"is-4by5": {
|
|
3652
|
+
"writes": [{ "prop": "size", "value": "4by5" }],
|
|
3653
|
+
"rendersTag": "figure"
|
|
3654
|
+
},
|
|
3655
|
+
"is-3by4": {
|
|
3656
|
+
"writes": [{ "prop": "size", "value": "3by4" }],
|
|
3657
|
+
"rendersTag": "figure"
|
|
3658
|
+
},
|
|
3659
|
+
"is-2by3": {
|
|
3660
|
+
"writes": [{ "prop": "size", "value": "2by3" }],
|
|
3661
|
+
"rendersTag": "figure"
|
|
3662
|
+
},
|
|
3663
|
+
"is-3by5": {
|
|
3664
|
+
"writes": [{ "prop": "size", "value": "3by5" }],
|
|
3665
|
+
"rendersTag": "figure"
|
|
3666
|
+
},
|
|
3667
|
+
"is-9by16": {
|
|
3668
|
+
"writes": [{ "prop": "size", "value": "9by16" }],
|
|
3669
|
+
"rendersTag": "figure"
|
|
3670
|
+
},
|
|
3671
|
+
"is-1by2": {
|
|
3672
|
+
"writes": [{ "prop": "size", "value": "1by2" }],
|
|
3673
|
+
"rendersTag": "figure"
|
|
3674
|
+
},
|
|
3675
|
+
"is-1by3": {
|
|
3676
|
+
"writes": [{ "prop": "size", "value": "1by3" }],
|
|
3677
|
+
"rendersTag": "figure"
|
|
3678
|
+
}
|
|
3636
3679
|
},
|
|
3680
|
+
"omits": {},
|
|
3637
3681
|
"wrapsChildren": null,
|
|
3638
3682
|
"folds": null,
|
|
3639
3683
|
"absorbs": {
|
package/data/catalog.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"generatedFrom": {
|
|
4
4
|
"package": "@allxsmith/bestax-bulma",
|
|
5
|
-
"version": "5.
|
|
5
|
+
"version": "5.41.0"
|
|
6
6
|
},
|
|
7
7
|
"docsBase": "https://bestax.io/docs",
|
|
8
8
|
"categories": [
|
|
@@ -738,8 +738,8 @@
|
|
|
738
738
|
"slug": "components/menu",
|
|
739
739
|
"import": "import { Menu } from '@allxsmith/bestax-bulma';",
|
|
740
740
|
"compound": true,
|
|
741
|
-
"propCount":
|
|
742
|
-
"exampleCount":
|
|
741
|
+
"propCount": 22,
|
|
742
|
+
"exampleCount": 8
|
|
743
743
|
},
|
|
744
744
|
{
|
|
745
745
|
"name": "Message",
|
|
@@ -1200,8 +1200,8 @@
|
|
|
1200
1200
|
"slug": "components/treeview",
|
|
1201
1201
|
"import": "import { TreeView } from '@allxsmith/bestax-bulma';",
|
|
1202
1202
|
"compound": true,
|
|
1203
|
-
"propCount":
|
|
1204
|
-
"exampleCount":
|
|
1203
|
+
"propCount": 27,
|
|
1204
|
+
"exampleCount": 9
|
|
1205
1205
|
},
|
|
1206
1206
|
{
|
|
1207
1207
|
"name": "UnorderedList",
|
|
@@ -2080,6 +2080,10 @@
|
|
|
2080
2080
|
"--bulma-menu-list-link-padding": ["Menu"],
|
|
2081
2081
|
"--bulma-menu-nested-list-margin": ["Menu"],
|
|
2082
2082
|
"--bulma-menu-nested-list-padding-left": ["Menu"],
|
|
2083
|
+
"--bulma-menu-section-chevron-color": ["Menu"],
|
|
2084
|
+
"--bulma-menu-section-chevron-duration": ["Menu"],
|
|
2085
|
+
"--bulma-menu-section-chevron-size": ["Menu"],
|
|
2086
|
+
"--bulma-menu-section-gap": ["Menu"],
|
|
2083
2087
|
"--bulma-message-background-l": ["Message"],
|
|
2084
2088
|
"--bulma-message-body-border-width": ["Message"],
|
|
2085
2089
|
"--bulma-message-body-color": ["Message"],
|
|
@@ -2801,6 +2805,11 @@
|
|
|
2801
2805
|
"path": "Menu.List",
|
|
2802
2806
|
"purpose": "MenuList applies `menu-list` class only at the top level (not for nested lists)."
|
|
2803
2807
|
},
|
|
2808
|
+
"MenuSection": {
|
|
2809
|
+
"component": "Menu",
|
|
2810
|
+
"path": "Menu.Section",
|
|
2811
|
+
"purpose": "A section of the menu that opens and closes: its heading is a button drawn like `Menu.Label`, and its `Menu.List` of links shows while it's open."
|
|
2812
|
+
},
|
|
2804
2813
|
"NavbarBrand": {
|
|
2805
2814
|
"component": "Navbar",
|
|
2806
2815
|
"path": "Navbar.Brand",
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
"name": "size",
|
|
98
98
|
"type": "'16x16' | '24x24' | '32x32' | '48x48' | '64x64' | '96x96' | '128x128' | 'square' | '1by1' | '5by4' | '4by3' | '3by2' | '5by3' | '16by9' | '2by1' | '3by1' | '4by5' | '3by4' | '2by3' | '3by5' | '9by16' | '1by2' | '1by3'",
|
|
99
99
|
"default": null,
|
|
100
|
-
"description": "Fixed size or aspect ratio modifier for the image container.",
|
|
100
|
+
"description": "Fixed size or aspect ratio modifier for the image container. A ratio (`16by9` and the rest) renders only its `is-<ratio>` class there. Bulma stretches an `<img>` inside to fill the ratio, but any other child, such as an `iframe`, fills it only with `className=\"has-ratio\"` of its own.",
|
|
101
101
|
"required": false,
|
|
102
102
|
"inherited": false,
|
|
103
103
|
"deprecated": false
|
|
@@ -21,17 +21,25 @@
|
|
|
21
21
|
"title": "Deeply Nested Menu",
|
|
22
22
|
"code": "<Menu>\n <Menu.Label>Levels</Menu.Label>\n <Menu.List>\n <Menu.Item href=\"#level-1\">\n Level 1\n <Menu.List>\n <Menu.Item href=\"#level-2\">\n Level 2\n <Menu.List>\n <Menu.Item href=\"#level-3\">Level 3</Menu.Item>\n </Menu.List>\n </Menu.Item>\n </Menu.List>\n </Menu.Item>\n </Menu.List>\n</Menu>"
|
|
23
23
|
},
|
|
24
|
+
{
|
|
25
|
+
"title": "Sections that open and close",
|
|
26
|
+
"code": "<nav aria-label=\"Documentation\">\n <Menu>\n <Menu.Section label=\"Getting started\">\n <Menu.List>\n <Menu.Item href=\"#installation\">Installation</Menu.Item>\n <Menu.Item href=\"#quick-start\">Quick start</Menu.Item>\n </Menu.List>\n </Menu.Section>\n <Menu.Section label=\"Components\" defaultOpen>\n <Menu.List>\n <Menu.Item href=\"#button\">Button</Menu.Item>\n <Menu.Item href=\"#card\" active>\n Card\n </Menu.Item>\n <Menu.Item href=\"#modal\">Modal</Menu.Item>\n </Menu.List>\n </Menu.Section>\n <Menu.Section label=\"Form\">\n <Menu.List>\n <Menu.Item href=\"#input\">Input</Menu.Item>\n <Menu.Item href=\"#select\">Select</Menu.Item>\n </Menu.List>\n </Menu.Section>\n </Menu>\n</nav>"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"title": "Controlled sections",
|
|
30
|
+
"code": "function example() {\n const groups = {\n Components: ['Button', 'Card', 'Modal'],\n Form: ['Input', 'Select'],\n };\n const [open, setOpen] = React.useState(['Components']);\n\n return (\n <>\n <Buttons mb=\"3\">\n <Button size=\"small\" onClick={() => setOpen(Object.keys(groups))}>\n Open all\n </Button>\n <Button size=\"small\" onClick={() => setOpen([])}>\n Close all\n </Button>\n </Buttons>\n <Menu>\n {Object.entries(groups).map(([label, links]) => (\n <Menu.Section\n key={label}\n label={label}\n open={open.includes(label)}\n onOpenChange={isOpen =>\n setOpen(current =>\n isOpen ? [...current, label] : current.filter(l => l !== label)\n )\n }\n >\n <Menu.List>\n {links.map(link => (\n <Menu.Item key={link} href={`#${link.toLowerCase()}`}>\n {link}\n </Menu.Item>\n ))}\n </Menu.List>\n </Menu.Section>\n ))}\n </Menu>\n </>\n );\n}"
|
|
31
|
+
},
|
|
24
32
|
{
|
|
25
33
|
"title": "Forwarded ref",
|
|
26
34
|
"code": "function example() {\n const itemRef = React.useRef<HTMLAnchorElement>(null);\n\n return (\n <>\n <Button mb=\"3\" onClick={() => itemRef.current?.focus()}>\n Focus Customers\n </Button>\n <Menu>\n <Menu.Label>General</Menu.Label>\n <Menu.List>\n <Menu.Item href=\"#dashboard\">Dashboard</Menu.Item>\n <Menu.Item href=\"#customers\" ref={itemRef}>\n Customers\n </Menu.Item>\n </Menu.List>\n </Menu>\n </>\n );\n}"
|
|
27
35
|
},
|
|
28
36
|
{
|
|
29
37
|
"title": "Compound (dot-notation) usage",
|
|
30
|
-
"code": "<Menu>\n <Menu.Label>General</Menu.Label>\n <Menu.List>\n <Menu.Item active href=\"#dashboard\">\n Dashboard\n </Menu.Item>\n <Menu.Item href=\"#customers\">Customers</Menu.Item>\n </Menu.List>\n</Menu>"
|
|
38
|
+
"code": "<Menu>\n <Menu.Label>General</Menu.Label>\n <Menu.List>\n <Menu.Item active href=\"#dashboard\">\n Dashboard\n </Menu.Item>\n <Menu.Item href=\"#customers\">Customers</Menu.Item>\n </Menu.List>\n <Menu.Section label=\"Transactions\">\n <Menu.List>\n <Menu.Item href=\"#payments\">Payments</Menu.Item>\n <Menu.Item href=\"#transfers\">Transfers</Menu.Item>\n </Menu.List>\n </Menu.Section>\n</Menu>"
|
|
31
39
|
}
|
|
32
40
|
],
|
|
33
|
-
"accessibility": "- The root `Menu` renders as `<aside class=\"menu\">`. A menu that's navigation is better inside a\n `<nav>` with an `aria-label`, which gives assistive tech a navigation landmark to jump to.\n- Every `Menu.List` has `role=\"list\"`, nested ones included. That's the role a `<ul>` already has,\n set again for Safari, whose WebKit stops exposing a `<ul>` as a list once its markers are hidden,\n as Bulma's reset hides them, unless the role is written out.\n- `Menu.Item active` sets `aria-current=\"page\"` on the element `as` renders, the link by\n default, so the current item isn't only a color. An `aria-current` you pass wins: a menu of\n sections on one page can say `\"location\"`, and `aria-current={false}` turns it off.\n- Use semantic nesting (`Menu.List` inside `Menu.Item`) for submenus.\n- Give each item an `href`, a router link through `as`, or `as=\"button\"`, so the keyboard can\n reach it.\n- `Menu.Item as=\"button\"` defaults to `type=\"button\"`, so a menu item inside a form doesn't\n submit it. Pass `type=\"submit\"` or `type=\"reset\"` and yours is used instead; any other value\n renders `type=\"button\"`.\n- In forced colors (Windows' contrast themes), the browser replaces Bulma's fill and text color\n for the active item along with every other color, so bestax's stylesheet marks an\n `is-active` or `is-selected` item with a bar in the system's highlight color along its start\n edge there instead. The item's text, and an icon or a tag inside it, keep the colors your\n contrast theme gives them. That rule ships in `bestax.css` and `extras.css`, not in Bulma's\n own stylesheet.\n\n:::note\nFor router integrations, pass the correct `as` and `to`/`href` props to `Menu.Item`.\n:::",
|
|
34
|
-
"related": ["Panel", "Sidebar", "TreeView", "useBulmaClasses"],
|
|
41
|
+
"accessibility": "- The root `Menu` renders as `<aside class=\"menu\">`. A menu that's navigation is better inside a\n `<nav>` with an `aria-label`, which gives assistive tech a navigation landmark to jump to.\n- Every `Menu.List` has `role=\"list\"`, nested ones included. That's the role a `<ul>` already has,\n set again for Safari, whose WebKit stops exposing a `<ul>` as a list once its markers are hidden,\n as Bulma's reset hides them, unless the role is written out.\n- `Menu.Item active` sets `aria-current=\"page\"` on the element `as` renders, the link by\n default, so the current item isn't only a color. An `aria-current` you pass wins: a menu of\n sections on one page can say `\"location\"`, and `aria-current={false}` turns it off.\n- Use semantic nesting (`Menu.List` inside `Menu.Item`) for submenus.\n- Give each item an `href`, a router link through `as`, or `as=\"button\"`, so the keyboard can\n reach it.\n- `Menu.Item as=\"button\"` defaults to `type=\"button\"`, so a menu item inside a form doesn't\n submit it. Pass `type=\"submit\"` or `type=\"reset\"` and yours is used instead; any other value\n renders `type=\"button\"`.\n- In forced colors (Windows' contrast themes), the browser replaces Bulma's fill and text color\n for the active item along with every other color, so bestax's stylesheet marks an\n `is-active` or `is-selected` item with a bar in the system's highlight color along its start\n edge there instead. The item's text, and an icon or a tag inside it, keep the colors your\n contrast theme gives them. That rule ships in `bestax.css` and `extras.css`, not in Bulma's\n own stylesheet.\n\nFor `Menu.Section`, a section that opens and closes:\n\n- It follows APG's\n [Disclosure Navigation Menu example](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/examples/disclosure-navigation/).\n The heading is a `<button type=\"button\">` with `aria-expanded`, and `aria-controls` naming the\n wrapper around its list. Enter and Space open and close it, as they do any button, and it\n doesn't submit a form it sits in.\n- The links under it are ordinary links. Each is its own tab stop, and there's no roving focus or\n arrow-key handling, which is a tree's model rather than navigation's. They keep their `href`,\n so a link still opens in a new tab, and `active` still sets `aria-current=\"page\"`.\n- A closed section's list is `hidden` and `inert`, so it isn't drawn, Tab skips its links, and\n assistive tech and find-in-page don't reach them. `inert` keeps the links out of reach even if a\n stylesheet sets `display` on the list and shows it. The list stays in the page, so\n `aria-controls` always names an element that exists, and a section that starts closed is\n already closed in the server's HTML, before any script runs.\n- Open the section that holds the current page, with `defaultOpen` or `open`, so the link marked\n `aria-current` can be seen and reached.\n- When the app closes a section while focus is on one of its links, as a sidebar that keeps only\n the current route's section open does after a link is followed, focus moves to that section's\n heading. Without that, the browser drops focus to the page, and the next Tab starts from the\n top.\n- `label` is the button's content and its accessible name. Keep links and other buttons out of\n it.\n- In forced colors the chevron stays visible, since it's drawn as a border, and the focus ring\n takes the system's text color. In a right-to-left page the chevron sits at the left end of the\n heading and points left while closed. It turns with a short transition, which stops when the\n reader asks for reduced motion.\n\n:::note\nFor router integrations, pass the correct `as` and `to`/`href` props to `Menu.Item`.\n:::",
|
|
42
|
+
"related": ["Collapse", "Panel", "Sidebar", "TreeView", "useBulmaClasses"],
|
|
35
43
|
"storybook": "https://bestax.io/storybook/?path=/story/components-menu--basic",
|
|
36
44
|
"summary": "The `Menu` component provides Bulma's vertical navigation menu: a simple, accessible sidebar or section menu for your Bulma React UI.",
|
|
37
45
|
"import": "import { Menu } from '@allxsmith/bestax-bulma';",
|
|
@@ -241,9 +249,97 @@
|
|
|
241
249
|
],
|
|
242
250
|
"catchAll": "Remaining props of the element or component selected by as (default <a>) and Bulma helper props",
|
|
243
251
|
"types": []
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
"path": "Menu.Section",
|
|
255
|
+
"summary": "A section of the menu that opens and closes: its heading is a button drawn like `Menu.Label`, and its `Menu.List` of links shows while it's open. It's APG's disclosure navigation, for a sidebar of link groups: the links stay ordinary links, each its own tab stop, and a closed section's list is `hidden` and `inert`, so Tab and assistive tech skip it. It goes directly in `Menu`, holding a `Menu.List`. Closing it while focus is on one of its links moves focus to its heading.",
|
|
256
|
+
"component": null,
|
|
257
|
+
"props": [
|
|
258
|
+
{
|
|
259
|
+
"name": "className",
|
|
260
|
+
"type": "string",
|
|
261
|
+
"default": null,
|
|
262
|
+
"description": "Additional CSS classes for the wrapping `<div>`.",
|
|
263
|
+
"required": false,
|
|
264
|
+
"inherited": false,
|
|
265
|
+
"deprecated": false
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
"name": "label",
|
|
269
|
+
"type": "React.ReactNode",
|
|
270
|
+
"default": null,
|
|
271
|
+
"description": "**Required.** The section's heading, drawn like `Menu.Label` inside the button that opens and closes it. Keep links and buttons out of it, since it's inside one.",
|
|
272
|
+
"required": false,
|
|
273
|
+
"inherited": false,
|
|
274
|
+
"deprecated": false
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
"name": "children",
|
|
278
|
+
"type": "React.ReactNode",
|
|
279
|
+
"default": null,
|
|
280
|
+
"description": "The `Menu.List` of links the section shows while it's open. The section itself goes directly in `Menu`, beside its labels and lists: inside a `Menu.Item` it would render inside the item's link, and inside a `Menu.List` it wouldn't be a list item.",
|
|
281
|
+
"required": false,
|
|
282
|
+
"inherited": false,
|
|
283
|
+
"deprecated": false
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
"name": "open",
|
|
287
|
+
"type": "boolean",
|
|
288
|
+
"default": null,
|
|
289
|
+
"description": "Controlled open state. If provided, the section is controlled, and its button only reports through `onOpenChange`.",
|
|
290
|
+
"required": false,
|
|
291
|
+
"inherited": false,
|
|
292
|
+
"deprecated": false
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
"name": "defaultOpen",
|
|
296
|
+
"type": "boolean",
|
|
297
|
+
"default": "false",
|
|
298
|
+
"description": "Initial open state for uncontrolled usage. Open the section that holds the current page, so the reader sees where they are.",
|
|
299
|
+
"required": false,
|
|
300
|
+
"inherited": false,
|
|
301
|
+
"deprecated": false
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
"name": "onOpenChange",
|
|
305
|
+
"type": "(open: boolean) => void",
|
|
306
|
+
"default": null,
|
|
307
|
+
"description": "Called with the state the button asks for (`true` to open) each time it is clicked or activated with Enter or Space, in every mode. Pair it with `open` to let the button drive a controlled section.",
|
|
308
|
+
"required": false,
|
|
309
|
+
"inherited": false,
|
|
310
|
+
"deprecated": false
|
|
311
|
+
}
|
|
312
|
+
],
|
|
313
|
+
"extraProps": [],
|
|
314
|
+
"catchAll": "All standard <div> attributes and Bulma helper props",
|
|
315
|
+
"types": []
|
|
244
316
|
}
|
|
245
317
|
],
|
|
246
318
|
"cssVars": [
|
|
319
|
+
{
|
|
320
|
+
"css": "--bulma-menu-section-gap",
|
|
321
|
+
"sass": "$menu-section-gap",
|
|
322
|
+
"default": "0.375em",
|
|
323
|
+
"scope": "root"
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
"css": "--bulma-menu-section-chevron-size",
|
|
327
|
+
"sass": "$menu-section-chevron-size",
|
|
328
|
+
"default": "1em",
|
|
329
|
+
"scope": "root"
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
"css": "--bulma-menu-section-chevron-color",
|
|
333
|
+
"sass": "$menu-section-chevron-color",
|
|
334
|
+
"default": "currentColor",
|
|
335
|
+
"scope": "root"
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
"css": "--bulma-menu-section-chevron-duration",
|
|
339
|
+
"sass": "$menu-section-chevron-duration",
|
|
340
|
+
"default": "var(--bulma-duration)",
|
|
341
|
+
"scope": "root"
|
|
342
|
+
},
|
|
247
343
|
{
|
|
248
344
|
"css": "--bulma-menu-item-h",
|
|
249
345
|
"sass": "$menu-item-h",
|
|
@@ -23,7 +23,11 @@
|
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
"title": "Folders That Load",
|
|
26
|
-
"code": "function example() {\n interface Folder {\n id: string;\n name: string;\n hasFolders: boolean;\n }\n\n // A pretend server: each folder holds two more, three levels down.\n const listFolders = (id: string) =>\n new Promise<Folder[]>(resolve =>\n setTimeout(\n ()
|
|
26
|
+
"code": "function example() {\n interface Folder {\n id: string;\n name: string;\n hasFolders: boolean;\n }\n\n // A pretend server: each folder holds two more, three levels down.\n const listFolders = (id: string, fail: boolean) =>\n new Promise<Folder[]>((resolve, reject) =>\n setTimeout(() => {\n if (fail) {\n reject(new Error(`Couldn't reach ${id}`));\n return;\n }\n resolve(\n ['a', 'b'].map(name => ({\n id: `${id}/${name}`,\n name: `Folder ${name.toUpperCase()}`,\n hasFolders: id.split('/').length < 3,\n }))\n );\n }, 800)\n );\n\n const [expanded, setExpanded] = useState<string[]>([]);\n // A folder's children, or how loading them is going.\n const [found, setFound] = useState<\n Record<string, Folder[] | 'loading' | 'error'>\n >({});\n // Shared fails the first time, so there's a Retry to press.\n const [sharedFails, setSharedFails] = useState(true);\n\n function load(id: string) {\n const fail = id === 'shared' && sharedFails;\n if (fail) setSharedFails(false);\n setFound(current => ({ ...current, [id]: 'loading' }));\n listFolders(id, fail).then(\n folders => setFound(current => ({ ...current, [id]: folders })),\n () => setFound(current => ({ ...current, [id]: 'error' }))\n );\n }\n\n function changeOpen(next: string[]) {\n setExpanded(next);\n for (const id of next) {\n if (!found[id]) load(id);\n }\n }\n\n const items = (folders: Folder[]): React.ReactNode =>\n folders.map(folder => {\n const state = found[folder.id];\n return (\n <TreeView.Item\n key={folder.id}\n value={folder.id}\n label={folder.name}\n icon=\"folder\"\n hasChildren={folder.hasFolders}\n isLoading={state === 'loading'}\n loadError={\n state === 'error' ? `Couldn't load ${folder.name}` : undefined\n }\n onRetry={() => load(folder.id)}\n >\n {Array.isArray(state) && items(state)}\n </TreeView.Item>\n );\n });\n\n return (\n <TreeView\n aria-label=\"Folders\"\n expanded={expanded}\n onExpandedChange={changeOpen}\n >\n {items([\n { id: 'home', name: 'Home', hasFolders: true },\n { id: 'shared', name: 'Shared', hasFolders: true },\n ])}\n </TreeView>\n );\n}"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"title": "Folders That Load",
|
|
30
|
+
"code": "<TreeView\n aria-label=\"Dossiers\"\n loadingLabel=\"Chargement…\"\n retryLabel=\"Réessayer\"\n loadingAnnouncement={name => `Chargement de ${name}`}\n loadedAnnouncement={name => `${name} chargé`}\n>\n {/* … */}\n</TreeView>"
|
|
27
31
|
},
|
|
28
32
|
{
|
|
29
33
|
"title": "An Outline With Expand All and Collapse All",
|
|
@@ -38,8 +42,15 @@
|
|
|
38
42
|
"code": "<TreeView\n aria-label=\"Files\"\n selectionMode=\"single\"\n defaultSelected=\"src/App.tsx\"\n defaultExpanded={['src']}\n>\n <TreeView.Item value=\"src\" label=\"src\" icon=\"folder\">\n <TreeView.Item value=\"src/index.ts\" label=\"index.ts\" icon=\"file\" />\n <TreeView.Item value=\"src/App.tsx\" label=\"App.tsx\" icon=\"file\" />\n <TreeView.Item value=\"src/legacy.ts\" label=\"legacy.ts\" disabled />\n </TreeView.Item>\n <TreeView.Item value=\"package.json\" label=\"package.json\" icon=\"file\" />\n</TreeView>"
|
|
39
43
|
}
|
|
40
44
|
],
|
|
41
|
-
"accessibility": "`TreeView` follows the [APG Tree View Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/).\n\n- **Roles:** the root is a `<ul role=\"tree\">`, each item an `<li role=\"treeitem\">`, and an open\n parent's children sit in a `<ul role=\"group\">` inside it. A closed item's children have no\n markup.\n- **A name:** a tree needs one, from `aria-label` or `aria-labelledby`, and a development build\n warns when it has neither. Each item is named by `aria-labelledby` pointing at its own label, and\n declares its `aria-level`, which VoiceOver needs according to\n [GitHub's testing](https://github.blog/engineering/user-experience/considerations-for-making-a-tree-view-component-accessible/).\n `aria-expanded` is on parents only.\n- **One tab stop, with real focus:** one item holds `tabIndex={0}` and the rest `-1`, and focus\n moves from item to item, rather than through `aria-activedescendant`. When focus comes into the\n tree, it lands on the selected item, or in a checkbox tree on the first checked item on the page.\n Without one, it lands where focus last was, or on the first item. If the item holding the stop\n goes away, its nearest ancestor on the page takes it, and focus goes too if it had focus.\n- **Selection:** a `single` tree marks each enabled item `aria-selected`, `true` or `false`. A\n `checkbox` tree marks `aria-checked` on every item, `true`, `false` or `mixed`, disabled ones\n included, and adds `aria-multiselectable=\"true\"` to the tree. A tree never has both, and a tree\n with no `selectionMode` has neither. Selection doesn't follow focus.\n- **The box** is a [`Checkbox`](https://bestax.io/docs/api/form/checkbox) inside an `aria-hidden` span, with\n `tabIndex={-1}`, so it's neither a tab stop nor a second thing for a screen reader to read. The\n item's `aria-checked` is what a screen reader hears.\n- **Disabled items** are `aria-disabled` and stay reachable from the arrow keys, Home, End and\n typing, so a screen reader user finds them. They're never selected, checked, opened or closed.\n One that's already open stays open, so what's under it is still reachable.\n\nEnter and Space do the item's one thing, which depends on the mode:\n\n| Key | No `selectionMode` | `single` | `checkbox` |\n| ----- | ------------------------ | ---------------- | ------------------------ |\n| Enter | Opens or closes a parent | Selects the item | Opens or closes a parent |\n| Space | Opens or closes a parent | Selects the item | Checks or unchecks it |\n\
|
|
42
|
-
"related": [
|
|
45
|
+
"accessibility": "`TreeView` follows the [APG Tree View Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/).\n\n- **Roles:** the root is a `<ul role=\"tree\">`, each item an `<li role=\"treeitem\">`, and an open\n parent's children sit in a `<ul role=\"group\">` inside it. A closed item's children have no\n markup.\n- **A name:** a tree needs one, from `aria-label` or `aria-labelledby`, and a development build\n warns when it has neither. Each item is named by `aria-labelledby` pointing at its own label, and\n declares its `aria-level`, which VoiceOver needs according to\n [GitHub's testing](https://github.blog/engineering/user-experience/considerations-for-making-a-tree-view-component-accessible/).\n `aria-expanded` is on parents only.\n- **One tab stop, with real focus:** one item holds `tabIndex={0}` and the rest `-1`, and focus\n moves from item to item, rather than through `aria-activedescendant`. When focus comes into the\n tree, it lands on the selected item, or in a checkbox tree on the first checked item on the page.\n Without one, it lands where focus last was, or on the first item. If the item holding the stop\n goes away, its nearest ancestor on the page takes it, and focus goes too if it had focus.\n- **Selection:** a `single` tree marks each enabled item `aria-selected`, `true` or `false`. A\n `checkbox` tree marks `aria-checked` on every item, `true`, `false` or `mixed`, disabled ones\n included, and adds `aria-multiselectable=\"true\"` to the tree. A tree never has both, and a tree\n with no `selectionMode` has neither. Selection doesn't follow focus.\n- **The box** is a [`Checkbox`](https://bestax.io/docs/api/form/checkbox) inside an `aria-hidden` span, with\n `tabIndex={-1}`, so it's neither a tab stop nor a second thing for a screen reader to read. The\n item's `aria-checked` is what a screen reader hears.\n- **Disabled items** are `aria-disabled` and stay reachable from the arrow keys, Home, End and\n typing, so a screen reader user finds them. They're never selected, checked, opened or closed.\n One that's already open stays open, so what's under it is still reachable.\n- **A loading item's row** is a `treeitem` that says Loading…, marked `aria-disabled` and reached\n the way a disabled item is, so a screen reader user who moves into the folder hears what's\n happening. It has no value: it's never selected or checked, it has no `aria-selected` or\n `aria-checked`, and a checkbox tree doesn't count it. If it goes with focus on it, focus moves to\n the item it was loading for. Bulma's loader turns in its chevron's place, hidden from assistive\n tech.\n- **A failed load's row** is a `treeitem` named by its message and the Retry button's text, such\n as \"Couldn't load src Retry\". Enter, Space or a click on the row calls `onRetry`, in every mode.\n The button is drawn inside an `aria-hidden` span and takes no focus, the way an item's box is,\n so the tree keeps its one tab stop, and nothing in the message should be a link or a button\n either. A retry that starts loading keeps the same row, so focus stays on it. With no `onRetry`,\n the row is disabled and shows the message alone.\n- **Announcements:** a visually hidden `role=\"status\"` region with `aria-live=\"polite\"` sits right\n after the tree, one for the whole tree, from the moment it mounts, so a screen reader is already\n listening when something is written into it. It says \"Loading src\" when an open item starts\n loading, a failed load's message, and \"src loaded\" when the loading row or a failed load's row\n goes with the item still open.\n The name is the item's label as a screen reader reads it. A closed item, or one in a closed\n branch, says nothing. Each announcement is written a moment after the change and cleared soon\n after, so the page doesn't keep a second copy of the text.\n\nEnter and Space do the item's one thing, which depends on the mode:\n\n| Key | No `selectionMode` | `single` | `checkbox` |\n| ----- | ------------------------ | ---------------- | ------------------------ |\n| Enter | Opens or closes a parent | Selects the item | Opens or closes a parent |\n| Space | Opens or closes a parent | Selects the item | Checks or unchecks it |\n\nOn a failed load's row, both retry, whatever the mode. The rest work the same in every mode:\n\n| Key | What it does |\n| ---------- | ----------------------------------------------------------------------------------------- |\n| Down, Up | The next or previous item on the page, without wrapping |\n| Right | Opens a closed parent, and focus stays. On an open parent, moves to its first child |\n| Left | Closes an open parent. Otherwise moves to its parent |\n| Home, End | The first item, and the last one on the page |\n| `*` | Opens every closed parent beside the focused item, itself included. Focus stays |\n| Characters | Moves to the next item whose label starts with what's typed, several characters adding up |\n| Tab | Leaves the tree |\n\n- **Right to left:** Right and Left swap in a right-to-left page, and the chevron points to the\n left while closed.\n- **Keys it leaves alone:** anything with Control, Alt or Meta held, anything while an input method\n is composing, and Escape, so it reaches a Modal or Popover around the tree. An item's own\n `onKeyDown` runs first, and calling `preventDefault()` there keeps the tree from acting on the\n key.\n- **Clicks:** the chevron only opens and closes. The rest of the row does the item's one thing:\n opens or closes it with no `selectionMode`, selects it (and opens or closes a parent) in a\n `single` tree, and checks it in a `checkbox` tree. A click moves focus and the tab stop to the\n item.\n- **Not for site navigation:** APG's own\n [navigation tree example](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/examples/treeview-navigation/)\n says \"A pattern more suited for typical site navigation with expandable groups of links is the\n disclosure pattern\", and a tree's items aren't links. Keep a sidebar on [`Menu`](https://bestax.io/docs/api/components/menu),\n whose `Menu.Section` is that disclosure pattern.\n- **Focus and the selected look differ:** the focus ring sits outside the row, so it shows on the\n selected fill as well as on a plain row.\n- **Forced colors:** in Windows high contrast mode the selected row is the system's Highlight and\n HighlightText, a disabled row or a loading one is GrayText, and the focus ring, the chevrons and\n the guide lines stay visible, since they're outlines and borders. A mixed box keeps its dash, and\n the loader's ring stays open.\n- **Motion:** the chevron turns with a short transition, and the loader spins. Both stop when the\n reader asks for reduced motion, and the loader stays drawn.",
|
|
46
|
+
"related": [
|
|
47
|
+
"Checkbox",
|
|
48
|
+
"Collapse",
|
|
49
|
+
"Loader",
|
|
50
|
+
"Menu",
|
|
51
|
+
"Sidebar",
|
|
52
|
+
"useBulmaClasses"
|
|
53
|
+
],
|
|
43
54
|
"storybook": "https://bestax.io/storybook/?path=/story/components-treeview--default",
|
|
44
55
|
"summary": "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, and Right and Left to open and close. It's the APG Tree View pattern, drawn to look like Bulma's menu. It isn't for site navigation: items aren't links, and a sidebar whose sections open and close is `Menu`'s job. The root owns which items are open, through `expanded`, `defaultExpanded` and `onExpandedChange`. `selectionMode` picks one of three trees. With `none`, nothing is picked. With `single`, one item is, and `selected` says which. With `checkbox`, each item has a box, a check reaches every enabled item under it, closed branches included, and a parent with some of its items checked is mixed. `selected` is read in a `single` tree only, and `checked` in a `checkbox` tree only. Selection never follows focus. Disabled items stay reachable from the keys, so a screen reader finds them. The tree needs a name, from `aria-label` or `aria-labelledby`, and a development build warns when it has neither.",
|
|
45
56
|
"import": "import { TreeView } from '@allxsmith/bestax-bulma';",
|
|
@@ -159,6 +170,42 @@
|
|
|
159
170
|
"inherited": false,
|
|
160
171
|
"deprecated": false
|
|
161
172
|
},
|
|
173
|
+
{
|
|
174
|
+
"name": "loadingLabel",
|
|
175
|
+
"type": "string",
|
|
176
|
+
"default": "'Loading…'",
|
|
177
|
+
"description": "What the row in a loading item's group says. In your app's language.",
|
|
178
|
+
"required": false,
|
|
179
|
+
"inherited": false,
|
|
180
|
+
"deprecated": false
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"name": "retryLabel",
|
|
184
|
+
"type": "string",
|
|
185
|
+
"default": "'Retry'",
|
|
186
|
+
"description": "The Retry button's text on a failed load's row. In your app's language.",
|
|
187
|
+
"required": false,
|
|
188
|
+
"inherited": false,
|
|
189
|
+
"deprecated": false
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
"name": "loadingAnnouncement",
|
|
193
|
+
"type": "(name: string) => string",
|
|
194
|
+
"default": null,
|
|
195
|
+
"description": "What the live region says when an open item starts loading, built from the item's name. In your app's language. Default: `` `Loading ${name}` ``.",
|
|
196
|
+
"required": false,
|
|
197
|
+
"inherited": false,
|
|
198
|
+
"deprecated": false
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
"name": "loadedAnnouncement",
|
|
202
|
+
"type": "(name: string) => string",
|
|
203
|
+
"default": null,
|
|
204
|
+
"description": "What the live region says when that load finishes. In your app's language. Default: `` `${name} loaded` ``.",
|
|
205
|
+
"required": false,
|
|
206
|
+
"inherited": false,
|
|
207
|
+
"deprecated": false
|
|
208
|
+
},
|
|
162
209
|
{
|
|
163
210
|
"name": "className",
|
|
164
211
|
"type": "string",
|
|
@@ -251,6 +298,33 @@
|
|
|
251
298
|
"inherited": false,
|
|
252
299
|
"deprecated": false
|
|
253
300
|
},
|
|
301
|
+
{
|
|
302
|
+
"name": "isLoading",
|
|
303
|
+
"type": "boolean",
|
|
304
|
+
"default": "false",
|
|
305
|
+
"description": "Its children are on their way. While it's open, its group ends in a row that says so, and the tree's live region says it's loading, then that it's loaded once this goes back to `false`. A parent only: pair it with `hasChildren`, or with the children it already has.",
|
|
306
|
+
"required": false,
|
|
307
|
+
"inherited": false,
|
|
308
|
+
"deprecated": false
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
"name": "loadError",
|
|
312
|
+
"type": "React.ReactNode",
|
|
313
|
+
"default": null,
|
|
314
|
+
"description": "Its children didn't load. While it's open, its group ends in a row with this message, and a Retry button when there's an `onRetry`, and the live region reads the message out, so name the item in it (\"Couldn't load src\"), then says it's loaded once this goes with the item still open. A parent only, like `isLoading`. Nothing in it should be a link or a button: the row is what's pressed. `isLoading` wins while both are given.",
|
|
315
|
+
"required": false,
|
|
316
|
+
"inherited": false,
|
|
317
|
+
"deprecated": false
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
"name": "onRetry",
|
|
321
|
+
"type": "() => void",
|
|
322
|
+
"default": null,
|
|
323
|
+
"description": "Called by the Retry button on a failed load's row, and by Enter or Space there. Set `isLoading` while the retry runs, so the live region says it's loading, and reads the message out again if it fails again. The same message passed a second time with no loading between is no change the tree can see.",
|
|
324
|
+
"required": false,
|
|
325
|
+
"inherited": false,
|
|
326
|
+
"deprecated": false
|
|
327
|
+
},
|
|
254
328
|
{
|
|
255
329
|
"name": "disabled",
|
|
256
330
|
"type": "boolean",
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"storybook": "https://bestax.io/storybook/?path=/story/helpers-usefocustrap--default",
|
|
24
24
|
"summary": "`useFocusTrap` keeps keyboard focus inside a container while it is active: focus moves in when the trap turns on, Tab and Shift+Tab wrap between the first and…",
|
|
25
25
|
"import": "import { useFocusTrap } from '@allxsmith/bestax-bulma';",
|
|
26
|
-
"api": "```ts\nfunction useFocusTrap(\n containerRef: RefObject<HTMLElement | null>,\n options?: UseFocusTrapOptions\n): void;\n\ninterface UseFocusTrapOptions {\n active?: boolean;\n initialFocusRef?: RefObject<HTMLElement | null>;\n restoreFocus?: boolean | RefObject<HTMLElement | null>;\n}\n```\n\n### Parameters\n\n| Parameter | Type | Default | Description
|
|
27
|
-
"doc": "# useFocusTrap\n\n## Overview\n\n`useFocusTrap` keeps keyboard focus inside a container while it is active: focus moves in when the trap turns on, Tab and Shift+Tab wrap between the first and last tab stops, and focus goes back where it came from when the trap turns off.\n\nUse it for your own floating or modal content, such as a command palette or a custom overlay. `Modal`, `Dialog`, `Sidebar`, `Popover` and the date and time pickers already manage focus themselves, and a panel opened from a button is what [`Popover`](https://bestax.io/docs/api/components/popover) is for.\n\n---\n\n## Import\n\n```tsx\nimport { useFocusTrap } from '@allxsmith/bestax-bulma';\n```\n\n---\n\n## Usage\n\n### A panel opened from a button\n\nPass a ref to the container and turn the trap on with the panel. Give the container `tabIndex={-1}` so it can hold focus itself when nothing inside can. Open the panel below, then Tab past Cancel or Shift+Tab past the input: focus wraps, skipping the disabled button.\n\n```tsx live\nfunction example() {\n const [open, setOpen] = useState(false);\n const buttonRef = React.useRef<HTMLButtonElement>(null);\n const panelRef = React.useRef<HTMLDivElement>(null);\n useFocusTrap(panelRef, { active: open, restoreFocus: buttonRef });\n\n return (\n <>\n <Button\n ref={buttonRef}\n aria-haspopup=\"dialog\"\n aria-expanded={open}\n aria-controls=\"filters-panel\"\n onClick={() => setOpen(o => !o)}\n >\n Filters\n </Button>\n {open && (\n <div\n ref={panelRef}\n id=\"filters-panel\"\n role=\"dialog\"\n aria-label=\"Filters\"\n tabIndex={-1}\n onKeyDown={e => {\n if (e.key !== 'Escape') return;\n e.preventDefault();\n setOpen(false);\n }}\n >\n <Box mt=\"3\">\n <Input label=\"Name contains\" />\n <Buttons>\n <Button color=\"primary\" onClick={() => setOpen(false)}>\n Apply\n </Button>\n <Button disabled>Export</Button>\n <Button onClick={() => setOpen(false)}>Cancel</Button>\n </Buttons>\n </Box>\n </div>\n )}\n </>\n );\n}\n```\n\nThe trap doesn't close anything. Escape, a click outside and the buttons that dismiss the panel are the component's to handle, as the `onKeyDown` above does. Its `preventDefault()` marks the Escape used, so a [`Modal`](https://bestax.io/docs/api/components/modal) or [`Sidebar`](https://bestax.io/docs/api/components/sidebar) around the panel stays open for the next one.\n\n### Where focus starts\n\nOn its own the trap focuses the first tab stop inside the container, or the container when there is none. `initialFocusRef` picks a specific element inside the container instead, such as the search box of a command palette. The element has to be able to take focus, so a heading needs `tabIndex={-1}`. When focusing it doesn't put focus inside the container, because the ref is empty or its element is outside, disabled, hidden or can't take focus, the trap starts on the first tab stop as it would without the ref.\n\n```tsx\nconst searchRef = useRef<HTMLInputElement>(null);\nuseFocusTrap(paletteRef, { active: open, initialFocusRef: searchRef });\n```\n\n### Where focus goes back to\n\n`restoreFocus` decides where focus lands when the trap turns off, whether because `active` went `false`, the container left the page or the component unmounted.\n\n- `true` (the default) returns it to the element that had focus when the trap turned on.\n- A ref sends it to that element instead. Prefer this for a panel opened from a button: some browsers don't focus a button when it is clicked, and content that focuses itself as it mounts (an `autoFocus` input) takes focus before the trap can see where it came from.\n- `false` leaves focus alone.\n\nFocus only moves back while the trap still holds it. If a click has already put focus on something else outside the container, it stays there.\n\n### With portals\n\nA trapped panel is often rendered through a portal, at the end of `document.body`, and a portaled panel can mount a render after the trap turns on: once the page has hydrated, or once a target held in state exists. The trap waits for hydration and looks for its container again each time the component calling it renders, so it attaches to the panel once the panel is there.\n\nPut the ref on the element inside the [`Portal`](https://bestax.io/docs/api/helpers/portal), so the trap holds the portaled content itself. The panel below is declared inside a box that clips its overflow and portals into an element after it, as in the Portal page's [container example](https://bestax.io/docs/api/helpers/portal#into-a-container). Open it, then Tab past Cancel: focus wraps inside the panel, and Escape or either button closes it and returns focus to Share.\n\n```tsx live\nfunction example() {\n const [open, setOpen] = useState(false);\n const [target, setTarget] = useState<HTMLElement | null>(null);\n const buttonRef = React.useRef<HTMLButtonElement>(null);\n const panelRef = React.useRef<HTMLDivElement>(null);\n useFocusTrap(panelRef, { active: open, restoreFocus: buttonRef });\n\n return (\n <>\n <Box overflow=\"clipped\">\n <Button\n ref={buttonRef}\n aria-haspopup=\"dialog\"\n aria-expanded={open}\n aria-controls=\"share-panel\"\n onClick={() => setOpen(o => !o)}\n >\n Share\n </Button>\n {open && target && (\n <Portal container={target}>\n <div\n ref={panelRef}\n id=\"share-panel\"\n role=\"dialog\"\n aria-label=\"Share\"\n tabIndex={-1}\n onKeyDown={e => {\n if (e.key !== 'Escape') return;\n e.preventDefault();\n setOpen(false);\n }}\n >\n <Box mt=\"3\">\n <Input label=\"Email\" type=\"email\" />\n <Buttons>\n <Button color=\"primary\" onClick={() => setOpen(false)}>\n Send\n </Button>\n <Button onClick={() => setOpen(false)}>Cancel</Button>\n </Buttons>\n </Box>\n </div>\n </Portal>\n )}\n </Box>\n <div ref={setTarget} />\n </>\n );\n}\n```\n\nThe other way round doesn't work: a portal rendered _inside_ the trapped container puts its content somewhere else in the DOM, outside the container, where Tab from the trap can't reach it and the trap doesn't hold focus. Render a nested overlay inside the container, or give it a trap of its own.\n\n### Which elements are tab stops\n\nThe trap wraps at the first and last elements the browser's Tab would visit, taken in document order:\n\n- links with an `href`, buttons, inputs other than `type=\"hidden\"`, selects and text areas\n- the areas with an `href` of an image map whose image is shown, taken where the `<map>` sits\n- frames: `<iframe>`, `<embed>` and `<object>`\n- `<audio>` and `<video>` with `controls`\n- the summary of a `<details>`, which stays a stop while the details is closed\n- an editable region (`contentEditable`), counted once rather than once per editable element inside it\n- anything with a `tabIndex` of `0` or more\n- the content of an open shadow root inside the container, such as a web component's own controls, with anything slotted into it taken where its slot is\n\nWithin those, what the browser's Tab skips is skipped here too, so none of it can become an end of the trap and let Tab walk out of the container:\n\n- a negative `tabIndex`, such as the unfocused items of a roving-tabindex grid\n- disabled controls, including those inside a disabled `<fieldset>`\n- anything not rendered: `hidden`, `display: none` on the element or an ancestor (for slotted content, an ancestor of its slot counts), `visibility: hidden`, the content of a closed `<details>`, and a child of an open shadow root's host that no slot takes\n- anything inside an `inert` subtree\n- all but one button of a radio group: the checked one, or the first when none is checked\n- every button of a radio group whose checked button is outside the container, because a group is every button that shares its name, form owner and tree (the page, or one shadow root), not only the ones in the container\n\n### When Tab leaves anyway\n\nA browser can leave out a stop the page has, such as links when its settings say to. When a Tab the trap let through still takes focus out of the container, the trap sends it back to the other end. If the container is the last thing on the page, there is nothing outside for focus to land on and the browser can take it to its own toolbar instead, so avoid ending a trap with a link.\n\nOnly Tab is watched. A click or a script that moves focus out of the container is left alone, and the next Tab starts from wherever focus went. Pulling focus back from a click is modal behaviour, and a modal also has to block the page behind it; [`Modal`](https://bestax.io/docs/api/components/modal) does both. For a non-modal panel, close it when focus or a click goes elsewhere.\n\n### What the trap can't see into\n\nSome elements move focus among parts of their own, and the trap sees them as one stop:\n\n- A frame's content is its own document. Once focus is inside, Tab moves through the frame without the trap seeing it, and Tab from the frame's last stop goes on to whatever follows the frame. At the end of the trap, that is outside it: follow a frame with another stop.\n- A native date or time input has a stop for each field. At the edge of the trap, Tab wraps instead of visiting the later fields. Keep it away from the edges, or follow it with another stop.\n- A closed shadow root can't be looked into. The trap sees its host and the host's own children, but not the stops inside the root or which of those children a slot takes. A child that no slot takes isn't rendered and can't take focus, so if it ends up as the trap's first or last stop, turning the trap on or wrapping Tab to it leaves focus where it was. Keep a closed-root host away from the trap's edges, or give it no children that a slot doesn't take.\n- A scrollable region with nothing focusable inside is a stop in some browsers and not in others. Give it `tabIndex={0}` so it is one everywhere, which also lets keyboard users scroll it.\n\n### What the trap takes on trust\n\n- A browser can visit an image map's areas where the `<map>` sits or where its image is. The trap takes them where the map is, so keep the map right after its image, where the two agree.\n- A positive `tabIndex` changes the order the browser's Tab visits, but not the trap's ends, which stay the first and last stops in document order. Where the two disagree, a Tab past the browser's end is let through and then sent to the trap's end instead, which can be the very element it left, so focus seems stuck. Keep positive `tabIndex` out of a trap.\n- The trap listens for Tab on the container. Content inside that stops a Tab keydown from propagating keeps it from the trap, so that Tab isn't wrapped, and focus it takes out isn't sent back.\n\n### Nested traps\n\nThe trap listens for Tab on its container, so a trap inside another one handles Tab first and the outer trap stands aside. A panel that opens a second panel inside it keeps focus in the inner one.\n\n---\n\n## API\n\n```ts\nfunction useFocusTrap(\n containerRef: RefObject<HTMLElement | null>,\n options?: UseFocusTrapOptions\n): void;\n\ninterface UseFocusTrapOptions {\n active?: boolean;\n initialFocusRef?: RefObject<HTMLElement | null>;\n restoreFocus?: boolean | RefObject<HTMLElement | null>;\n}\n```\n\n### Parameters\n\n| Parameter | Type | Default | Description |\n| ------------------------- | --------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `containerRef` | `RefObject<HTMLElement \\| null>` | — | The element focus stays inside. |\n| `options.active` | `boolean` | `true` | Whether the trap is on. Turning it on moves focus in; turning it off (or unmounting) releases Tab and restores focus. |\n| `options.initialFocusRef` | `RefObject<HTMLElement \\| null>` | — | An element inside the container that can take focus, focused when the trap turns on; otherwise the first tab stop, then the container. |\n| `options.restoreFocus` | `boolean` \\| `RefObject<HTMLElement \\| null>` | `true` | Where focus goes when the trap turns off: `true` for the element focused when it turned on, a ref for that element, `false` to leave focus alone. |\n\nIt returns nothing. On the server it does nothing, and the container renders as written.\n\n---\n\n## Accessibility\n\n- Trap focus in content that asks for the reader's attention until it is dismissed: a dialog, a command palette, a panel with its own controls. Content a reader should be able to Tab past, such as a tooltip or a disclosure, shouldn't be trapped.\n- Pair the trap with a way out. The trap handles Tab only, so wire Escape (and any close button) to turn it off.\n- Give the container a role and an accessible name (`role=\"dialog\"` with `aria-label` or `aria-labelledby`), and give the button that opens it `aria-haspopup`, `aria-expanded` and `aria-controls`.\n- Return focus to the element that opened the content when it closes. The default does, and a ref to the trigger makes it reliable.\n- The trap is for the keyboard. It doesn't hide the rest of the page from screen readers or pointers, and it doesn't pull focus back from a click; for fully modal content, use [`Modal`](https://bestax.io/docs/api/components/modal), which sets `aria-modal`.\n\n---\n\n## Related Components\n\n- [`Modal`](https://bestax.io/docs/api/components/modal): A modal overlay with its own focus management.\n- [`Dialog`](https://bestax.io/docs/api/components/dialog): Ready-made confirm and alert dialogs.\n\n---\n\n## Additional Resources\n\n- [Storybook: useFocusTrap Stories](https://bestax.io/storybook/?path=/story/helpers-usefocustrap--default)\n- [WAI-ARIA Authoring Practices: Dialog (Modal) pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/)",
|
|
26
|
+
"api": "```ts\nfunction useFocusTrap(\n containerRef: RefObject<HTMLElement | null>,\n options?: UseFocusTrapOptions\n): void;\n\ninterface UseFocusTrapOptions {\n active?: boolean;\n initialFocusRef?: RefObject<HTMLElement | null>;\n restoreFocus?: boolean | RefObject<HTMLElement | null>;\n}\n```\n\n### Parameters\n\n| Parameter | Type | Default | Description |\n| ------------------------- | --------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `containerRef` | `RefObject<HTMLElement \\| null>` | — | The element focus stays inside. |\n| `options.active` | `boolean` | `true` | Whether the trap is on. Turning it on moves focus in; turning it off (or unmounting) releases Tab and restores focus. |\n| `options.initialFocusRef` | `RefObject<HTMLElement \\| null>` | — | An element inside the container that can take focus, focused when the trap turns on; otherwise the first tab stop that takes focus, then the container. |\n| `options.restoreFocus` | `boolean` \\| `RefObject<HTMLElement \\| null>` | `true` | Where focus goes when the trap turns off: `true` for the element focused when it turned on, a ref for that element, `false` to leave focus alone. |\n\nIt returns nothing. On the server it does nothing, and the container renders as written.",
|
|
27
|
+
"doc": "# useFocusTrap\n\n## Overview\n\n`useFocusTrap` keeps keyboard focus inside a container while it is active: focus moves in when the trap turns on, Tab and Shift+Tab wrap between the first and last tab stops, and focus goes back where it came from when the trap turns off.\n\nUse it for your own floating or modal content, such as a command palette or a custom overlay. `Modal`, `Dialog`, `Sidebar`, `Popover` and the date and time pickers already manage focus themselves, and a panel opened from a button is what [`Popover`](https://bestax.io/docs/api/components/popover) is for.\n\n---\n\n## Import\n\n```tsx\nimport { useFocusTrap } from '@allxsmith/bestax-bulma';\n```\n\n---\n\n## Usage\n\n### A panel opened from a button\n\nPass a ref to the container and turn the trap on with the panel. Give the container `tabIndex={-1}` so it can hold focus itself when nothing inside can. Open the panel below, then Tab past Cancel or Shift+Tab past the input: focus wraps, skipping the disabled button.\n\n```tsx live\nfunction example() {\n const [open, setOpen] = useState(false);\n const buttonRef = React.useRef<HTMLButtonElement>(null);\n const panelRef = React.useRef<HTMLDivElement>(null);\n useFocusTrap(panelRef, { active: open, restoreFocus: buttonRef });\n\n return (\n <>\n <Button\n ref={buttonRef}\n aria-haspopup=\"dialog\"\n aria-expanded={open}\n aria-controls=\"filters-panel\"\n onClick={() => setOpen(o => !o)}\n >\n Filters\n </Button>\n {open && (\n <div\n ref={panelRef}\n id=\"filters-panel\"\n role=\"dialog\"\n aria-label=\"Filters\"\n tabIndex={-1}\n onKeyDown={e => {\n if (e.key !== 'Escape') return;\n e.preventDefault();\n setOpen(false);\n }}\n >\n <Box mt=\"3\">\n <Input label=\"Name contains\" />\n <Buttons>\n <Button color=\"primary\" onClick={() => setOpen(false)}>\n Apply\n </Button>\n <Button disabled>Export</Button>\n <Button onClick={() => setOpen(false)}>Cancel</Button>\n </Buttons>\n </Box>\n </div>\n )}\n </>\n );\n}\n```\n\nThe trap doesn't close anything. Escape, a click outside and the buttons that dismiss the panel are the component's to handle, as the `onKeyDown` above does. Its `preventDefault()` marks the Escape used, so a [`Modal`](https://bestax.io/docs/api/components/modal) or [`Sidebar`](https://bestax.io/docs/api/components/sidebar) around the panel stays open for the next one.\n\n### Where focus starts\n\nOn its own the trap focuses the first tab stop inside the container that takes focus, or the container when none does. `initialFocusRef` picks a specific element inside the container instead, such as the search box of a command palette. The element has to be able to take focus, so a heading needs `tabIndex={-1}`. When focusing it doesn't put focus inside the container, because the ref is empty or its element is outside, disabled, hidden or can't take focus, the trap starts on the first tab stop as it would without the ref.\n\n```tsx\nconst searchRef = useRef<HTMLInputElement>(null);\nuseFocusTrap(paletteRef, { active: open, initialFocusRef: searchRef });\n```\n\n### Where focus goes back to\n\n`restoreFocus` decides where focus lands when the trap turns off, whether because `active` went `false`, the container left the page or the component unmounted.\n\n- `true` (the default) returns it to the element that had focus when the trap turned on.\n- A ref sends it to that element instead. Prefer this for a panel opened from a button: some browsers don't focus a button when it is clicked, and content that focuses itself as it mounts (an `autoFocus` input) takes focus before the trap can see where it came from.\n- `false` leaves focus alone.\n\nFocus only moves back while the trap still holds it. If a click has already put focus on something else outside the container, it stays there.\n\n### With portals\n\nA trapped panel is often rendered through a portal, at the end of `document.body`, and a portaled panel can mount a render after the trap turns on: once the page has hydrated, or once a target held in state exists. The trap waits for hydration and looks for its container again each time the component calling it renders, so it attaches to the panel once the panel is there.\n\nPut the ref on the element inside the [`Portal`](https://bestax.io/docs/api/helpers/portal), so the trap holds the portaled content itself. The panel below is declared inside a box that clips its overflow and portals into an element after it, as in the Portal page's [container example](https://bestax.io/docs/api/helpers/portal#into-a-container). Open it, then Tab past Cancel: focus wraps inside the panel, and Escape or either button closes it and returns focus to Share.\n\n```tsx live\nfunction example() {\n const [open, setOpen] = useState(false);\n const [target, setTarget] = useState<HTMLElement | null>(null);\n const buttonRef = React.useRef<HTMLButtonElement>(null);\n const panelRef = React.useRef<HTMLDivElement>(null);\n useFocusTrap(panelRef, { active: open, restoreFocus: buttonRef });\n\n return (\n <>\n <Box overflow=\"clipped\">\n <Button\n ref={buttonRef}\n aria-haspopup=\"dialog\"\n aria-expanded={open}\n aria-controls=\"share-panel\"\n onClick={() => setOpen(o => !o)}\n >\n Share\n </Button>\n {open && target && (\n <Portal container={target}>\n <div\n ref={panelRef}\n id=\"share-panel\"\n role=\"dialog\"\n aria-label=\"Share\"\n tabIndex={-1}\n onKeyDown={e => {\n if (e.key !== 'Escape') return;\n e.preventDefault();\n setOpen(false);\n }}\n >\n <Box mt=\"3\">\n <Input label=\"Email\" type=\"email\" />\n <Buttons>\n <Button color=\"primary\" onClick={() => setOpen(false)}>\n Send\n </Button>\n <Button onClick={() => setOpen(false)}>Cancel</Button>\n </Buttons>\n </Box>\n </div>\n </Portal>\n )}\n </Box>\n <div ref={setTarget} />\n </>\n );\n}\n```\n\nThe other way round doesn't work: a portal rendered _inside_ the trapped container puts its content somewhere else in the DOM, outside the container, where Tab from the trap can't reach it and the trap doesn't hold focus. Render a nested overlay inside the container, or give it a trap of its own.\n\n### Which elements are tab stops\n\nThe trap wraps at the first and last elements the browser's Tab would visit, taken in document order:\n\n- links with an `href`, buttons, inputs other than `type=\"hidden\"`, selects and text areas\n- the areas with an `href` of an image map whose image is shown, taken where the `<map>` sits\n- frames: `<iframe>`, `<embed>` and `<object>`\n- `<audio>` and `<video>` with `controls`\n- the summary of a `<details>`, which stays a stop while the details is closed\n- an editable region (`contentEditable`), counted once rather than once per editable element inside it\n- anything with a `tabIndex` of `0` or more\n- the content of an open shadow root inside the container, such as a web component's own controls, with anything slotted into it taken where its slot is\n\nWithin those, what the browser's Tab skips is skipped here too, so none of it can become an end of the trap and let Tab walk out of the container:\n\n- a negative `tabIndex`, such as the unfocused items of a roving-tabindex grid\n- disabled controls, including those inside a disabled `<fieldset>`\n- anything not rendered: `hidden`, `display: none` on the element or an ancestor (for slotted content, an ancestor of its slot counts), `visibility: hidden`, the content of a closed `<details>`, and a child of an open shadow root's host that no slot takes\n- anything inside an `inert` subtree\n- all but one button of a radio group: the checked one, or the first when none is checked\n- every button of a radio group whose checked button is outside the container, because a group is every button that shares its name, form owner and tree (the page, or one shadow root), not only the ones in the container\n\n### When Tab leaves anyway\n\nA browser can leave out a stop the page has, such as links when its settings say to. When a Tab the trap let through still takes focus out of the container, the trap sends it back to the other end. If the container is the last thing on the page, there is nothing outside for focus to land on and the browser can take it to its own toolbar instead, so avoid ending a trap with a link.\n\nA browser can also refuse focus to a stop. An `<object>` or `<embed>` with nothing to show is the common one, such as an `<object>` whose file failed to load, and which of them a browser refuses differs between browsers and with what has loaded, so the trap doesn't guess. Whenever it moves focus, as it turns on, when Tab wraps, and when it sends a Tab back in, it passes over a stop that doesn't take focus for the next one, and falls back to the container when none does.\n\nA refused stop at an end of the trap is the one case that still gets out. The trap can't tell it will be refused, so a Tab toward it is left to the browser, which goes past it and out of the container the way it goes past a skipped link. Focus comes back only when there's something outside for it to land on, so don't end a trap that sits last on the page with an `<object>` or `<embed>` that may have nothing to show, any more than with a link.\n\nOnly Tab is watched. A click or a script that moves focus out of the container is left alone, and the next Tab starts from wherever focus went. Pulling focus back from a click is modal behaviour, and a modal also has to block the page behind it; [`Modal`](https://bestax.io/docs/api/components/modal) does both. For a non-modal panel, close it when focus or a click goes elsewhere.\n\n### What the trap can't see into\n\nSome elements move focus among parts of their own, and the trap sees them as one stop:\n\n- A frame's content is its own document. Once focus is inside, Tab moves through the frame without the trap seeing it, and Tab from the frame's last stop goes on to whatever follows the frame. At the end of the trap, that is outside it: follow a frame with another stop.\n- A native date or time input has a stop for each field. At the edge of the trap, Tab wraps instead of visiting the later fields. Keep it away from the edges, or follow it with another stop.\n- A closed shadow root can't be looked into. The trap sees its host and the host's own children, but not the stops inside the root or which of those children a slot takes. A child that no slot takes isn't rendered and can't take focus, so the trap passes over it when it moves focus, as it does any stop the browser refuses. At an end of the trap it's a refused stop like any other, so keep a closed-root host away from the trap's edges, or give it no children that a slot doesn't take.\n- A scrollable region with nothing focusable inside is a stop in some browsers and not in others. Give it `tabIndex={0}` so it is one everywhere, which also lets keyboard users scroll it.\n\n### What the trap takes on trust\n\n- A browser can visit an image map's areas where the `<map>` sits or where its image is. The trap takes them where the map is, so keep the map right after its image, where the two agree.\n- A positive `tabIndex` changes the order the browser's Tab visits, but not the trap's ends, which stay the first and last stops in document order. Where the two disagree, a Tab past the browser's end is let through and then sent to the trap's end instead, which can be the very element it left, so focus seems stuck. Keep positive `tabIndex` out of a trap.\n- The trap listens for Tab on the container. Content inside that stops a Tab keydown from propagating keeps it from the trap, so that Tab isn't wrapped, and focus it takes out isn't sent back.\n\n### Nested traps\n\nThe trap listens for Tab on its container, so a trap inside another one handles Tab first and the outer trap stands aside. A panel that opens a second panel inside it keeps focus in the inner one.\n\n---\n\n## API\n\n```ts\nfunction useFocusTrap(\n containerRef: RefObject<HTMLElement | null>,\n options?: UseFocusTrapOptions\n): void;\n\ninterface UseFocusTrapOptions {\n active?: boolean;\n initialFocusRef?: RefObject<HTMLElement | null>;\n restoreFocus?: boolean | RefObject<HTMLElement | null>;\n}\n```\n\n### Parameters\n\n| Parameter | Type | Default | Description |\n| ------------------------- | --------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `containerRef` | `RefObject<HTMLElement \\| null>` | — | The element focus stays inside. |\n| `options.active` | `boolean` | `true` | Whether the trap is on. Turning it on moves focus in; turning it off (or unmounting) releases Tab and restores focus. |\n| `options.initialFocusRef` | `RefObject<HTMLElement \\| null>` | — | An element inside the container that can take focus, focused when the trap turns on; otherwise the first tab stop that takes focus, then the container. |\n| `options.restoreFocus` | `boolean` \\| `RefObject<HTMLElement \\| null>` | `true` | Where focus goes when the trap turns off: `true` for the element focused when it turned on, a ref for that element, `false` to leave focus alone. |\n\nIt returns nothing. On the server it does nothing, and the container renders as written.\n\n---\n\n## Accessibility\n\n- Trap focus in content that asks for the reader's attention until it is dismissed: a dialog, a command palette, a panel with its own controls. Content a reader should be able to Tab past, such as a tooltip or a disclosure, shouldn't be trapped.\n- Pair the trap with a way out. The trap handles Tab only, so wire Escape (and any close button) to turn it off.\n- Give the container a role and an accessible name (`role=\"dialog\"` with `aria-label` or `aria-labelledby`), and give the button that opens it `aria-haspopup`, `aria-expanded` and `aria-controls`.\n- Return focus to the element that opened the content when it closes. The default does, and a ref to the trigger makes it reliable.\n- The trap is for the keyboard. It doesn't hide the rest of the page from screen readers or pointers, and it doesn't pull focus back from a click; for fully modal content, use [`Modal`](https://bestax.io/docs/api/components/modal), which sets `aria-modal`.\n\n---\n\n## Related Components\n\n- [`Modal`](https://bestax.io/docs/api/components/modal): A modal overlay with its own focus management.\n- [`Dialog`](https://bestax.io/docs/api/components/dialog): Ready-made confirm and alert dialogs.\n\n---\n\n## Additional Resources\n\n- [Storybook: useFocusTrap Stories](https://bestax.io/storybook/?path=/story/helpers-usefocustrap--default)\n- [WAI-ARIA Authoring Practices: Dialog (Modal) pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/)",
|
|
28
28
|
"parts": [],
|
|
29
29
|
"cssVars": []
|
|
30
30
|
}
|
|
@@ -110,8 +110,18 @@ Centered; a collection of items → Card grid. For mixed requests, pick the domi
|
|
|
110
110
|
other: `<TreeView aria-label="Files" selectionMode="single">` with `selected`/`onSelectedChange`
|
|
111
111
|
picks one, and `selectionMode="checkbox"` with `checked`/`onCheckedChange` draws boxes with
|
|
112
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.
|
|
114
|
-
|
|
113
|
+
function, and render an item's children whether it's open or not. A folder that loads when it
|
|
114
|
+
opens takes `hasChildren`, `isLoading` while its children are on their way, and
|
|
115
|
+
`loadError="Couldn't load src"` with `onRetry` when the load fails, setting `isLoading` again
|
|
116
|
+
while the retry runs: the tree draws the Loading… and Retry rows and announces them, so don't
|
|
117
|
+
hand-write a placeholder item. Its items aren't
|
|
118
|
+
links, so the app's sidebar and any list of pages stay on `Menu` with `Menu.Item href`.
|
|
119
|
+
- **A sidebar whose groups of links open and close is `Menu.Section`**, one per group, directly
|
|
120
|
+
in `Menu` (never inside a `Menu.Item` or `Menu.List`):
|
|
121
|
+
`<Menu.Section label="Components" defaultOpen>` around that group's `Menu.List`, with
|
|
122
|
+
`defaultOpen` on the group holding the current page (or `open`/`onOpenChange` to hold the state).
|
|
123
|
+
Its heading is a real button with `aria-expanded`, and its links stay links, each a tab stop.
|
|
124
|
+
Not `TreeView`, and not a `Collapse` inside a hand-written `<li>`.
|
|
115
125
|
- Link lists (footer nav, sidebars): a bare `UnorderedList` of `ListItem`s is already
|
|
116
126
|
marker-less and flush — Bulma's reset unstyles `ul` — so no prop or CSS is needed;
|
|
117
127
|
bullets appear only inside `Content`.
|
|
@@ -267,11 +267,13 @@ https://bestax.io/docs/guides/features/routing.
|
|
|
267
267
|
|
|
268
268
|
## Menu
|
|
269
269
|
|
|
270
|
-
`<Menu>` is a vertical sidebar menu. Subcomponents: `Menu.Label`, `Menu.List`, `Menu.Item
|
|
270
|
+
`<Menu>` is a vertical sidebar menu. Subcomponents: `Menu.Label`, `Menu.List`, `Menu.Item`,
|
|
271
|
+
`Menu.Section`.
|
|
271
272
|
|
|
272
|
-
| Prop
|
|
273
|
-
|
|
|
274
|
-
| `Menu.Item`
|
|
273
|
+
| Prop | Type |
|
|
274
|
+
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
275
|
+
| `Menu.Item` | `active?` (the current page: `is-active` plus `aria-current="page"`, so don't add your own), `href?`, `as?` (custom link component); nest a `Menu.List` inside an item for submenus |
|
|
276
|
+
| `Menu.Section` | `label` (the heading, a `ReactNode`), `defaultOpen?`, or `open?` with `onOpenChange?(open)` to hold the state yourself, as `Collapse` does; its child is the group's `Menu.List`, and a closed section's links are out of reach |
|
|
275
277
|
|
|
276
278
|
```tsx
|
|
277
279
|
<Menu>
|
|
@@ -285,6 +287,34 @@ https://bestax.io/docs/guides/features/routing.
|
|
|
285
287
|
</Menu>
|
|
286
288
|
```
|
|
287
289
|
|
|
290
|
+
A sidebar whose groups of links open and close (a docs sidebar, an admin app's sections) puts each
|
|
291
|
+
group's `Menu.List` in a `Menu.Section`. Its heading is a real button drawn like `Menu.Label`, with
|
|
292
|
+
`aria-expanded` and a chevron, and its links stay ordinary links, each a tab stop. Open the
|
|
293
|
+
section that holds the current page. A section goes directly in `Menu`, never inside a `Menu.Item`
|
|
294
|
+
(it would render inside the item's link) or a `Menu.List` (it isn't a list item). Don't build this
|
|
295
|
+
from `TreeView` (its items aren't links) or from a `Collapse` in a hand-written `<li>` (its trigger
|
|
296
|
+
is a `div` with `role="button"`).
|
|
297
|
+
|
|
298
|
+
```tsx
|
|
299
|
+
<nav aria-label="Documentation">
|
|
300
|
+
<Menu>
|
|
301
|
+
<Menu.Section label="Getting started">
|
|
302
|
+
<Menu.List>
|
|
303
|
+
<Menu.Item href="/docs/install">Installation</Menu.Item>
|
|
304
|
+
</Menu.List>
|
|
305
|
+
</Menu.Section>
|
|
306
|
+
<Menu.Section label="Components" defaultOpen>
|
|
307
|
+
<Menu.List>
|
|
308
|
+
<Menu.Item href="/docs/button" active>
|
|
309
|
+
Button
|
|
310
|
+
</Menu.Item>
|
|
311
|
+
<Menu.Item href="/docs/card">Card</Menu.Item>
|
|
312
|
+
</Menu.List>
|
|
313
|
+
</Menu.Section>
|
|
314
|
+
</Menu>
|
|
315
|
+
</nav>
|
|
316
|
+
```
|
|
317
|
+
|
|
288
318
|
## Card
|
|
289
319
|
|
|
290
320
|
`<Card>` for catalog/grid items. Use the props form (`header`, `image`, `footer`) or the compound
|
|
@@ -272,7 +272,8 @@ go straight inside `Breadcrumb`, and `<figure className="image is-64x64"><img sr
|
|
|
272
272
|
one, so the `<ul>` carries nothing and the `.breadcrumb` needs an `aria-label` of its own.
|
|
273
273
|
- `Image` renders its `<img>` from `src` and `alt`, so the `<img>` carries nothing else, and
|
|
274
274
|
`is-rounded` on it becomes `isRounded`. The `.image` keeps its own attributes. A ratio
|
|
275
|
-
(`is-4by3`)
|
|
275
|
+
(`is-4by3`) becomes `size` too. `Image` renders a ratio on a `<figure>`, so a `<figure>` needs no
|
|
276
|
+
`as` there and a `<div>` keeps `as="div"`. Around anything else (an
|
|
276
277
|
`<img>` with more attributes, an `<iframe>`), `Image` renders its children as given, so the
|
|
277
278
|
`.image` converts around them and they stay as written, as long as they're HTML elements written
|
|
278
279
|
out. An empty `.image` stays markup, since `Image` would render an `<img>` of its own, and so does
|
|
@@ -103,7 +103,8 @@ renders the same `<h2>`.
|
|
|
103
103
|
| the `<select>` inside `.select` | `is-hovered`, `is-focused` | `isHovered`, `isFocused` |
|
|
104
104
|
| `.hero` → `Hero` | `is-primary`, `is-link`, `is-info`, `is-success`, `is-warning`, `is-danger`, `is-black`, `is-white`, `is-light`, `is-dark` | `color` |
|
|
105
105
|
| `.hero` → `Hero` | `is-small`, `is-medium`, `is-large`, `is-fullheight`, `is-fullheight-with-navbar` | `size` |
|
|
106
|
-
| `.image` → `Image` | `is-16x16`, `is-24x24`, `is-32x32`, `is-48x48`, `is-64x64`, `is-96x96`, `is-128x128`, `is-square` | `size`
|
|
106
|
+
| `.image` → `Image` | `is-16x16`, `is-24x24`, `is-32x32`, `is-48x48`, `is-64x64`, `is-96x96`, `is-128x128`, `is-square` | `size` |
|
|
107
|
+
| `.image` → `Image` | `is-{ratio}`, for each of the Ratios under Helper classes (`is-16by9`), but not `is-square` (see below) | `size`, on the `<figure>` it renders with no `as` (see below) |
|
|
107
108
|
| the `<img>` inside `.image` | `is-rounded` | `isRounded` |
|
|
108
109
|
| `.icon` → `Icon` | `is-small`, `is-medium`, `is-large` | `size` |
|
|
109
110
|
| `.tabs` → `Tabs` | `is-centered`, `is-right`, `is-left` | `align` |
|
|
@@ -130,6 +131,16 @@ renders the same `<h2>`.
|
|
|
130
131
|
| `.tag` → `Tag` | `is-light`, `is-rounded`, `is-hoverable` | `isLight`, `isRounded`, `isHoverable` |
|
|
131
132
|
| `.tag` → `Tag` | `is-medium`, `is-large` | `size` |
|
|
132
133
|
|
|
134
|
+
`is-square` sits with the fixed sizes, not the ratios. Bulma sizes it like `is-1by1`, but `Image`
|
|
135
|
+
renders `size="square"` on a `<div>`, as it does a fixed size. So
|
|
136
|
+
`<figure className="image is-square">` becomes `<Image as="figure" size="square">`, while
|
|
137
|
+
`<figure className="image is-1by1">` becomes `<Image size="1by1">`.
|
|
138
|
+
|
|
139
|
+
A ratio converts to identical markup only on a bestax-bulma whose `Image` writes nothing but the
|
|
140
|
+
ratio class on its container. An older one also writes `has-ratio` there, which Bulma reads only
|
|
141
|
+
on a child, so the page looks the same, unless that `.image` sits inside another ratio `.image`,
|
|
142
|
+
which then stretches it to fill. Upgrade bestax-bulma, or keep that ratio as a class.
|
|
143
|
+
|
|
133
144
|
`Progress` takes `value` and `max` as numbers: `value="40"` becomes `value={40}`. An
|
|
134
145
|
expression (`value={percent}`) carries over as written, so if it holds a string the migrated
|
|
135
146
|
file fails to typecheck while still rendering the same; wrap it in `Number(…)`. `Delete`
|
|
@@ -232,8 +243,8 @@ Some classes stay put because of how bestax renders them:
|
|
|
232
243
|
## Classes that stay classes
|
|
233
244
|
|
|
234
245
|
Color shades (`has-text-primary-65`), the `-touch` and `-only` breakpoints of text size and
|
|
235
|
-
alignment, Grid and `.image` modifiers away from their own element,
|
|
236
|
-
`
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
246
|
+
alignment, Grid and `.image` modifiers away from their own element, v1's `marginless` and
|
|
247
|
+
`paddingless` (the rules of `m-0` and `p-0` under other names), and the Bulma helpers bestax has
|
|
248
|
+
no prop for (`is-display-*`, `is-visibility-*`, `is-float-*`, `is-clear-*`, …) stay in
|
|
249
|
+
`className`. They render exactly as before. An `.image` size or ratio away from its `.image` also
|
|
250
|
+
gets a `modifier:image` TODO, since Bulma styles it only there.
|
|
@@ -400,7 +400,8 @@ Run on an HTML page (`bestax-migrate bulma-classes page.html`, or a folder with
|
|
|
400
400
|
codemod writes a module beside the page and runs everything above on it, so a page gets the same
|
|
401
401
|
TODOs its markup would as JSX. What a component can't carry gets an `html-*` rule. TODOs about the
|
|
402
402
|
whole page sit above the component, the rest above its `return`, and each ends with the page and
|
|
403
|
-
the line it came from (`(index.html:42)`)
|
|
403
|
+
the line it came from (`(index.html:42)`), or every line, for a TODO several elements share
|
|
404
|
+
(`(index.html:42, 57)`). The page itself stays as it was, so the code a TODO
|
|
404
405
|
points at is still there to read. A rule names only a tag, an attribute or a fixed word, never
|
|
405
406
|
anything the page says.
|
|
406
407
|
|
|
@@ -414,6 +415,11 @@ A component renders into the `<body>` of the app's own document, so nothing in t
|
|
|
414
415
|
it into the head; React 18 leaves it in the body, so use `document.title` in an effect there.
|
|
415
416
|
- **`html-head:meta`**: `charset` and `viewport` go once in the app's `index.html`. A page's own
|
|
416
417
|
(a description, Open Graph tags) go where the app sets each page's head.
|
|
418
|
+
- **`html-head:csp`**: a `<meta http-equiv="Content-Security-Policy">`. Its policy was written for
|
|
419
|
+
what the page loads, and copied into `index.html` as it is, it can block what the app loads
|
|
420
|
+
instead: a Vite dev server runs an inline script for React's fast refresh and opens a socket for
|
|
421
|
+
hot reloading, and the build serves the app's own scripts, styles and fonts. Write the app's own
|
|
422
|
+
policy, best sent as a response header, and try it under both the dev server and the build.
|
|
417
423
|
- **`html-head:link`**: icons, preconnects and preloads are app-wide, so keep them in
|
|
418
424
|
`index.html`; a `canonical` is per page.
|
|
419
425
|
- **`html-head:style`**: the `<style>` block was dropped. Move its rules into the app's stylesheet.
|
|
@@ -586,7 +592,9 @@ read for the burger, a carousel and a theme toggle too, and its TODO names the f
|
|
|
586
592
|
app loads paints the right one, and the toggle's state starts from what it wrote. The
|
|
587
593
|
bestax-theming skill covers `Theme` and `colorMode`, and its `examples/dark-mode.tsx` has the
|
|
588
594
|
script and the app's side.
|
|
589
|
-
- **`html-script:json-ld`** is structured data for search engines.
|
|
595
|
+
- **`html-script:json-ld`** is structured data for search engines. Data about the whole site, or
|
|
596
|
+
about a single-page app's one page, goes in the app's `index.html` as it is, where a crawler that
|
|
597
|
+
runs no JavaScript still reads it. Data that differs from page to page goes where the app sets
|
|
590
598
|
each page's head, as
|
|
591
599
|
`<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }} />`.
|
|
592
600
|
- **`html-script`**: any other inline script, or a `<script type>` that isn't code (a template,
|
|
@@ -604,8 +612,9 @@ The app's document has its own `<html>` and `<body>`, which a component can't re
|
|
|
604
612
|
bestax's `Navbar` adds none there, whatever its `fixed` says.
|
|
605
613
|
- **`html-document:data-theme`**, **`html-document:theme-dark`**, **`html-document:theme-light`**:
|
|
606
614
|
the page's color scheme. Put it on the `<html>` in `index.html`, where the first paint has it.
|
|
607
|
-
|
|
608
|
-
|
|
615
|
+
For a scheme the visitor switches, `Theme colorMode` writes the choice only after that paint,
|
|
616
|
+
so write a saved one before it too, as `html-script:theme` says and the bestax-theming skill's
|
|
617
|
+
`examples/dark-mode.tsx` does.
|
|
609
618
|
- **`html-document:lang`**, **`html-document:dir`**: set them on the `<html>` in `index.html`.
|
|
610
619
|
- **`html-document:class`**: the page's own classes on `<html>` or `<body>`. Put them on the
|
|
611
620
|
app's own `<body>` (or `<html>`), or on an element around the page's content if the styles
|
|
@@ -656,6 +665,12 @@ than `this`. A `<body onload>` becomes an effect.
|
|
|
656
665
|
- **`html-attr:name`**: an attribute JSX can't write (`@click`, `:class`, `x-on:click`), or one the
|
|
657
666
|
element already had under another spelling, was left out. A framework's directive is code to port
|
|
658
667
|
into the component.
|
|
668
|
+
- **`html-attr:stray`**: a stray character in a tag, with no letter or digit in it (`\` in
|
|
669
|
+
`<figure class="image" \>`, a typo for `/>`), which the browser read as the name of an empty
|
|
670
|
+
attribute. It was left out, so there is nothing to port; delete it from the page too.
|
|
671
|
+
- **`html-attr:ignored`**: an attribute HTML gives the element no meaning for, so the browser
|
|
672
|
+
ignored it, and React's types reject it there: `type` on a `<textarea>`. It was left out, so the
|
|
673
|
+
module typechecks. Nothing reads it, unless a stylesheet or script of the page's selects on it.
|
|
659
674
|
- **`html-attr:unknown`**: an attribute React has no prop for by that name, written as it was.
|
|
660
675
|
React renders it. Whether a TypeScript build takes it depends on the element (React's types
|
|
661
676
|
list `valign` on a `<td>` and not on a `<th>`), so the TODO doesn't say; what to do depends on
|
|
@@ -107,8 +107,9 @@ Notes:
|
|
|
107
107
|
@use '@allxsmith/bestax-bulma/scss/components/tabs' as bestax-tabs;
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
- The `menu` partial **extends** stock Bulma menu (it
|
|
111
|
-
colors, Windows' contrast themes), so an app using
|
|
110
|
+
- The `menu` partial **extends** stock Bulma menu (it draws `Menu.Section`'s heading and chevron,
|
|
111
|
+
and keeps the active item marked in forced colors, Windows' contrast themes), so an app using
|
|
112
|
+
`Menu` needs both, in either order:
|
|
112
113
|
|
|
113
114
|
```scss
|
|
114
115
|
@use 'bulma/sass/components/menu';
|
|
@@ -462,6 +462,17 @@ Declared on the compound `.button.link-button` — see the compound-selector exc
|
|
|
462
462
|
`--bulma-loading-overlay-opacity-opaque`, `--bulma-loading-text-color`,
|
|
463
463
|
`--bulma-loading-text-size`
|
|
464
464
|
|
|
465
|
+
### Menu
|
|
466
|
+
|
|
467
|
+
`Menu.Section`'s, declared on the root, `.menu`, where Bulma declares its own `--bulma-menu-*`:
|
|
468
|
+
the room between a section heading's words and its chevron, and the chevron's box, color and
|
|
469
|
+
turn. The heading itself reads Bulma's label variables (`--bulma-menu-label-color`,
|
|
470
|
+
`--bulma-menu-label-font-size`, `--bulma-menu-label-letter-spacing`,
|
|
471
|
+
`--bulma-menu-label-spacing`), so retheming the menu's labels restyles section headings too.
|
|
472
|
+
|
|
473
|
+
`--bulma-menu-section-chevron-color`, `--bulma-menu-section-chevron-duration`,
|
|
474
|
+
`--bulma-menu-section-chevron-size`, `--bulma-menu-section-gap`
|
|
475
|
+
|
|
465
476
|
### Notification
|
|
466
477
|
|
|
467
478
|
`--bulma-notification-delete-padding-inline-end` is the end padding of a notification with a close
|
package/data/skills.json
CHANGED
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
{
|
|
65
65
|
"id": "layout-components",
|
|
66
66
|
"file": "references/layout-components.md",
|
|
67
|
-
"bytes":
|
|
67
|
+
"bytes": 18956
|
|
68
68
|
}
|
|
69
69
|
],
|
|
70
70
|
"examples": [
|
|
@@ -103,17 +103,17 @@
|
|
|
103
103
|
{
|
|
104
104
|
"id": "bulma-classes-component-map",
|
|
105
105
|
"file": "references/bulma-classes/component-map.md",
|
|
106
|
-
"bytes":
|
|
106
|
+
"bytes": 29733
|
|
107
107
|
},
|
|
108
108
|
{
|
|
109
109
|
"id": "bulma-classes-prop-map",
|
|
110
110
|
"file": "references/bulma-classes/prop-map.md",
|
|
111
|
-
"bytes":
|
|
111
|
+
"bytes": 37225
|
|
112
112
|
},
|
|
113
113
|
{
|
|
114
114
|
"id": "bulma-classes-unmappables",
|
|
115
115
|
"file": "references/bulma-classes/unmappables.md",
|
|
116
|
-
"bytes":
|
|
116
|
+
"bytes": 50861
|
|
117
117
|
},
|
|
118
118
|
{
|
|
119
119
|
"id": "css-migration",
|
|
@@ -162,7 +162,7 @@
|
|
|
162
162
|
{
|
|
163
163
|
"id": "modular-build",
|
|
164
164
|
"file": "references/modular-build.md",
|
|
165
|
-
"bytes":
|
|
165
|
+
"bytes": 12075
|
|
166
166
|
}
|
|
167
167
|
],
|
|
168
168
|
"examples": []
|
|
@@ -176,7 +176,7 @@
|
|
|
176
176
|
{
|
|
177
177
|
"id": "css-variables",
|
|
178
178
|
"file": "references/css-variables.md",
|
|
179
|
-
"bytes":
|
|
179
|
+
"bytes": 45565
|
|
180
180
|
},
|
|
181
181
|
{
|
|
182
182
|
"id": "themeable-components",
|
package/dist/bulma-classes.d.ts
CHANGED
|
@@ -34,6 +34,12 @@ export interface Modifier {
|
|
|
34
34
|
name: string;
|
|
35
35
|
value: string;
|
|
36
36
|
};
|
|
37
|
+
/**
|
|
38
|
+
* The tag the component renders with this class's prop and no `as`, in
|
|
39
|
+
* place of the root's own (`Image` puts a ratio on a <figure>). Only one
|
|
40
|
+
* such class converts on an element.
|
|
41
|
+
*/
|
|
42
|
+
rendersTag?: string;
|
|
37
43
|
}
|
|
38
44
|
export interface RootRecord {
|
|
39
45
|
status: 'mapped' | 'todo' | 'plain' | 'fold';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bulma-classes.d.ts","sourceRoot":"","sources":["../src/bulma-classes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAGhD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,uCAAuC;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,yEAAyE;IACzE,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB;;;OAGG;IACH,SAAS,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"bulma-classes.d.ts","sourceRoot":"","sources":["../src/bulma-classes.ts"],"names":[],"mappings":"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"}
|
package/dist/bulma-classes.js
CHANGED
|
@@ -112,9 +112,16 @@ function listTags(tags) {
|
|
|
112
112
|
? `${named.slice(0, -1).join(', ')} or ${named[named.length - 1]}`
|
|
113
113
|
: named[0];
|
|
114
114
|
}
|
|
115
|
-
function reaches(entry) {
|
|
115
|
+
function reaches(entry, retagged) {
|
|
116
|
+
// The tag it renders with no `as`: the one a class picks, else its own.
|
|
117
|
+
const plain = retagged
|
|
118
|
+
? `<${retagged}> with these classes`
|
|
119
|
+
: `<${entry.tag}>`;
|
|
116
120
|
if (entry.as === 'any')
|
|
117
|
-
return
|
|
121
|
+
return `${plain}, or any tag through \`as\``;
|
|
122
|
+
if (retagged && entry.as) {
|
|
123
|
+
return `${plain}, or ${listTags(entry.as)} through \`as\``;
|
|
124
|
+
}
|
|
118
125
|
if (entry.sizeDrivesTag && entry.as) {
|
|
119
126
|
return `the heading its \`size\` names (<${entry.tag}> with none), or ${listTags(entry.as)} through \`as\``;
|
|
120
127
|
}
|
|
@@ -300,6 +307,8 @@ export function lookupClasses(table, input, tag) {
|
|
|
300
307
|
write.prop,
|
|
301
308
|
{ value: write.value ?? true, token: root ?? '' },
|
|
302
309
|
]));
|
|
310
|
+
/** The one class that picks the component's tag, as the codemod takes it. */
|
|
311
|
+
let retaggedBy;
|
|
303
312
|
const claiming = [
|
|
304
313
|
...tokens.filter(token => !own(table.replaced, token)),
|
|
305
314
|
...tokens.filter(token => own(table.replaced, token)),
|
|
@@ -320,9 +329,16 @@ export function lookupClasses(table, input, tag) {
|
|
|
320
329
|
stays(token, `\`${taken.prop}\` is already set by \`${writes.get(taken.prop).token}\``);
|
|
321
330
|
continue;
|
|
322
331
|
}
|
|
332
|
+
// Only one class picks the tag, as only one sets a prop.
|
|
333
|
+
if (modifier.rendersTag && retaggedBy) {
|
|
334
|
+
stays(token, `\`${retaggedBy}\` already picks the tag`);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
323
337
|
for (const write of modifier.writes) {
|
|
324
338
|
writes.set(write.prop, { value: write.value ?? true, token });
|
|
325
339
|
}
|
|
340
|
+
if (modifier.rendersTag)
|
|
341
|
+
retaggedBy = token;
|
|
326
342
|
// A class the component renders from an attribute of the element
|
|
327
343
|
// inside (`is-multiple` beside a `<select multiple>`).
|
|
328
344
|
const paired = Object.entries(entry.absorbs?.pairs ?? {}).find(([, pairedToken]) => pairedToken === token)?.[0];
|
|
@@ -451,15 +467,26 @@ export function lookupClasses(table, input, tag) {
|
|
|
451
467
|
});
|
|
452
468
|
}
|
|
453
469
|
const size = writes.get('size')?.value;
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
470
|
+
// A class whose prop puts the component on another tag when no `as` is
|
|
471
|
+
// given (`Image`'s ratios on a <figure>).
|
|
472
|
+
const retagged = retaggedBy
|
|
473
|
+
? own(entry.modifiers, retaggedBy)?.rendersTag
|
|
474
|
+
: undefined;
|
|
475
|
+
const renders = retagged ??
|
|
476
|
+
(entry.sizeDrivesTag && tag !== 'p' && typeof size === 'string'
|
|
477
|
+
? `h${size}`
|
|
478
|
+
: entry.tag);
|
|
457
479
|
const reachable = !tag ||
|
|
458
480
|
renders === tag ||
|
|
459
481
|
entry.as === 'any' ||
|
|
460
482
|
(Array.isArray(entry.as) && entry.as.includes(tag));
|
|
461
483
|
if (!reachable) {
|
|
462
|
-
return result({
|
|
484
|
+
return result({
|
|
485
|
+
kind: 'wrong-tag',
|
|
486
|
+
target,
|
|
487
|
+
tag,
|
|
488
|
+
reaches: reaches(entry, retagged),
|
|
489
|
+
});
|
|
463
490
|
}
|
|
464
491
|
// A `className` it's given replaces its own class, so a class left over
|
|
465
492
|
// keeps the element as markup.
|
|
@@ -488,7 +515,7 @@ export function lookupClasses(table, input, tag) {
|
|
|
488
515
|
return result({
|
|
489
516
|
kind: 'component',
|
|
490
517
|
target,
|
|
491
|
-
renders: reaches(entry),
|
|
518
|
+
renders: reaches(entry, retagged),
|
|
492
519
|
...about,
|
|
493
520
|
});
|
|
494
521
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bestax-mcp",
|
|
3
|
-
"version": "1.16.
|
|
3
|
+
"version": "1.16.4",
|
|
4
4
|
"mcpName": "io.github.allxsmith/bestax-mcp",
|
|
5
5
|
"description": "MCP server for @allxsmith/bestax-bulma — component props, examples, CSS variables and agent skills, served offline from a version-pinned index",
|
|
6
6
|
"type": "module",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@modelcontextprotocol/sdk": "^1.31.0",
|
|
44
44
|
"zod": "^4.4.3",
|
|
45
|
-
"@allxsmith/bestax-bulma": "^5.
|
|
45
|
+
"@allxsmith/bestax-bulma": "^5.41.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@jest/globals": "^30.0.0",
|