Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
369 changes: 124 additions & 245 deletions bestax-mcp/data/bulma-classes.json

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions bestax-mcp/data/skills.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
{
"id": "bloomer-prop-map",
"file": "references/bloomer/prop-map.md",
"bytes": 24541
"bytes": 24523
},
{
"id": "bloomer-unmappables",
Expand All @@ -103,17 +103,17 @@
{
"id": "bulma-classes-component-map",
"file": "references/bulma-classes/component-map.md",
"bytes": 14387
"bytes": 14321
},
{
"id": "bulma-classes-prop-map",
"file": "references/bulma-classes/prop-map.md",
"bytes": 26913
"bytes": 26868
},
{
"id": "bulma-classes-unmappables",
"file": "references/bulma-classes/unmappables.md",
"bytes": 15718
"bytes": 15550
},
{
"id": "css-migration",
Expand Down
2 changes: 1 addition & 1 deletion bestax-mcp/src/__tests__/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -696,7 +696,7 @@ describe('lookup_bulma_classes', () => {
'| `mt-2` | stays in `className` | bestax `Navbar.DropdownMenu` takes no helper props |'
);
expect(await lookup('navbar-divider mt-2', 'hr')).toContain(
'**Stays markup:** bestax `Navbar.Divider` drops its own class'
'| `mt-2` | stays in `className` | bestax `Navbar.Divider` takes no helper props |'
);
expect(await lookup('navbar-divider', 'hr')).toContain(
'**Component:** `Navbar.Divider`.'
Expand Down
14 changes: 0 additions & 14 deletions bestax-mcp/src/bulma-classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,6 @@ export interface RootRecord {
wrapsChildren: Wraps | null;
/** The component takes no helper props, so helper classes stay classes. */
noHelpers: boolean;
/** The component drops its own class for a `className` it is given. */
ownClassOnly: boolean;
/** For a `fold` root: the props that render it on the target inside. */
folds: PropWrite[] | null;
/** The component renders the element's only child itself. */
Expand Down Expand Up @@ -371,7 +369,6 @@ export function lookupClasses(
omits: {},
wrapsChildren: null,
noHelpers: false,
ownClassOnly: false,
folds: null,
absorbs: null,
writes: null,
Expand Down Expand Up @@ -549,17 +546,6 @@ export function lookupClasses(
if (!reachable) {
return result({ kind: 'wrong-tag', target, tag, reaches: reaches(entry) });
}
// Every class but the component's own has to become a prop here, since a
// `className` would take the place of that class.
if (
entry.ownClassOnly &&
tokens.some(token => token !== root && verdicts.get(token)?.kind !== 'prop')
) {
return result({
kind: 'markup',
why: `bestax \`${target}\` drops its own class when it is given a \`className\`, so it converts only with no other class`,
});
}
const about = {
wraps,
absorbs: entry.absorbs ?? undefined,
Expand Down
1 change: 0 additions & 1 deletion bestax-migrate/e2e/bulma-classes-kitchen-sink.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,6 @@ describe('bulma-classes kitchen-sink e2e', () => {
const rules = (app.todosByFile.get('leftovers.tsx') ?? []).map(t => t.rule);
expect(rules.sort()).toEqual(
[
'attr:className',
'attr:color',
'children:Card',
'defaults:Delete',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ export function Layout() {
<a className="navbar-item" href="/about">
About
</a>
<hr className="navbar-divider" />
<hr className="navbar-divider my-1" />
<a className="navbar-item" href="/jobs">
Jobs
</a>
</div>
</div>
</div>
<div className="navbar-end">
<div className="navbar-end has-text-weight-semibold has-text-link">
<div className="navbar-item">
<div className="buttons">
<a className="button is-light" href="/login">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ export function Leftovers() {
<span aria-hidden="true" />
<span aria-hidden="true" />
</a>
<hr className="navbar-divider mt-2" />
<div className="dropdown">A family converted later</div>
<div className="tile is-ancestor">A Bulma 0.9 tile</div>
</div>
Expand Down
18 changes: 15 additions & 3 deletions bestax-migrate/src/sources/bloomer/__tests__/specials.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -785,17 +785,29 @@ describe('bloomer navigation handlers', () => {

it('leaves a class-hostile target alone when it carries a spread', () => {
const { output, rules } = migrate(
"import { NavbarDivider, PageEllipsis, DropdownDivider } from 'bloomer';\nexport const A = (p: Record<string, any>) => (<><NavbarDivider {...p.rest} /><PageEllipsis {...p.rest} /><DropdownDivider {...p.rest} /></>);\n"
"import { PageEllipsis, DropdownDivider } from 'bloomer';\nexport const A = (p: Record<string, any>) => (<><PageEllipsis {...p.rest} /><DropdownDivider {...p.rest} /></>);\n"
);
expect(rules).toEqual([
'component:NavbarDivider',
'component:PageEllipsis',
'component:DropdownDivider',
]);
expect(output).toContain('<NavbarDivider {...p.rest} />');
expect(output).toContain('<PageEllipsis {...p.rest} />');
expect(output).toContain("from 'bloomer'");
});

it('converts a NavbarDivider with helpers or a spread', () => {
// bestax `Navbar.Divider` merges a `className` with its own class.
const { output, rules } = migrate(
dyn(
'NavbarDivider',
'<><NavbarDivider isHidden="mobile" /><NavbarDivider {...p.rest} /></>'
)
);
expect(rules).toEqual([]);
expect(output).toContain('<Navbar.Divider className="is-hidden-mobile" />');
expect(output).toContain('<Navbar.Divider {...p.rest} />');
});

it('keeps a NavbarDropdown without isBoxed as the menu', () => {
const { output, rules } = migrate(
dyn('NavbarDropdown', '<NavbarDropdown>x</NavbarDropdown>')
Expand Down
9 changes: 4 additions & 5 deletions bestax-migrate/src/sources/bloomer/mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,15 +146,14 @@ export const RESPONSIVE_PROPS: Record<string, string | null> = {
* emitting an excess-property type error.
*/
/**
* Targets whose `className` cannot be used: `Navbar.Divider` and
* `Pagination.Ellipsis` spread `{...props}` AFTER their own className (so one
* written here replaces the class that makes them work), and
* Targets whose `className` cannot be used: `Pagination.Ellipsis` spreads
* `{...props}` AFTER its own className (so one written here replaces the
* class that makes it work), and
* `Dropdown.Divider` is a bare `React.FC` that declares no props at all and
* renders its own `<hr>` regardless. A helper on one of these is named, never
* converted.
*/
export const NO_CLASSNAME_TARGETS = new Set([
'Navbar.Divider',
'Pagination.Ellipsis',
'Dropdown.Divider',
]);
Expand All @@ -164,7 +163,7 @@ export const HELPERLESS_TARGETS = new Set([
'Pagination.Next',
'Navbar.Dropdown',
'Navbar.DropdownMenu',
'Dropdown.Divider',
'Navbar.Divider',
'Panel.Heading',
'Panel.Tabs',
'Panel.Block',
Expand Down
10 changes: 5 additions & 5 deletions bestax-migrate/src/sources/bloomer/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,13 +343,13 @@ export default function transform(
// Targets that extend only React's HTML attributes take none of bloomer's
// universal helpers; each becomes a TODO naming the Bulma class rather
// than an excess-property type error the report never mentions.
// `Navbar.Divider` and `Pagination.Ellipsis` spread their props after
// their own className, and `Dropdown.Divider` declares none at all, so a
// class cannot be written on any of them: every helper is named instead,
// and anything else left on the divider would not compile.
// `Pagination.Ellipsis` spreads its props after its own className, and
// `Dropdown.Divider` declares none at all, so a class cannot be written
// on either of them: every helper is named instead, and anything else
// left on the divider would not compile.
if (NO_CLASSNAME_TARGETS.has(target)) {
// A spread may carry a `className` that would replace the structural
// class on these two, or props `Dropdown.Divider` silently ignores —
// class on `Pagination.Ellipsis`, or props `Dropdown.Divider` silently ignores —
// and the codemod cannot read inside it. Keep bloomer's element.
const spread = (element.openingElement.attributes ?? []).some(
(a: any) => a.type === 'JSXSpreadAttribute'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,24 @@ export function SiteNavbar() {
</nav>
);
}

export function DarkNavbar() {
return (
<nav className="navbar is-dark" role="navigation" aria-label="main navigation">
<div className="navbar-menu has-text-light">
<div className="navbar-start has-text-weight-bold">
<div className="navbar-item has-dropdown is-hoverable">
<div className="navbar-dropdown">
<a className="navbar-item">About</a>
<hr className="navbar-divider my-0 site-divider" />
<a className="navbar-item">Jobs</a>
</div>
</div>
</div>
<div className="navbar-end has-text-white">
<a className="navbar-item">Log in</a>
</div>
</div>
</nav>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,24 @@ export function SiteNavbar() {
</Navbar>
);
}

export function DarkNavbar() {
return (
<Navbar color="dark" role="navigation" aria-label="main navigation">
<Navbar.Menu textColor="light">
<Navbar.Start textWeight="bold">
<Navbar.Item as="div" className="has-dropdown is-hoverable">
<Navbar.DropdownMenu>
<Navbar.Item>About</Navbar.Item>
<Navbar.Divider className="my-0 site-divider" />
<Navbar.Item>Jobs</Navbar.Item>
</Navbar.DropdownMenu>
</Navbar.Item>
</Navbar.Start>
<Navbar.End textColor="white">
<Navbar.Item>Log in</Navbar.Item>
</Navbar.End>
</Navbar.Menu>
</Navbar>
);
}
28 changes: 18 additions & 10 deletions bestax-migrate/src/sources/bulma-classes/__tests__/plan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,19 +444,27 @@ describe('plan', () => {
});
});

it('converts a divider only when it carries no other class', () => {
it('converts a divider, keeping its other classes in className', () => {
expect(plan(facts('hr', 'navbar-divider')).conversion?.target).toBe(
'Navbar.Divider'
);
expect(plan(facts('hr', 'navbar-divider mt-2'))).toEqual({
conversion: null,
todos: [
{
rule: 'attr:className',
message: expect.stringContaining('drops its own class'),
},
],
});
const { conversion, todos } = plan(facts('hr', 'navbar-divider mt-2'));
expect(todos).toEqual([]);
expect(conversion?.props).toEqual([]);
expect(conversion?.className).toBe('mt-2');
});

it('writes a text color on the navbar areas as textColor', () => {
for (const [root, target] of [
['navbar-menu', 'Navbar.Menu'],
['navbar-start', 'Navbar.Start'],
['navbar-end', 'Navbar.End'],
]) {
const { conversion } = plan(facts('div', `${root} has-text-white`));
expect(conversion?.target).toBe(target);
expect(conversion?.props).toContainEqual(['textColor', 'white']);
expect(conversion?.className).toBeNull();
}
});

it('leaves the burger and the dropdown trigger to a person', () => {
Expand Down
14 changes: 1 addition & 13 deletions bestax-migrate/src/sources/bulma-classes/class-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,11 +152,6 @@ export interface RootEntry {
readonly countsChildren?: { readonly tag: string; readonly prop: string };
/** The target takes no helper props, so every helper class stays a class. */
readonly noHelpers?: boolean;
/**
* The target drops its own class when it is given a `className`, so the
* element converts only when it carries no other class.
*/
readonly ownClassOnly?: boolean;
/**
* Attributes the target types as numbers, among the ones it is given (the
* child's, when it `absorbs` the child and puts them there). A numeric
Expand Down Expand Up @@ -1216,9 +1211,7 @@ export const ROOTS: Readonly<Record<string, RootEntry>> = {
defaults: { role: 'navigation', 'aria-label': 'main navigation' },
ownProps: ['textColor', 'color', 'bgColor', 'transparent', 'fixed'],
},
// No background prop on these parts. Menu, Start and End spread `textColor`
// onto the element rather than reading it, and type `color` to six colors,
// so their text color stays a class.
// No background prop on these parts.
'navbar-brand': {
...BASE,
target: 'Navbar.Brand',
Expand All @@ -1230,7 +1223,6 @@ export const ROOTS: Readonly<Record<string, RootEntry>> = {
...BASE,
target: 'Navbar.Menu',
tag: 'div',
textColor: null,
bgColor: null,
Comment thread
claude[bot] marked this conversation as resolved.
modifiers: flags({ 'is-active': 'active' }),
ownProps: ['textColor', 'color', 'active'],
Expand All @@ -1239,15 +1231,13 @@ export const ROOTS: Readonly<Record<string, RootEntry>> = {
...BASE,
target: 'Navbar.Start',
tag: 'div',
textColor: null,
bgColor: null,
ownProps: ['textColor', 'color'],
},
'navbar-end': {
...BASE,
target: 'Navbar.End',
tag: 'div',
textColor: null,
bgColor: null,
ownProps: ['textColor', 'color'],
},
Expand Down Expand Up @@ -1278,8 +1268,6 @@ export const ROOTS: Readonly<Record<string, RootEntry>> = {
textColor: null,
bgColor: null,
noHelpers: true,
// bulma-ui's Navbar.Divider spreads `className` over its own class.
ownClassOnly: true,
ownProps: [],
},
notification: {
Expand Down
7 changes: 0 additions & 7 deletions bestax-migrate/src/sources/bulma-classes/plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -554,13 +554,6 @@ export function plan(facts: ElementFacts): Plan {
});

const rest = tokens.filter(token => !converted.has(token));
if (entry.ownClassOnly && (rest.length > 0 || conditionalStays)) {
return refuse(
'attr',
'className',
`bestax \`${target}\` drops its own class when it is given a \`className\`, so this element's other classes would take the place of \`.${root}\`; keep it as markup`
);
}
return {
conversion: {
target,
Expand Down
2 changes: 1 addition & 1 deletion bestax-migrate/src/sources/bulma-classes/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const VOCABULARY: ReadonlySet<string> = new Set([
// Refused on every target, so no entry lists them.
'dangerouslySetInnerHTML',
'children',
// Refused where the target drops its own class for one it is given.
// Refused on a wrapper a target renders and on a child it renders itself.
'className',
// Refused on the child a target renders itself.
'key',
Expand Down
1 change: 0 additions & 1 deletion scripts/gen-mcp-index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,6 @@ export async function bulmaClassTable() {
writes: entry.writes ?? null,
countsChildren: entry.countsChildren ?? null,
noHelpers: entry.noHelpers ?? false,
ownClassOnly: entry.ownClassOnly ?? false,
},
])
);
Expand Down
5 changes: 2 additions & 3 deletions scripts/gen-mcp-index.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -235,10 +235,9 @@ test("the bulma-classes table is bestax-migrate's own, whole", () => {
// A family the codemod leaves as markup still says why, for the lookup.
assert.equal(roots.dropdown.status, 'todo');
assert.match(roots.dropdown.why, /Dropdown/);
// And the parts that take no helpers, or no class beside their own, say so.
// And the parts that take no helpers say so.
assert.equal(roots['navbar-dropdown'].noHelpers, true);
assert.equal(roots['navbar-divider'].ownClassOnly, true);
assert.equal(roots.button.ownClassOnly, false);
assert.equal(roots.button.noHelpers, false);
// And a component that converts only beside one of its parts says which.
assert.ok(roots.card.wrapsChildren.unless.includes('Card.Content'));
assert.equal(roots.button.wrapsChildren, null);
Expand Down
4 changes: 2 additions & 2 deletions skills/bestax-migrate/references/bloomer/prop-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,8 @@ A few bestax parts extend only React's HTML attributes and take no Bulma helper
`Pagination.Previous`/`Next`/`Ellipsis`, `Navbar.Dropdown`/`DropdownMenu`/`Divider`,
`Panel.Heading`/`Tabs`/`Block`, `Tabs.List`/`Item`, `Message.Header`/`Body` and the `Modal`
parts. A bloomer helper on one of those becomes the Bulma class in `className` — except on
`Navbar.Divider`, `Pagination.Ellipsis` and `Dropdown.Divider`, which write their own className
last or take no props at all: there the helper is named in a TODO instead, and an element
`Pagination.Ellipsis` and `Dropdown.Divider`, which write their own className last or take no
props at all: there the helper is named in a TODO instead, and an element
carrying a spread is left as bloomer's. Otherwise it is the Bulma class in `className` (`is-pulled-right`,
`m-0`, `is-hidden-mobile`, …), since Bulma v1 still ships every one of them; only a dynamic
value is flagged. The same conversion covers the modifiers bestax has no prop for anywhere —
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ that doesn't gets a `defaults:Navbar` TODO. A `.has-dropdown` item becomes a `Na
keeps `has-dropdown` as a class, because a `Navbar.Dropdown` would give the `.navbar-link` inside
it dropdown semantics the markup didn't have. That link and the `.navbar-burger` stay markup with
a `family:<class>` TODO; converting either means building the dropdown or the toggle with bestax,
by hand. A `.navbar-divider` converts only when it carries no other class.
by hand.

Form markup converts piece by piece: `.field` to `Field`, `.control` to `Control`, the input
and textarea to `InputBase` and `TextAreaBase`, the controls without wrappers of their own, and a
Expand Down
Loading
Loading