diff --git a/bestax-mcp/data/skills.json b/bestax-mcp/data/skills.json index 11c74993a..2eb4c888d 100644 --- a/bestax-mcp/data/skills.json +++ b/bestax-mcp/data/skills.json @@ -43,7 +43,7 @@ { "id": "icon-libraries", "file": "references/icon-libraries.md", - "bytes": 6229 + "bytes": 6691 } ], "examples": [ @@ -162,7 +162,7 @@ { "id": "modular-build", "file": "references/modular-build.md", - "bytes": 9431 + "bytes": 9583 } ], "examples": [] diff --git a/create-bestax/CLAUDE.md b/create-bestax/CLAUDE.md index 63b0d2424..88b6d5011 100644 --- a/create-bestax/CLAUDE.md +++ b/create-bestax/CLAUDE.md @@ -75,6 +75,15 @@ manifest to every required peer of the installed library, so a new or changed pe silently dropped at ingest. - Templates pin the library's CSS import and icon setup — a change to bulma-ui's published exports or flavors (`bestax.css`, `versions/*.css`) may require a template update. +- The scaffolder edits the starter `App` with string patterns, so `src/__tests__/templates.test.ts` + runs every edit against the real templates. Under a `noHelpers` flavor each helper prop in + the starter becomes a named class from `NO_HELPERS_STARTER_CLASSES` in `src/constants.ts`; + a helper prop added to the starter needs a row there, and that test fails until it has one. + The icon step runs after that swap and writes into the same `App`, so what it inserts can + use no helper prop at all; the test scans the `App` both steps leave behind. +- An icon library's `packageVersion` follows the newest arm of bestax-bulma's peer range for + it, not the newest release: on a 0.x package the caret holds the minor, so a pin past the + peer range makes npm refuse the scaffold's install. The same test holds the two together. ## Testing diff --git a/create-bestax/e2e/README.md b/create-bestax/e2e/README.md index dd5a85bbb..5e436a931 100644 --- a/create-bestax/e2e/README.md +++ b/create-bestax/e2e/README.md @@ -107,6 +107,10 @@ Each scaffolded app is tested with: 8. **Tablet View** - Responsive layout at 768×1024 9. **Mobile View** - Responsive layout at 375×667 10. **Full Page** - Complete page screenshot +11. **Layout and Accessibility** - Assertions with no screenshot: no sideways scroll at phone + width, a centered title and even card heights in every flavor, icons spaced from their + text, the notification's close button, landmarks and heading order, and an underlined + link in running text ## Visual Regression Testing diff --git a/create-bestax/e2e/tests/app.spec.ts b/create-bestax/e2e/tests/app.spec.ts index b84af0608..4f6530fcf 100644 --- a/create-bestax/e2e/tests/app.spec.ts +++ b/create-bestax/e2e/tests/app.spec.ts @@ -282,3 +282,213 @@ test.describe('Scaffolded App - Full Page', () => { }); }); }); + +/** + * Behavior the screenshots can't pin down: each of these held in one flavor + * or at one width and broke in another, so they are asserted directly and run + * in every scenario. + */ +test.describe('Scaffolded App - Layout and Accessibility', () => { + test('does not scroll sideways at phone width', async ({ page }) => { + await page.setViewportSize({ width: 390, height: 844 }); + await page.goto('/'); + await expect(page.locator('h1').first()).toBeVisible(); + + const overflow = await page.evaluate( + () => + document.documentElement.scrollWidth - + document.documentElement.clientWidth + ); + expect(overflow).toBeLessThanOrEqual(0); + }); + + test('centers the title and evens the card heights in every flavor', async ({ + page, + }) => { + await page.goto('/'); + + await expect(page.locator('h1').first()).toHaveCSS('text-align', 'center'); + + const heights = await page + .locator('.card, .bestax-card') + .evaluateAll(cards => + cards.map(card => Math.round(card.getBoundingClientRect().height)) + ); + expect(heights).toHaveLength(3); + expect(new Set(heights).size).toBe(1); + }); + + test('spaces each card icon from its title', async ({ page }) => { + // CI names each scenario template-flavor-icon, so whether icons were + // asked for comes from there, and a scaffold that asked for them but + // rendered none fails here rather than skipping. A run with no scenario + // set falls back to what the settled page shows. + const scenario = process.env.TEST_SCENARIO; + await page.goto('/'); + await expect(page.locator('h1').first()).toBeVisible(); + const titles = page.locator( + ':is(.card-header-title, .bestax-card-header-title):has(:is(.icon, .bestax-icon))' + ); + const wantsIcons = scenario + ? !scenario.endsWith('-none') + : (await titles.count()) > 0; + test.skip(!wantsIcons, 'scaffolded without icons'); + await expect(titles).toHaveCount(3); + + // From the icon's right edge to where the first glyph of the title text + // is drawn, wherever in the title the text node sits. + const gaps = await titles.evaluateAll(items => + items.map(item => { + const icon = item.querySelector('.icon, .bestax-icon')!; + const walker = document.createTreeWalker(item, NodeFilter.SHOW_TEXT); + let text: Node | null = walker.nextNode(); + while (text && (icon.contains(text) || !text.textContent?.trim())) { + text = walker.nextNode(); + } + const range = document.createRange(); + range.selectNodeContents(text!); + return ( + range.getClientRects()[0].left - icon.getBoundingClientRect().right + ); + }) + ); + for (const gap of gaps) expect(gap).toBeGreaterThan(0); + }); + + test('closes the success notification from its delete button', async ({ + page, + }) => { + await page.goto('/'); + const toggleButton = page + .locator('button') + .filter({ hasText: 'Toggle Notification' }); + await toggleButton.click(); + + const notification = page.locator( + '[class*="notification"][class*="is-success"]' + ); + await expect(notification).toBeVisible(); + // From the keyboard: the close button goes away with the notification, + // so focus has to land somewhere the next Tab continues from. + await notification + .getByRole('button', { name: 'Close notification' }) + .focus(); + await page.keyboard.press('Enter'); + await expect(notification).toHaveCount(0); + await expect(toggleButton).toBeFocused(); + }); + + test('keeps focus on the counter when Reset disables itself', async ({ + page, + }) => { + await page.goto('/'); + const counterButton = page.locator('button').filter({ hasText: 'Count:' }); + const resetButton = page.locator('button').filter({ hasText: 'Reset' }); + await counterButton.click(); + await expect(resetButton).toBeEnabled(); + + // From the keyboard: Reset turns disabled at zero, and a disabled button + // gives up focus, so focus has to move somewhere the next Tab continues + // from. + await resetButton.focus(); + await page.keyboard.press('Enter'); + await expect(counterButton).toContainText('Count: 0'); + await expect(resetButton).toBeDisabled(); + await expect(counterButton).toBeFocused(); + }); + + test('announces each notification through a status region of its own', async ({ + page, + }) => { + await page.goto('/'); + // A live region added along with its content may not be read out, so the + // regions are on the page, empty, before either notification is. + const regions = page.getByRole('status'); + await expect(regions).toHaveCount(2); + const success = regions.nth(0); + const milestone = regions.nth(1); + await expect(success).toBeEmpty(); + await expect(milestone).toBeEmpty(); + const before = await regions.elementHandles(); + + await page + .locator('button') + .filter({ hasText: 'Toggle Notification' }) + .click(); + await expect(success).toContainText('Success!'); + await expect(milestone).toBeEmpty(); + + // A status region reads out all it holds whenever any of it changes, so + // the count's notification is alone in its region, and a click does not + // read the success notification out again. + const counterButton = page.locator('button').filter({ hasText: 'Count:' }); + for (let i = 0; i < 12; i++) { + await counterButton.click(); + } + await expect(milestone).toHaveText("You've clicked the button 12 times!"); + await expect(success).not.toContainText('clicked'); + + // Still the nodes that were there before anything appeared in them. + const after = await regions.elementHandles(); + for (const [i, region] of before.entries()) { + expect(await after[i].evaluate((el, b) => el === b, region)).toBe(true); + } + }); + + // Bulma spaces a notification from what follows it in the same parent, and + // each one is alone in its region, so App.css puts that space between the + // regions. No screenshot shows both notifications at once. The space is + // Bulma's block spacing, which is also what it leaves under the first box. + test('spaces two notifications showing at once', async ({ page }) => { + await page.goto('/'); + await page + .locator('button') + .filter({ hasText: 'Toggle Notification' }) + .click(); + const counterButton = page.locator('button').filter({ hasText: 'Count:' }); + for (let i = 0; i < 11; i++) { + await counterButton.click(); + } + + const success = page.locator( + '[class*="notification"][class*="is-success"]' + ); + const milestone = page.locator('[class*="notification"][class*="is-info"]'); + const top = (await success.boundingBox())!; + const bottom = (await milestone.boundingBox())!; + const blockSpacing = await page + .locator('.box, .bulma-box, .bestax-box') + .first() + .evaluate(box => parseFloat(getComputedStyle(box).marginBottom)); + expect(blockSpacing).toBeGreaterThan(0); + expect(bottom.y - (top.y + top.height)).toBeCloseTo(blockSpacing); + }); + + test('has one main landmark, no banners, and no skipped heading levels', async ({ + page, + }) => { + await page.goto('/'); + + await expect(page.getByRole('main')).toHaveCount(1); + // A card header outside every landmark is exposed as a page banner. + await expect(page.getByRole('banner')).toHaveCount(0); + + const levels = await page + .locator('h1, h2, h3, h4, h5, h6') + .evaluateAll(headings => headings.map(h => Number(h.tagName.slice(1)))); + expect(levels[0]).toBe(1); + levels.slice(1).forEach((level, i) => { + expect(level - levels[i]).toBeLessThanOrEqual(1); + }); + }); + + test('marks the link in running text by more than its color', async ({ + page, + }) => { + await page.goto('/'); + await expect(page.getByRole('link', { name: 'bestax.io' })).toHaveCSS( + 'text-decoration-line', + 'underline' + ); + }); +}); diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/01-logo-section-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/01-logo-section-chromium-linux.png index 6e834c3bd..182e897d0 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/01-logo-section-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/01-logo-section-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/02-info-cards-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/02-info-cards-chromium-linux.png index 065713afe..0ba7db922 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/02-info-cards-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/02-info-cards-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/03-notification-visible-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/03-notification-visible-chromium-linux.png index 5decff162..5cc3fd53b 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/03-notification-visible-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/03-notification-visible-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/04-notification-hidden-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/04-notification-hidden-chromium-linux.png index 4848f0097..30d47a1d3 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/04-notification-hidden-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/04-notification-hidden-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/05-counter-incremented-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/05-counter-incremented-chromium-linux.png index 597397de5..3644b9099 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/05-counter-incremented-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/05-counter-incremented-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/06-counter-reset-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/06-counter-reset-chromium-linux.png index 3a292605f..305be19d0 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/06-counter-reset-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/06-counter-reset-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/07-counter-milestone-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/07-counter-milestone-chromium-linux.png index 31fb6a9bd..c2643b289 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/07-counter-milestone-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/07-counter-milestone-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/08-tablet-view-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/08-tablet-view-chromium-linux.png index 0ce272c12..ab11a4503 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/08-tablet-view-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/08-tablet-view-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/09-mobile-view-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/09-mobile-view-chromium-linux.png index 74c8caec0..6ba0a80cc 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/09-mobile-view-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/09-mobile-view-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/10-full-page-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/10-full-page-chromium-linux.png index e00d0a416..a5c15de64 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/10-full-page-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-fontawesome/10-full-page-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/01-logo-section-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/01-logo-section-chromium-linux.png index 6e834c3bd..182e897d0 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/01-logo-section-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/01-logo-section-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/02-info-cards-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/02-info-cards-chromium-linux.png index a3267d4bd..8f4a22b09 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/02-info-cards-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/02-info-cards-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/03-notification-visible-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/03-notification-visible-chromium-linux.png index 5decff162..5cc3fd53b 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/03-notification-visible-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/03-notification-visible-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/04-notification-hidden-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/04-notification-hidden-chromium-linux.png index 4848f0097..30d47a1d3 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/04-notification-hidden-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/04-notification-hidden-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/05-counter-incremented-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/05-counter-incremented-chromium-linux.png index 597397de5..3644b9099 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/05-counter-incremented-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/05-counter-incremented-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/06-counter-reset-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/06-counter-reset-chromium-linux.png index 3a292605f..305be19d0 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/06-counter-reset-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/06-counter-reset-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/07-counter-milestone-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/07-counter-milestone-chromium-linux.png index 31fb6a9bd..c2643b289 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/07-counter-milestone-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/07-counter-milestone-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/08-tablet-view-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/08-tablet-view-chromium-linux.png index 253fb686d..19242bd50 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/08-tablet-view-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/08-tablet-view-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/09-mobile-view-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/09-mobile-view-chromium-linux.png index ad728169b..b1c9373e4 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/09-mobile-view-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/09-mobile-view-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/10-full-page-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/10-full-page-chromium-linux.png index ac4aac851..ebfbd25ef 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/10-full-page-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-mdi/10-full-page-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-none/01-logo-section-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-none/01-logo-section-chromium-linux.png index 6e834c3bd..182e897d0 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-none/01-logo-section-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-none/01-logo-section-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-none/03-notification-visible-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-none/03-notification-visible-chromium-linux.png index 5decff162..5cc3fd53b 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-none/03-notification-visible-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-none/03-notification-visible-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-none/04-notification-hidden-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-none/04-notification-hidden-chromium-linux.png index 4848f0097..30d47a1d3 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-none/04-notification-hidden-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-none/04-notification-hidden-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-none/05-counter-incremented-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-none/05-counter-incremented-chromium-linux.png index 597397de5..3644b9099 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-none/05-counter-incremented-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-none/05-counter-incremented-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-none/06-counter-reset-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-none/06-counter-reset-chromium-linux.png index 3a292605f..305be19d0 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-none/06-counter-reset-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-none/06-counter-reset-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-none/07-counter-milestone-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-none/07-counter-milestone-chromium-linux.png index 31fb6a9bd..c2643b289 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-none/07-counter-milestone-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-none/07-counter-milestone-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-none/08-tablet-view-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-none/08-tablet-view-chromium-linux.png index b593db0c7..b31f64a30 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-none/08-tablet-view-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-none/08-tablet-view-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-none/09-mobile-view-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-none/09-mobile-view-chromium-linux.png index 0e5194932..74d89717e 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-none/09-mobile-view-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-none/09-mobile-view-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-none/10-full-page-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-none/10-full-page-chromium-linux.png index a2d7344fa..aaa9ebc20 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-none/10-full-page-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-complete-none/10-full-page-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-no-helpers-none/01-logo-section-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-no-helpers-none/01-logo-section-chromium-linux.png index 39aa500a8..182e897d0 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-no-helpers-none/01-logo-section-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-no-helpers-none/01-logo-section-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-no-helpers-none/03-notification-visible-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-no-helpers-none/03-notification-visible-chromium-linux.png index 5decff162..5cc3fd53b 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-no-helpers-none/03-notification-visible-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-no-helpers-none/03-notification-visible-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-no-helpers-none/04-notification-hidden-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-no-helpers-none/04-notification-hidden-chromium-linux.png index 4848f0097..30d47a1d3 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-no-helpers-none/04-notification-hidden-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-no-helpers-none/04-notification-hidden-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-no-helpers-none/05-counter-incremented-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-no-helpers-none/05-counter-incremented-chromium-linux.png index 597397de5..3644b9099 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-no-helpers-none/05-counter-incremented-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-no-helpers-none/05-counter-incremented-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-no-helpers-none/06-counter-reset-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-no-helpers-none/06-counter-reset-chromium-linux.png index 3a292605f..305be19d0 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-no-helpers-none/06-counter-reset-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-no-helpers-none/06-counter-reset-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-no-helpers-none/07-counter-milestone-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-no-helpers-none/07-counter-milestone-chromium-linux.png index 31fb6a9bd..c2643b289 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-no-helpers-none/07-counter-milestone-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-no-helpers-none/07-counter-milestone-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-no-helpers-none/08-tablet-view-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-no-helpers-none/08-tablet-view-chromium-linux.png index 34977b737..b31f64a30 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-no-helpers-none/08-tablet-view-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-no-helpers-none/08-tablet-view-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-no-helpers-none/09-mobile-view-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-no-helpers-none/09-mobile-view-chromium-linux.png index 5b160bbb2..74d89717e 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-no-helpers-none/09-mobile-view-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-no-helpers-none/09-mobile-view-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-no-helpers-none/10-full-page-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-no-helpers-none/10-full-page-chromium-linux.png index e62a7085a..aaa9ebc20 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-no-helpers-none/10-full-page-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-no-helpers-none/10-full-page-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-prefixed-none/01-logo-section-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-prefixed-none/01-logo-section-chromium-linux.png index 6e834c3bd..182e897d0 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-prefixed-none/01-logo-section-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-prefixed-none/01-logo-section-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-prefixed-none/03-notification-visible-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-prefixed-none/03-notification-visible-chromium-linux.png index 5decff162..5cc3fd53b 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-prefixed-none/03-notification-visible-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-prefixed-none/03-notification-visible-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-prefixed-none/04-notification-hidden-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-prefixed-none/04-notification-hidden-chromium-linux.png index 4848f0097..30d47a1d3 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-prefixed-none/04-notification-hidden-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-prefixed-none/04-notification-hidden-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-prefixed-none/05-counter-incremented-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-prefixed-none/05-counter-incremented-chromium-linux.png index 597397de5..3644b9099 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-prefixed-none/05-counter-incremented-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-prefixed-none/05-counter-incremented-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-prefixed-none/06-counter-reset-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-prefixed-none/06-counter-reset-chromium-linux.png index 3a292605f..305be19d0 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-prefixed-none/06-counter-reset-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-prefixed-none/06-counter-reset-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-prefixed-none/07-counter-milestone-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-prefixed-none/07-counter-milestone-chromium-linux.png index 31fb6a9bd..c2643b289 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-prefixed-none/07-counter-milestone-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-prefixed-none/07-counter-milestone-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-prefixed-none/08-tablet-view-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-prefixed-none/08-tablet-view-chromium-linux.png index b593db0c7..b31f64a30 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-prefixed-none/08-tablet-view-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-prefixed-none/08-tablet-view-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-prefixed-none/09-mobile-view-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-prefixed-none/09-mobile-view-chromium-linux.png index 0e5194932..74d89717e 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-prefixed-none/09-mobile-view-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-prefixed-none/09-mobile-view-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-prefixed-none/10-full-page-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-prefixed-none/10-full-page-chromium-linux.png index a2d7344fa..aaa9ebc20 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-prefixed-none/10-full-page-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-prefixed-none/10-full-page-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/01-logo-section-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/01-logo-section-chromium-linux.png index 6e834c3bd..182e897d0 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/01-logo-section-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/01-logo-section-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/02-info-cards-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/02-info-cards-chromium-linux.png index e628d33a0..126426a91 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/02-info-cards-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/02-info-cards-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/03-notification-visible-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/03-notification-visible-chromium-linux.png index 5decff162..5cc3fd53b 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/03-notification-visible-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/03-notification-visible-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/04-notification-hidden-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/04-notification-hidden-chromium-linux.png index 4848f0097..30d47a1d3 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/04-notification-hidden-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/04-notification-hidden-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/05-counter-incremented-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/05-counter-incremented-chromium-linux.png index 597397de5..3644b9099 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/05-counter-incremented-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/05-counter-incremented-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/06-counter-reset-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/06-counter-reset-chromium-linux.png index 3a292605f..305be19d0 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/06-counter-reset-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/06-counter-reset-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/07-counter-milestone-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/07-counter-milestone-chromium-linux.png index 31fb6a9bd..c2643b289 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/07-counter-milestone-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/07-counter-milestone-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/08-tablet-view-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/08-tablet-view-chromium-linux.png index abf52a866..202a8f4cf 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/08-tablet-view-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/08-tablet-view-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/09-mobile-view-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/09-mobile-view-chromium-linux.png index 6dfce1386..8da00e17d 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/09-mobile-view-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/09-mobile-view-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/10-full-page-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/10-full-page-chromium-linux.png index e97391a79..3fe02bc45 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/10-full-page-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-fontawesome/10-full-page-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/01-logo-section-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/01-logo-section-chromium-linux.png index 6e834c3bd..182e897d0 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/01-logo-section-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/01-logo-section-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/02-info-cards-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/02-info-cards-chromium-linux.png index 690c84887..7dabd0c61 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/02-info-cards-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/02-info-cards-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/03-notification-visible-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/03-notification-visible-chromium-linux.png index 5decff162..5cc3fd53b 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/03-notification-visible-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/03-notification-visible-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/04-notification-hidden-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/04-notification-hidden-chromium-linux.png index 4848f0097..30d47a1d3 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/04-notification-hidden-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/04-notification-hidden-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/05-counter-incremented-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/05-counter-incremented-chromium-linux.png index 597397de5..3644b9099 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/05-counter-incremented-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/05-counter-incremented-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/06-counter-reset-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/06-counter-reset-chromium-linux.png index 3a292605f..305be19d0 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/06-counter-reset-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/06-counter-reset-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/07-counter-milestone-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/07-counter-milestone-chromium-linux.png index 31fb6a9bd..c2643b289 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/07-counter-milestone-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/07-counter-milestone-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/08-tablet-view-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/08-tablet-view-chromium-linux.png index 693aab6ce..5ae0e9efb 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/08-tablet-view-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/08-tablet-view-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/09-mobile-view-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/09-mobile-view-chromium-linux.png index b8cbbc56f..124273de3 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/09-mobile-view-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/09-mobile-view-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/10-full-page-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/10-full-page-chromium-linux.png index e7e06037c..9d098da03 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/10-full-page-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-mdi/10-full-page-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-none/01-logo-section-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-none/01-logo-section-chromium-linux.png index 6e834c3bd..182e897d0 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-none/01-logo-section-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-none/01-logo-section-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-none/03-notification-visible-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-none/03-notification-visible-chromium-linux.png index 5decff162..5cc3fd53b 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-none/03-notification-visible-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-none/03-notification-visible-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-none/04-notification-hidden-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-none/04-notification-hidden-chromium-linux.png index 4848f0097..30d47a1d3 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-none/04-notification-hidden-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-none/04-notification-hidden-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-none/05-counter-incremented-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-none/05-counter-incremented-chromium-linux.png index 597397de5..3644b9099 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-none/05-counter-incremented-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-none/05-counter-incremented-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-none/06-counter-reset-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-none/06-counter-reset-chromium-linux.png index 3a292605f..305be19d0 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-none/06-counter-reset-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-none/06-counter-reset-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-none/07-counter-milestone-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-none/07-counter-milestone-chromium-linux.png index 31fb6a9bd..c2643b289 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-none/07-counter-milestone-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-none/07-counter-milestone-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-none/08-tablet-view-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-none/08-tablet-view-chromium-linux.png index 63c8cee37..d7922389b 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-none/08-tablet-view-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-none/08-tablet-view-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-none/09-mobile-view-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-none/09-mobile-view-chromium-linux.png index c9b769237..a4d024c65 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-none/09-mobile-view-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-none/09-mobile-view-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-none/10-full-page-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-none/10-full-page-chromium-linux.png index 40a77cf6c..af0f86c11 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-none/10-full-page-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-complete-none/10-full-page-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-no-helpers-none/01-logo-section-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-no-helpers-none/01-logo-section-chromium-linux.png index 39aa500a8..182e897d0 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-no-helpers-none/01-logo-section-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-no-helpers-none/01-logo-section-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-no-helpers-none/03-notification-visible-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-no-helpers-none/03-notification-visible-chromium-linux.png index 5decff162..5cc3fd53b 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-no-helpers-none/03-notification-visible-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-no-helpers-none/03-notification-visible-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-no-helpers-none/04-notification-hidden-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-no-helpers-none/04-notification-hidden-chromium-linux.png index 4848f0097..30d47a1d3 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-no-helpers-none/04-notification-hidden-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-no-helpers-none/04-notification-hidden-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-no-helpers-none/05-counter-incremented-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-no-helpers-none/05-counter-incremented-chromium-linux.png index 597397de5..3644b9099 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-no-helpers-none/05-counter-incremented-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-no-helpers-none/05-counter-incremented-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-no-helpers-none/06-counter-reset-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-no-helpers-none/06-counter-reset-chromium-linux.png index 3a292605f..305be19d0 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-no-helpers-none/06-counter-reset-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-no-helpers-none/06-counter-reset-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-no-helpers-none/07-counter-milestone-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-no-helpers-none/07-counter-milestone-chromium-linux.png index 31fb6a9bd..c2643b289 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-no-helpers-none/07-counter-milestone-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-no-helpers-none/07-counter-milestone-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-no-helpers-none/08-tablet-view-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-no-helpers-none/08-tablet-view-chromium-linux.png index 6e1d1f02b..d7922389b 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-no-helpers-none/08-tablet-view-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-no-helpers-none/08-tablet-view-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-no-helpers-none/09-mobile-view-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-no-helpers-none/09-mobile-view-chromium-linux.png index 4689dc3cc..a4d024c65 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-no-helpers-none/09-mobile-view-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-no-helpers-none/09-mobile-view-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-no-helpers-none/10-full-page-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-no-helpers-none/10-full-page-chromium-linux.png index 579ae34d6..af0f86c11 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-no-helpers-none/10-full-page-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-no-helpers-none/10-full-page-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-prefixed-none/01-logo-section-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-prefixed-none/01-logo-section-chromium-linux.png index 6e834c3bd..182e897d0 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-prefixed-none/01-logo-section-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-prefixed-none/01-logo-section-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-prefixed-none/03-notification-visible-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-prefixed-none/03-notification-visible-chromium-linux.png index 5decff162..5cc3fd53b 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-prefixed-none/03-notification-visible-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-prefixed-none/03-notification-visible-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-prefixed-none/04-notification-hidden-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-prefixed-none/04-notification-hidden-chromium-linux.png index 4848f0097..30d47a1d3 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-prefixed-none/04-notification-hidden-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-prefixed-none/04-notification-hidden-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-prefixed-none/05-counter-incremented-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-prefixed-none/05-counter-incremented-chromium-linux.png index 597397de5..3644b9099 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-prefixed-none/05-counter-incremented-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-prefixed-none/05-counter-incremented-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-prefixed-none/06-counter-reset-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-prefixed-none/06-counter-reset-chromium-linux.png index 3a292605f..305be19d0 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-prefixed-none/06-counter-reset-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-prefixed-none/06-counter-reset-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-prefixed-none/07-counter-milestone-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-prefixed-none/07-counter-milestone-chromium-linux.png index 31fb6a9bd..c2643b289 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-prefixed-none/07-counter-milestone-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-prefixed-none/07-counter-milestone-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-prefixed-none/08-tablet-view-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-prefixed-none/08-tablet-view-chromium-linux.png index 63c8cee37..d7922389b 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-prefixed-none/08-tablet-view-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-prefixed-none/08-tablet-view-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-prefixed-none/09-mobile-view-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-prefixed-none/09-mobile-view-chromium-linux.png index c9b769237..a4d024c65 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-prefixed-none/09-mobile-view-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-prefixed-none/09-mobile-view-chromium-linux.png differ diff --git a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-prefixed-none/10-full-page-chromium-linux.png b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-prefixed-none/10-full-page-chromium-linux.png index 40a77cf6c..af0f86c11 100644 Binary files a/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-prefixed-none/10-full-page-chromium-linux.png and b/create-bestax/e2e/tests/app.spec.ts-snapshots/vite-ts-prefixed-none/10-full-page-chromium-linux.png differ diff --git a/create-bestax/src/__tests__/constants.test.ts b/create-bestax/src/__tests__/constants.test.ts index 4d940f294..1e6aff49d 100644 --- a/create-bestax/src/__tests__/constants.test.ts +++ b/create-bestax/src/__tests__/constants.test.ts @@ -1,4 +1,7 @@ import { describe, it, expect, jest } from '@jest/globals'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; // Mock chalk jest.unstable_mockModule('chalk', () => ({ @@ -317,6 +320,18 @@ describe('constants', () => { }); }); + // Card.* sub-parts take helper props and Card.Header.Title an `as`, which + // the starter's own card titles use, so an agent told otherwise would + // strip the heading level the starter relies on. + it('keeps Card out of the sub-parts that take no helper props or `as`', () => { + const rule = md + .split('\n- ') + .find(line => line.startsWith('Compound sub-parts')); + expect(rule).toBeDefined(); + expect(rule).toContain('no `as`'); + expect(rule).not.toContain('`Card.*`'); + }); + it('flags Notification as the mixed case: textColor yes, bgColor no', () => { // Notification omits `backgroundColor` and re-adds only `textColor`, so a // `bgColor` on it is inert — its background is the semantic `color` prop. @@ -334,6 +349,116 @@ describe('constants', () => { }); }); + // The no-helpers flavors ship none of the classes the helper props compile + // to, so the table that sends every inline style to a helper prop would + // send an agent to props that render nothing (#948). + describe.each(['no-helpers', 'no-helpers-prefixed'])( + 'CLAUDE_MD under the %s flavor', + bulmaFlavor => { + const md = CLAUDE_MD('my-app', { bulmaFlavor, iconLibrary: 'none' }); + + it('says the helper props for the left-out classes render nothing', () => { + expect(md).toContain( + '**helper props that add those classes render nothing**' + ); + expect(md).toContain( + "this app's flavor leaves out Bulma's helper classes" + ); + }); + + // The flavor keeps Bulma's skeleton styles, so `skeleton` is a helper + // prop that still works, and a sentence saying every helper prop + // renders nothing would send an agent to hand-roll a loading state. + it('names skeleton as a helper prop that still works', () => { + expect(md).toContain('`skeleton` still works'); + expect(md).not.toContain('and the rest'); + }); + + // What the text says the flavor leaves out and keeps is what its + // stylesheet forwards. + it("matches what the flavor's stylesheet forwards", () => { + const flavor = BULMA_FLAVORS.find(f => f.name === bulmaFlavor)!; + const css = /versions\/([\w-]+)\.css/.exec(flavor.importStatement)![1]; + const scss = fs.readFileSync( + path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + `../../../bulma-ui/src/scss/versions/${css}.scss` + ), + 'utf8' + ); + expect(scss).toContain('@forward "bulma/sass/base/skeleton"'); + expect(scss).not.toContain('bulma/sass/helpers'); + }); + + it('sends inline styles to a named class instead of a helper prop', () => { + expect(md).toContain('Never inline `style={{}}`'); + expect(md).toContain( + "Write a named class in `src/App.css`, which loads after Bulma's CSS, and pass it via `className`." + ); + expect(md).not.toContain('Helper props instead'); + expect(md).not.toContain('No helper matches'); + }); + + // Neither a hand-written helper class nor the wrapper element's helper + // prop renders anything in this flavor, so the utility-class rule must + // not offer the wrapper as the way out. + it('does not send hand-written utility classes to helper props instead', () => { + expect(md).toContain("Don't hand-write Bulma utility classes"); + expect(md).not.toContain('take the same helper props'); + expect(md).toContain('`has-navbar-fixed-top`'); + }); + } + ); + + it('CLAUDE_MD offers the helper-prop wrappers where the flavor has helpers', () => { + const md = CLAUDE_MD('my-app', { + bulmaFlavor: 'complete', + iconLibrary: 'none', + }); + expect(md).toContain('take the same helper props'); + expect(md).toContain('`Span`, `Paragraph`, `Strong`'); + }); + + it('CLAUDE_MD keeps the helper-prop table for every flavor that has helpers', () => { + for (const flavor of BULMA_FLAVORS.filter(f => !f.noHelpers)) { + const md = CLAUDE_MD('my-app', { + bulmaFlavor: flavor.name, + iconLibrary: 'none', + }); + expect(md).toContain('Helper props instead'); + expect(md).not.toContain("leaves out Bulma's helper classes"); + } + }); + + it('CLAUDE_MD says only the outlined Material Symbols style is loaded', () => { + const md = CLAUDE_MD('my-app', { + bulmaFlavor: 'complete', + iconLibrary: 'material-symbols', + }); + expect(md).toContain('Only the outlined style is loaded'); + expect(md).toContain('`material-symbols/rounded.css`'); + }); + + it('CLAUDE_MD says only the filled Material Icons style is loaded', () => { + const md = CLAUDE_MD('my-app', { + bulmaFlavor: 'complete', + iconLibrary: 'material-icons', + }); + expect(md).toContain('Only the filled style is loaded'); + expect(md).toContain('`material-icons/iconfont/`'); + }); + + // Both Material packages' bare import pulls in every style's font, while + // the starter renders only the default style. + it.each(['material-icons', 'material-symbols'])( + 'imports one %s font style, not the bare package', + name => { + const library = ICON_LIBRARIES.find(lib => lib.name === name)!; + expect(library.importStatement).not.toBe(`import '${name}';`); + expect(library.importStatement).toMatch(/\.css';$/); + } + ); + describe('DEFAULT_PROJECT_NAME', () => { it('should be a valid project name', () => { expect(DEFAULT_PROJECT_NAME).toBeDefined(); diff --git a/create-bestax/src/__tests__/project-creator.test.ts b/create-bestax/src/__tests__/project-creator.test.ts index 96a3e31d7..7d1bc445d 100644 --- a/create-bestax/src/__tests__/project-creator.test.ts +++ b/create-bestax/src/__tests__/project-creator.test.ts @@ -515,6 +515,49 @@ describe('ProjectCreator', () => { ); }); + it('swaps the starter helper props for named classes under a no-helpers flavor', async () => { + const targetPath = '/test/project'; + ( + fs.default.existsSync as jest.MockedFunction + ).mockReturnValue(true); + ( + fs.default.readFile as jest.MockedFunction + ).mockImplementation((async (file: string) => + file.endsWith('App.tsx') + ? 'x' + : file.endsWith('App.css') + ? '/* app */\n\n\n' + : "import '@allxsmith/bestax-bulma/bestax.css';") as never); + + await projectCreator.setupBulmaFlavor( + targetPath, + 'no-helpers', + 'vite-ts' + ); + + expect(fs.default.writeFile).toHaveBeenCalledWith( + '/test/project/src/App.tsx', + 'x' + ); + expect(fs.default.writeFile).toHaveBeenCalledWith( + '/test/project/src/App.css', + expect.stringMatching( + /^\/\* app \*\/\n\n\/\* This flavor[\s\S]*\.page-title \{\n {2}text-align: center;\n\}/ + ) + ); + }); + + it('leaves the starter alone when a no-helpers scaffold has no App files', async () => { + ( + fs.default.existsSync as jest.MockedFunction + ).mockReturnValue(false); + + await projectCreator.replaceStarterHelperProps('/test/project', 'vite'); + + expect(fs.default.readFile).not.toHaveBeenCalled(); + expect(fs.default.writeFile).not.toHaveBeenCalled(); + }); + it('should handle unknown Bulma flavor gracefully', async () => { const targetPath = '/test/project'; @@ -561,12 +604,17 @@ describe('ProjectCreator', () => { expect(writeCall[0]).toBe(appFilePath); const writtenContent = writeCall[1] as string; - // Check Icon was added to import - expect(writtenContent).toContain('Icon'); - // Check icons were added to cards - expect(writtenContent).toContain('Icon name="rocket"'); - expect(writtenContent).toContain('Icon name="book"'); - expect(writtenContent).toContain('Icon name="code"'); + // Check IconText was added to the import + expect(writtenContent).toMatch(/Card,\n\s+IconText,\n\} from/); + // Check each card title wraps its text in IconText + expect(writtenContent).toContain("name: 'rocket'"); + expect(writtenContent).toContain("name: 'book'"); + expect(writtenContent).toContain("name: 'code'"); + expect(writtenContent).toMatch( + /\n\s+\n\s+<\/Card\.Header\.Title>/ + ); + // A margin helper would render nothing in the no-helpers flavors + expect(writtenContent).not.toContain('mr='); }); it('should handle App.jsx with Icon already in import', async () => { @@ -639,6 +687,19 @@ describe('ProjectCreator', () => { }); }); + describe('updateReadme', () => { + it('does nothing when the template has no README', async () => { + ( + fs.default.existsSync as jest.MockedFunction + ).mockReturnValue(false); + + await projectCreator.updateReadme('/test/project', 'fontawesome'); + + expect(fs.default.readFile).not.toHaveBeenCalled(); + expect(fs.default.writeFile).not.toHaveBeenCalled(); + }); + }); + describe('updateIndexHtmlTitle', () => { it('replaces the stock with the project name', async () => { const targetPath = '/test/project'; @@ -1059,27 +1120,28 @@ describe('ProjectCreator', () => { it('should return correct icon props for FontAwesome', () => { const pc = projectCreator as unknown; - expect(pc.getIconProps('fontawesome', 'rocket')).toBe( - 'name="rocket" variant="solid"' - ); - expect(pc.getIconProps('fontawesome', 'book')).toBe( - 'name="book" variant="solid"' - ); - expect(pc.getIconProps('fontawesome', 'code')).toBe( - 'name="code" variant="solid"' - ); + expect(pc.getIconProps('fontawesome', 'rocket')).toEqual([ + "name: 'rocket'", + "variant: 'solid'", + "'aria-hidden': 'true'", + ]); + expect(pc.getIconProps('fontawesome', 'book')[0]).toBe("name: 'book'"); + expect(pc.getIconProps('fontawesome', 'code')[0]).toBe("name: 'code'"); }); it('should return correct icon props for other libraries', () => { const pc = projectCreator as unknown; - expect(pc.getIconProps('mdi', 'rocket')).toBe('name="rocket-launch"'); - expect(pc.getIconProps('ionicons', 'book')).toBe('name="book"'); - expect(pc.getIconProps('material-icons', 'code')).toBe('name="code"'); + expect(pc.getIconProps('mdi', 'rocket')).toEqual([ + "name: 'rocket-launch'", + "'aria-hidden': 'true'", + ]); + expect(pc.getIconProps('ionicons', 'book')[0]).toBe("name: 'book'"); + expect(pc.getIconProps('material-icons', 'code')[0]).toBe("name: 'code'"); }); - it('should return empty string when no icon name found', () => { + it('should return no props when no icon name found', () => { const pc = projectCreator as unknown; - expect(pc.getIconProps('unknown', 'rocket')).toBe(''); + expect(pc.getIconProps('unknown', 'rocket')).toEqual([]); }); }); diff --git a/create-bestax/src/__tests__/templates.test.ts b/create-bestax/src/__tests__/templates.test.ts new file mode 100644 index 000000000..4cdcc7c09 --- /dev/null +++ b/create-bestax/src/__tests__/templates.test.ts @@ -0,0 +1,439 @@ +// The scaffolder edits the templates with string patterns, so a test against +// a hand-written stand-in proves the pattern and not that it still matches the +// file it ships with. Everything here runs against the real templates, copied +// to a temp directory, with the real filesystem. +import { + describe, + it, + expect, + jest, + beforeEach, + afterEach, +} from '@jest/globals'; +import fs from 'fs-extra'; +import os from 'os'; +import path from 'path'; +import { createRequire } from 'module'; +import { fileURLToPath } from 'url'; +import ts from 'typescript'; + +const { ProjectCreator } = await import('../project-creator.js'); +const { + BULMA_FLAVORS, + CSS_ORDER_COMMENT, + ICON_LIBRARIES, + NO_HELPERS_STARTER_CLASSES, +} = await import('../constants.js'); + +const testDir = path.dirname(fileURLToPath(import.meta.url)); +const templatesDir = path.resolve(testDir, '../../templates'); +const TEMPLATES = ['vite', 'vite-ts'] as const; +const appFile = (template: string) => + template === 'vite-ts' ? 'App.tsx' : 'App.jsx'; +const mainFile = (template: string) => + template === 'vite-ts' ? 'main.tsx' : 'main.jsx'; + +// The helper props are the ones useBulmaClasses takes out of a component's +// props and turns into classes, viewport variants such as `displayTablet` +// included. They are read from its source rather than listed here, so a +// helper added to the library is one this gate already knows. +function helperPropsOfUseBulmaClasses(): string[] { + const file = path.resolve( + testDir, + '../../../bulma-ui/src/helpers/useBulmaClasses.tsx' + ); + const source = ts.createSourceFile( + file, + fs.readFileSync(file, 'utf8'), + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TSX + ); + const names: string[] = []; + const visit = (node: ts.Node) => { + if ( + ts.isVariableDeclaration(node) && + ts.isObjectBindingPattern(node.name) && + node.initializer && + ts.isIdentifier(node.initializer) && + node.initializer.text === 'props' + ) { + for (const element of node.name.elements) { + if (element.dotDotDotToken) continue; + names.push((element.propertyName ?? element.name).getText(source)); + } + } + ts.forEachChild(node, visit); + }; + visit(source); + return names; +} + +// `color` is left out: on Button and Notification it is the component's own +// modifier, which every flavor styles. `textColor` and `bgColor` are the +// names some components give the helper's `color` and `backgroundColor`. +const HELPER_PROPS = [ + ...helperPropsOfUseBulmaClasses().filter(name => name !== 'color'), + 'textColor', + 'bgColor', +]; + +// The helper props a JSX file writes. It parses the file rather than matching +// text, so a boolean prop written bare (`<Box relative>`) counts as much as one +// with a value, and the same word in a sentence or a comment does not. +function helperPropsIn(code: string): string[] { + const source = ts.createSourceFile( + 'App.tsx', + code, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TSX + ); + const found: string[] = []; + const visit = (node: ts.Node) => { + if (ts.isJsxAttribute(node)) { + const name = node.name.getText(source); + if (HELPER_PROPS.includes(name)) found.push(name); + } + ts.forEachChild(node, visit); + }; + visit(source); + return found; +} + +describe('the no-helpers gate', () => { + // If the hook stopped destructuring its props in one place, the read above + // would come back short and the gate would pass on helpers it can't see. + it('reads every helper prop from useBulmaClasses, viewport variants included', () => { + expect(HELPER_PROPS).toEqual( + expect.arrayContaining([ + 'mt', + 'textAlign', + 'display', + 'displayTablet', + 'displayTabletOnly', + 'textAlignMobile', + 'visibilityDesktopOnly', + 'overflowX', + 'overflowY', + 'flexGrow', + ]) + ); + }); + + it('finds a helper prop written bare, and nothing outside an attribute', () => { + expect(helperPropsIn('<Box displayTablet="flex" />')).toEqual([ + 'displayTablet', + ]); + expect(helperPropsIn('<Box relative>x</Box>')).toEqual(['relative']); + expect(helperPropsIn('<Box\n clearfix\n/>')).toEqual(['clearfix']); + expect(helperPropsIn('<Box mt={size} />')).toEqual(['mt']); + expect( + helperPropsIn( + '<Button color="info">\n {/* a gap or display */} relative gap display\n</Button>' + ) + ).toEqual([]); + }); +}); + +let workDir: string; +let projectCreator: InstanceType<typeof ProjectCreator>; + +beforeEach(async () => { + workDir = await fs.mkdtemp(path.join(os.tmpdir(), 'create-bestax-tpl-')); + projectCreator = new ProjectCreator(templatesDir); + jest.spyOn(console, 'log').mockImplementation(() => {}); +}); + +afterEach(async () => { + jest.restoreAllMocks(); + await fs.remove(workDir); +}); + +async function scaffold(template: string): Promise<string> { + const target = path.join(workDir, template); + await projectCreator.copyTemplate(template, target); + return target; +} + +describe('vite-ts template TypeScript projects', () => { + // `tsc -b` writes every referenced project's output unless it is told not + // to, and a vite.config.js written next to vite.config.ts is the file Vite + // then loads (#947). + // The same walk has to reach both halves of the build: `vite build` checks + // no types, so a graph that stopped reaching the project for `src/` would + // build green while type-checking nothing. + it('type-checks src/ and vite.config.ts, and emits nothing from either', () => { + const templateDir = path.join(templatesDir, 'vite-ts'); + const seen = new Set<string>(); + const checked: string[] = []; + const visit = (configPath: string) => { + if (seen.has(configPath)) return; + seen.add(configPath); + const parsed = ts.getParsedCommandLineOfConfigFile( + configPath, + {}, + { + ...ts.sys, + onUnRecoverableConfigFileDiagnostic: d => { + throw new Error(String(d.messageText)); + }, + } + ); + if (!parsed) throw new Error(`could not parse ${configPath}`); + if (parsed.fileNames.length > 0) { + expect({ + config: path.basename(configPath), + noEmit: parsed.options.noEmit, + }).toEqual({ + config: path.basename(configPath), + noEmit: true, + }); + checked.push(...parsed.fileNames); + } + for (const ref of parsed.projectReferences ?? []) { + visit(ts.resolveProjectReferencePath(ref)); + } + }; + visit(path.join(templateDir, 'tsconfig.json')); + + const src = path.join(templateDir, 'src') + path.sep; + expect(checked.some(file => file.startsWith(src))).toBe(true); + expect(checked).toContain(path.join(templateDir, 'vite.config.ts')); + }); +}); + +describe.each(TEMPLATES)( + '%s template under a flavor without helpers', + template => { + const noHelperFlavors = BULMA_FLAVORS.filter(f => f.noHelpers).map( + f => f.name + ); + + it.each(noHelperFlavors)( + 'replaces every helper prop in the starter (%s)', + async flavor => { + const target = await scaffold(template); + await projectCreator.setupBulmaFlavor(target, flavor, template); + + const app = await fs.readFile( + path.join(target, 'src', appFile(template)), + 'utf8' + ); + const css = await fs.readFile( + path.join(target, 'src', 'App.css'), + 'utf8' + ); + + expect(helperPropsIn(app)).toEqual([]); + for (const { className, declaration } of NO_HELPERS_STARTER_CLASSES) { + expect(app).toContain(`className="${className}"`); + expect(css).toContain(`.${className} {\n ${declaration}\n}`); + } + } + ); + + // The icon step runs after the swap above and writes into the same App, so + // a helper prop it adds (the old card icons' `mr="2"`) renders nothing + // here and no row in the table can replace it. Scan the App a scaffold + // ends with, after both steps, for every icon library. + it.each( + noHelperFlavors.flatMap(flavor => + ICON_LIBRARIES.map(lib => [flavor, lib.name] as const) + ) + )( + 'leaves no helper prop once the icon step has run too (%s, %s)', + async (flavor, iconLibrary) => { + const target = await scaffold(template); + await projectCreator.setupBulmaFlavor(target, flavor, template); + await projectCreator.setupIconLibrary(target, iconLibrary, template); + + const app = await fs.readFile( + path.join(target, 'src', appFile(template)), + 'utf8' + ); + expect(helperPropsIn(app)).toEqual([]); + } + ); + + it('leaves the helper props alone in a flavor that has them', async () => { + const target = await scaffold(template); + await projectCreator.setupBulmaFlavor(target, 'complete', template); + + const app = await fs.readFile( + path.join(target, 'src', appFile(template)), + 'utf8' + ); + expect(helperPropsIn(app)).not.toEqual([]); + for (const { prop, className } of NO_HELPERS_STARTER_CLASSES) { + expect(app).toContain(prop); + expect(app).not.toContain(`className="${className}"`); + } + }); + } +); + +describe.each(TEMPLATES)('%s template icon setup', template => { + const libraries = ICON_LIBRARIES.filter(lib => lib.name !== 'none'); + + it.each(libraries.map(lib => lib.name))( + 'titles every starter card with a decorative IconText (%s)', + async iconLibrary => { + const target = await scaffold(template); + await projectCreator.setupIconLibrary(target, iconLibrary, template); + + const app = await fs.readFile( + path.join(target, 'src', appFile(template)), + 'utf8' + ); + expect(app).toMatch( + /import\s*\{[^}]*\bIconText,[^}]*\}\s*from '@allxsmith\/bestax-bulma'/ + ); + for (const title of ['Quick Start', 'Documentation', 'Examples']) { + expect(app).toMatch( + new RegExp( + `<Card\\.Header\\.Title[^>]*>\\s*<IconText\\s+iconProps=\\{\\{[^}]*'aria-hidden': 'true',[^}]*\\}\\}\\s*>\\s*${title}\\s*</IconText>` + ) + ); + } + } + ); + + // project-creator.test.ts covers every flavor and icon library for #946 on + // an in-memory filesystem. This runs the same order end to end on disk, + // where the flavor step also rewrites the starter under no-helpers, so the + // template's entry file must still give the icon import a place to go. + it.each(BULMA_FLAVORS.map(f => f.name))( + 'loads the icon stylesheet after the %s flavor stylesheet', + async flavor => { + const target = await scaffold(template); + await projectCreator.setupBulmaFlavor(target, flavor, template); + await projectCreator.setupIconLibrary(target, 'fontawesome', template); + + const main = await fs.readFile( + path.join(target, 'src', mainFile(template)), + 'utf8' + ); + const flavorImport = BULMA_FLAVORS.find( + f => f.name === flavor + )!.importStatement; + const iconImport = ICON_LIBRARIES.find( + lib => lib.name === 'fontawesome' + )!.importStatement!; + expect(main).toContain(iconImport); + expect(main.indexOf(iconImport)).toBeGreaterThan( + main.indexOf(flavorImport) + ); + } + ); +}); + +// The entry file imports the stylesheets ahead of App, and the no-helpers +// classes in App.css depend on that order, so the comment saying why has to +// survive the flavor step rewriting the import under it. +describe.each(TEMPLATES)('%s template entry file', template => { + it('explains the stylesheet order where the scaffolder will look for it', async () => { + const main = await fs.readFile( + path.join(templatesDir, template, 'src', mainFile(template)), + 'utf8' + ); + expect(main).toContain( + `${CSS_ORDER_COMMENT}\nimport '@allxsmith/bestax-bulma/bestax.css';` + ); + }); + + it.each(['complete', 'no-helpers'])( + 'keeps that comment above the %s flavor stylesheet', + async flavor => { + const target = await scaffold(template); + await projectCreator.setupBulmaFlavor(target, flavor, template); + await projectCreator.setupIconLibrary(target, 'fontawesome', template); + + const main = await fs.readFile( + path.join(target, 'src', mainFile(template)), + 'utf8' + ); + const flavorImport = BULMA_FLAVORS.find( + f => f.name === flavor + )!.importStatement; + expect(main).toContain(`${CSS_ORDER_COMMENT}\n${flavorImport}`); + expect(main.split(CSS_ORDER_COMMENT)).toHaveLength(2); + expect(main.indexOf("import './index.css';")).toBeGreaterThan( + main.indexOf(flavorImport) + ); + expect(main.indexOf('import App from')).toBeGreaterThan( + main.indexOf("import './index.css';") + ); + } + ); +}); + +describe.each(TEMPLATES)('%s template README', template => { + const originalAgent = process.env.npm_config_user_agent; + afterEach(() => { + if (originalAgent === undefined) delete process.env.npm_config_user_agent; + else process.env.npm_config_user_agent = originalAgent; + }); + + it.each(['npm', 'yarn', 'bun', 'pnpm'])( + 'gives its commands for the package manager that ran the CLI (%s)', + async pm => { + process.env.npm_config_user_agent = `${pm}/1.0.0 node/v22 darwin arm64`; + const target = await scaffold(template); + await projectCreator.updateReadme(target, 'none'); + + const readme = await fs.readFile(path.join(target, 'README.md'), 'utf8'); + expect(readme).toContain(`${pm} install\n${pm} run dev`); + for (const script of ['dev', 'build', 'preview', 'lint']) { + expect(readme).toContain(`\`${pm} run ${script}\``); + } + if (pm !== 'pnpm') expect(readme).not.toMatch(/\bpnpm\b/); + } + ); + + it('names the icon library the user picked, and none when they picked none', async () => { + const withIcons = await scaffold(template); + await projectCreator.updateReadme(withIcons, 'mdi'); + const readme = await fs.readFile(path.join(withIcons, 'README.md'), 'utf8'); + expect(readme).toContain('- 🎯 Material Design Icons icons'); + + await fs.remove(withIcons); + const without = await scaffold(template); + await projectCreator.updateReadme(without, 'none'); + const plain = await fs.readFile(path.join(without, 'README.md'), 'utf8'); + expect(plain).not.toMatch(/icons/i); + }); +}); + +describe('icon library versions', () => { + // A 0.x caret holds the minor, so a pin newer than bestax-bulma's peer + // range makes npm refuse the scaffold's install outright (ERESOLVE), and one + // older than its newest arm leaves the app a minor behind for nothing. + const require = createRequire(import.meta.url); + const bulmaUiPkg = require('../../../bulma-ui/package.json') as { + peerDependencies: Record<string, string>; + }; + + it.each( + ICON_LIBRARIES.filter(lib => lib.packageName).map(lib => [ + lib.packageName!, + lib.packageVersion!, + ]) + )('pins %s within the newest arm of the library peer range', (pkg, pin) => { + const range = bulmaUiPkg.peerDependencies[pkg]; + expect(range).toBeDefined(); + const newestArm = range + .split('||') + .map(arm => arm.trim()) + .sort((a, b) => + a + .replace(/^\^/, '') + .localeCompare(b.replace(/^\^/, ''), undefined, { numeric: true }) + ) + .pop()!; + const [pinMajor, pinMinor] = pin.replace(/^\^/, '').split('.'); + const [armMajor, armMinor] = newestArm.replace(/^\^/, '').split('.'); + expect(pinMajor).toBe(armMajor); + if (armMajor === '0') expect(pinMinor).toBe(armMinor); + }); +}); diff --git a/create-bestax/src/constants.ts b/create-bestax/src/constants.ts index 4e000dbda..994cb3dd8 100644 --- a/create-bestax/src/constants.ts +++ b/create-bestax/src/constants.ts @@ -129,6 +129,38 @@ export interface ClaudeMdOptions { iconLibrary: string; } +// The house style opens with the inline-style table for a flavor that has the +// helper classes, and with the named-class rule for one that does not. +const HELPER_HOUSE_STYLE = `**Never inline \`style={{}}\`** — the components accept helper props that cover the common +cases. Before writing \`style\`, translate each declaration with this table: + +| Inline style you're about to write | Helper props instead | +| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | +| \`marginTop: '1rem'\` (any margin/padding) | \`mt="4"\` — \`m\`/\`mt\`/\`mx\`/\`p\`/\`py\`/… scale: \`1\`=0.25rem, \`2\`=0.5rem, \`3\`=0.75rem, \`4\`=1rem, \`5\`=1.5rem, \`6\`=3rem (nearest step) | +| \`textAlign: 'center'\` | \`textAlign="centered"\` (also \`left\`, \`right\`, \`justified\`) | +| \`color: '#…'\` | \`textColor\` with the nearest Bulma color: \`primary\`, \`link\`, \`info\`, \`success\`, \`warning\`, \`danger\`, \`white\`, \`black\`, \`grey\` (+ \`grey-light\`, \`grey-dark\`, …) | +| \`backgroundColor: '#…'\` | \`bgColor\` (same palette) | +| \`fontSize: …\` | \`textSize="1"\`…\`"7"\` (\`1\` largest) — for headings use \`Title\`/\`SubTitle\` \`size\` | +| \`fontWeight: …\` | \`textWeight\`: \`light\`, \`normal\`, \`medium\`, \`semibold\`, \`bold\` | +| \`textTransform\`, italics | \`textTransform\`: \`uppercase\`, \`lowercase\`, \`capitalized\`, \`italic\` | +| \`display: 'flex'\` + flex properties | same-named props: \`display="flex"\`, \`flexDirection\`, \`justifyContent\`, \`alignItems\`, \`flexWrap\` | +| \`height: '100%'\` on a flex child | \`flexGrow="1"\` | +| \`display: 'none'\` | \`visibility="hidden"\`, or responsive \`display*\` props (\`displayMobile\`, \`displayTablet\`, …) | +| \`gap: '1rem'\` (flex or grid) | \`gap="2"\`, or one axis: \`columnGap\`, \`rowGap\`; gap scale: \`1\`=0.5rem, \`2\`=1rem, \`4\`=2rem, half steps (\`"1.5"\`) | + +- Spacing, typography, and flex helpers are on every component; \`textColor\`/\`bgColor\` are + on the content components (\`Box\`, \`Block\`, \`Title\`, \`Content\`, \`Hero\`, \`Card\`, …) — the + ones with a semantic \`color\` variant (\`Tag\`, \`Tabs\`, \`Panel\`) take \`color\` instead. + \`Notification\` is the mixed case: it takes \`textColor\`, but its background comes from + the semantic \`color\` prop, not \`bgColor\`. +- No helper matches (e.g. \`maxWidth\`, a one-off gradient)? Add a named class to + \`src/App.css\` and pass it via \`className\` — still never inline \`style\`.`; + +const NO_HELPERS_HOUSE_STYLE = `**Never inline \`style={{}}\`**, and don't reach for helper props in its place either: +this app's flavor leaves out Bulma's helper classes, so \`mt="4"\`, \`textAlign="centered"\` and \`flexGrow="1"\` +render nothing. Write a named class in \`src/App.css\`, which loads after Bulma's CSS, and pass it via \`className\`. +`; + export const CLAUDE_MD = ( projectName: string, { bulmaFlavor, iconLibrary }: ClaudeMdOptions @@ -146,6 +178,15 @@ export const CLAUDE_MD = ( `\`<ConfigProvider classPrefix="bestax-">\` — custom CSS selectors must match the prefix.` ); } + if (flavor?.noHelpers) { + setupLines.push( + `- This flavor leaves out Bulma's helper classes, so **helper props that add those classes render nothing**: ` + + `\`mt\`, \`gap\`, \`textAlign\`, \`textColor\`, \`display\` and \`flexGrow\` still add ` + + `class names, but no rule matches them. \`skeleton\` still works, because the flavor keeps ` + + `Bulma's skeleton styles. The starter page lays itself out with named classes in ` + + `\`src/App.css\` instead.` + ); + } const providerIconValue = CONFIG_PROVIDER_ICON_VALUES[iconLibrary]; setupLines.push( iconLibrary === 'none' @@ -155,8 +196,18 @@ export const CLAUDE_MD = ( (providerIconValue ? ` The app is already wrapped in \`<ConfigProvider iconLibrary="${providerIconValue}">\` ` + `— extend that provider rather than adding a second one.` - : '') + : '') + + (icon?.setupInstructions ? ` ${icon.setupInstructions}` : '') ); + const houseStyleOpening = flavor?.noHelpers + ? NO_HELPERS_HOUSE_STYLE + : HELPER_HOUSE_STYLE; + // The wrapper elements are only a way out where their helper props render. + const utilityClassRule = flavor?.noHelpers + ? `- Don't hand-write Bulma utility classes either: this flavor doesn't ship them, so a + \`has-text-…\` class renders nothing, and the named class above is what works.` + : `- Don't hand-write Bulma utility classes either — bare text/markup has wrapper elements that + take the same helper props: \`Span\`, \`Paragraph\`, \`Strong\`, not \`<span className="has-text-…">\`.`; return `# ${projectName} This app is built with [\`@allxsmith/bestax-bulma\`](https://bestax.io) — React components for @@ -168,36 +219,12 @@ ${setupLines.join('\n')} ## House style -**Never inline \`style={{}}\`** — the components accept helper props that cover the common -cases. Before writing \`style\`, translate each declaration with this table: - -| Inline style you're about to write | Helper props instead | -| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | -| \`marginTop: '1rem'\` (any margin/padding) | \`mt="4"\` — \`m\`/\`mt\`/\`mx\`/\`p\`/\`py\`/… scale: \`1\`=0.25rem, \`2\`=0.5rem, \`3\`=0.75rem, \`4\`=1rem, \`5\`=1.5rem, \`6\`=3rem (nearest step) | -| \`textAlign: 'center'\` | \`textAlign="centered"\` (also \`left\`, \`right\`, \`justified\`) | -| \`color: '#…'\` | \`textColor\` with the nearest Bulma color: \`primary\`, \`link\`, \`info\`, \`success\`, \`warning\`, \`danger\`, \`white\`, \`black\`, \`grey\` (+ \`grey-light\`, \`grey-dark\`, …) | -| \`backgroundColor: '#…'\` | \`bgColor\` (same palette) | -| \`fontSize: …\` | \`textSize="1"\`…\`"7"\` (\`1\` largest) — for headings use \`Title\`/\`SubTitle\` \`size\` | -| \`fontWeight: …\` | \`textWeight\`: \`light\`, \`normal\`, \`medium\`, \`semibold\`, \`bold\` | -| \`textTransform\`, italics | \`textTransform\`: \`uppercase\`, \`lowercase\`, \`capitalized\`, \`italic\` | -| \`display: 'flex'\` + flex properties | same-named props: \`display="flex"\`, \`flexDirection\`, \`justifyContent\`, \`alignItems\`, \`flexWrap\` | -| \`height: '100%'\` on a flex child | \`flexGrow="1"\` | -| \`display: 'none'\` | \`visibility="hidden"\`, or responsive \`display*\` props (\`displayMobile\`, \`displayTablet\`, …) | -| \`gap: '1rem'\` (flex or grid) | \`gap="2"\`, or one axis: \`columnGap\`, \`rowGap\`; gap scale: \`1\`=0.5rem, \`2\`=1rem, \`4\`=2rem, half steps (\`"1.5"\`) | - -- Spacing, typography, and flex helpers are on every component; \`textColor\`/\`bgColor\` are - on the content components (\`Box\`, \`Block\`, \`Title\`, \`Content\`, \`Hero\`, \`Card\`, …) — the - ones with a semantic \`color\` variant (\`Tag\`, \`Tabs\`, \`Panel\`) take \`color\` instead. - \`Notification\` is the mixed case: it takes \`textColor\`, but its background comes from - the semantic \`color\` prop, not \`bgColor\`. -- No helper matches (e.g. \`maxWidth\`, a one-off gradient)? Add a named class to - \`src/App.css\` and pass it via \`className\` — still never inline \`style\`. -- Don't hand-write Bulma utility classes either — bare text/markup has wrapper elements that - take the same helper props: \`Span\`, \`Paragraph\`, \`Strong\`, not \`<span className="has-text-…">\`. +${houseStyleOpening} +${utilityClassRule} The one exception: companion classes Bulma requires on \`<html>\`/\`<body>\` (e.g. \`has-navbar-fixed-top\` with \`Navbar fixed="top"\`) are hand-added in \`index.html\` — no component renders those elements. -- Compound sub-parts (\`Card.*\`, \`Modal.*\`, \`Tabs.*\`, \`Message.*\`) take \`className\` + HTML +- Compound sub-parts (\`Modal.*\`, \`Tabs.*\`, \`Message.*\`) take \`className\` + HTML attributes and their own few props — no Bulma helper props, no \`as\`/\`href\`: nest a \`Link\`/\`Span\` inside instead. \`Tabs.Tab\` and \`Tabs.Content.Item\` each require \`index={i}\`, and \`Tabs.Tab\` has built-in \`icon\`/\`disabled\` props — no nested \`Icon\` needed. @@ -275,6 +302,8 @@ export interface IconLibrary { // would make scaffolded installs non-reproducible. packageVersion?: string; importStatement?: string; + // Anything about the library's setup an agent needs beyond its name and + // provider value, appended to the icon line of the generated CLAUDE.md. setupInstructions?: string; } @@ -313,15 +342,32 @@ export const ICON_LIBRARIES: IconLibrary[] = [ color: chalk.yellow, packageName: 'material-icons', packageVersion: '^1.13.14', - importStatement: "import 'material-icons';", + // The package's bare import puts the filled, outlined, round, sharp and + // two-tone fonts into the build. Filled is the style Icon renders by + // default, so it is the only one imported. + importStatement: "import 'material-icons/iconfont/filled.css';", + setupInstructions: + 'Only the filled style is loaded (`material-icons/iconfont/filled.css`, the variant `<Icon>` ' + + 'uses by default). Import `outlined.css`, `round.css`, `sharp.css` or `two-tone.css` from ' + + '`material-icons/iconfont/` in `src/main.*` before using those variants.', }, { name: 'material-symbols', display: 'Material Symbols', color: chalk.magenta, packageName: 'material-symbols', - packageVersion: '^0.45.2', - importStatement: "import 'material-symbols';", + // 0.x, so the caret holds the minor: this follows the newest minor + // bestax-bulma's material-symbols peer range admits, not the newest + // published one, or npm refuses the install as a peer conflict. + packageVersion: '^0.46.0', + // The package's bare import puts the outlined, rounded and sharp fonts + // into the build. Outlined is the style Icon renders by default, so it is + // the only one imported. + importStatement: "import 'material-symbols/outlined.css';", + setupInstructions: + 'Only the outlined style is loaded (`material-symbols/outlined.css`, the variant `<Icon>` ' + + 'uses by default). Import `material-symbols/rounded.css` or `sharp.css` in `src/main.*` ' + + 'before using `variant="rounded"` or `"sharp"`.', }, ]; @@ -332,6 +378,9 @@ export interface BulmaFlavor { color: typeof chalk.yellow; importStatement: string; needsPrefix?: boolean; + // The flavor's CSS leaves out Bulma's helper classes, so a helper prop that + // adds one (`mt`, `textAlign`, `flexGrow`, …) renders a class nothing styles. + noHelpers?: boolean; } export const BULMA_FLAVORS: BulmaFlavor[] = [ @@ -360,6 +409,7 @@ export const BULMA_FLAVORS: BulmaFlavor[] = [ color: chalk.yellow, importStatement: "import '@allxsmith/bestax-bulma/versions/bestax-no-helpers.css';", + noHelpers: true, }, { name: 'no-helpers-prefixed', @@ -369,6 +419,7 @@ export const BULMA_FLAVORS: BulmaFlavor[] = [ importStatement: "import '@allxsmith/bestax-bulma/versions/bestax-no-helpers-prefixed.css';", needsPrefix: true, + noHelpers: true, }, { name: 'no-dark-mode', @@ -379,3 +430,50 @@ export const BULMA_FLAVORS: BulmaFlavor[] = [ "import '@allxsmith/bestax-bulma/versions/bestax-no-dark-mode.css';", }, ]; + +// Sits above the stylesheet import in the templates' entry files, and the +// scaffolder writes it back when it swaps in a flavor's stylesheet, so the +// reason for the import order survives. The no-helpers classes depend on it. +export const CSS_ORDER_COMMENT = + "// Bestax's stylesheet loads before the app's own CSS, so a named class in\n" + + '// src/App.css wins over a Bulma rule of the same weight.'; + +export interface StarterClass { + // The helper prop exactly as the starter App writes it. + prop: string; + // The named class that replaces it. + className: string; + // That class's rule body in src/App.css. + declaration: string; +} + +// Every helper prop the starter App uses, with the named class that stands in +// for it under a `noHelpers` flavor, where the prop would render nothing. The +// scaffolder swaps each prop for its class and writes the rules into +// src/App.css, so the starter keeps its layout and shows that flavor's way of +// styling. A helper prop added to the starter needs a row here. +export const NO_HELPERS_STARTER_CLASSES: StarterClass[] = [ + { + prop: 'textAlign="centered"', + className: 'page-title', + declaration: 'text-align: center;', + }, + { + prop: 'display="flex"', + className: 'card-column', + declaration: 'display: flex;', + }, + { + prop: 'flexGrow="1"', + className: 'card-fill', + declaration: 'flex-grow: 1;', + }, +]; + +export const NO_HELPERS_APP_CSS = + '\n/* This flavor of Bulma leaves out the helper classes, so helper props such\n' + + ' as textAlign and flexGrow render nothing here. Named classes like these do\n' + + ' the job instead: write one, then pass it with className. */\n' + + NO_HELPERS_STARTER_CLASSES.map( + ({ className, declaration }) => `.${className} {\n ${declaration}\n}\n` + ).join('\n'); diff --git a/create-bestax/src/project-creator.ts b/create-bestax/src/project-creator.ts index 477502898..ac5c64796 100644 --- a/create-bestax/src/project-creator.ts +++ b/create-bestax/src/project-creator.ts @@ -36,8 +36,12 @@ import { CLAUDE_MD, LAUNCH_JSON, CONFIG_PROVIDER_ICON_VALUES, + NO_HELPERS_STARTER_CLASSES, + NO_HELPERS_APP_CSS, + CSS_ORDER_COMMENT, type ClaudeMdOptions, } from './constants.js'; +import { detectPackageManager } from './package-manager.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -184,6 +188,30 @@ export class ProjectCreator { } } + /** + * The template README is written with pnpm commands and lists no icon + * library. Rewrite the commands for the package manager that ran the CLI + * (the one the success message names too) and list the icon library the + * user picked, if any. + */ + async updateReadme(targetPath: string, iconLibrary: string): Promise<void> { + const readmePath = path.join(targetPath, 'README.md'); + if (!fs.existsSync(readmePath)) return; + + const pm = detectPackageManager(); + let readme = await fs.readFile(readmePath, 'utf8'); + readme = readme.replace(/\bpnpm (install|run)\b/g, `${pm} $1`); + + const library = ICON_LIBRARIES.find(lib => lib.name === iconLibrary); + if (library && iconLibrary !== 'none') { + readme = readme.replace( + /^(- .*bestax-bulma component library)$/m, + `$1\n- 🎯 ${library.display} icons` + ); + } + await fs.writeFile(readmePath, readme); + } + async setupSkills( targetPath: string, projectName: string, @@ -229,15 +257,17 @@ export class ProjectCreator { if (fs.existsSync(mainFilePath)) { let content = await fs.readFile(mainFilePath, 'utf8'); - // Replace the default bestax CSS import with the selected flavor + // Replace the default bestax CSS import, and the comment block directly + // above it, with the selected flavor's import under the comment that + // says why the stylesheets load before the app's own CSS. const bestaxImportRegex = - /\/\/.*\n\s*import\s+['"]@allxsmith\/bestax-bulma\/bestax\.css['"]\s*;?/; + /(?:[ \t]*\/\/[^\n]*\n)+[ \t]*import\s+['"]@allxsmith\/bestax-bulma\/bestax\.css['"]\s*;?/; const bestaxImportSimpleRegex = /import\s+['"]@allxsmith\/bestax-bulma\/bestax\.css['"]\s*;?/; if (bestaxImportRegex.test(content)) { content = content.replace( bestaxImportRegex, - '// Import CSS\n' + flavor.importStatement + CSS_ORDER_COMMENT + '\n' + flavor.importStatement ); } else if (bestaxImportSimpleRegex.test(content)) { content = content.replace( @@ -262,6 +292,38 @@ export class ProjectCreator { await fs.writeFile(mainFilePath, content); } + + if (flavor.noHelpers) { + await this.replaceStarterHelperProps(targetPath, template); + } + } + + /** + * A flavor without Bulma's helper classes leaves the starter's helper props + * rendering nothing, so its title loses its centering and its cards their + * equal heights. Swap each prop for the named class in + * NO_HELPERS_STARTER_CLASSES and append the classes' rules to src/App.css. + */ + async replaceStarterHelperProps( + targetPath: string, + template: string + ): Promise<void> { + const appFileName = template.includes('-ts') ? 'App.tsx' : 'App.jsx'; + const appFilePath = path.join(targetPath, 'src', appFileName); + const appCssPath = path.join(targetPath, 'src', 'App.css'); + if (!fs.existsSync(appFilePath) || !fs.existsSync(appCssPath)) return; + + let appContent = await fs.readFile(appFilePath, 'utf8'); + for (const { prop, className } of NO_HELPERS_STARTER_CLASSES) { + appContent = appContent.split(prop).join(`className="${className}"`); + } + await fs.writeFile(appFilePath, appContent); + + const css = await fs.readFile(appCssPath, 'utf8'); + await fs.writeFile( + appCssPath, + css.replace(/\n*$/, '\n') + NO_HELPERS_APP_CSS + ); } private getIconName( @@ -291,16 +353,23 @@ export class ProjectCreator { return iconMappings[iconLibrary]?.[iconType] || ''; } + /** + * The `iconProps` entries for a starter card icon. Decorative, since the + * card title beside it says the same thing, so it is hidden from screen + * readers rather than announced as "icon". + */ private getIconProps( iconLibrary: string, iconType: 'rocket' | 'book' | 'code' - ): string { + ): string[] { const iconName = this.getIconName(iconLibrary, iconType); - if (!iconName) return ''; + if (!iconName) return []; - return iconLibrary === 'fontawesome' - ? `name="${iconName}" variant="solid"` - : `name="${iconName}"`; + return [ + `name: '${iconName}'`, + ...(iconLibrary === 'fontawesome' ? [`variant: 'solid'`] : []), + `'aria-hidden': 'true'`, + ]; } async setupIconLibrary( @@ -394,7 +463,10 @@ export class ProjectCreator { } } - // Now add Icon components to App.jsx/App.tsx (for all icon libraries) + // Now add an icon to each card title in App.jsx/App.tsx (for all icon + // libraries). IconText spaces the icon from its text with Bulma's own + // .icon-text rule rather than a margin helper, so the gap holds in the + // no-helpers flavors too. const isTypeScript = template.includes('-ts'); const appFileName = isTypeScript ? 'App.tsx' : 'App.jsx'; const appFilePath = path.join(targetPath, 'src', appFileName); @@ -402,61 +474,52 @@ export class ProjectCreator { if (fs.existsSync(appFilePath)) { let appContent = await fs.readFile(appFilePath, 'utf8'); - // Add Icon import to the bestax-bulma imports + // Add IconText to the bestax-bulma imports const bulmaImportRegex = /(import\s+\{[\s\S]*?)(}\s+from\s+['"]@allxsmith\/bestax-bulma['"])/; - if (bulmaImportRegex.test(appContent) && !appContent.includes('Icon')) { - // Check if the import ends with a comma or not + if ( + bulmaImportRegex.test(appContent) && + !/\bIconText\b/.test(appContent) + ) { const importMatch = appContent.match(bulmaImportRegex); if (importMatch) { const beforeClosingBrace = importMatch[1]; - // Remove any trailing comma and whitespace, then add Icon properly + // Remove any trailing comma and whitespace, then add IconText properly const cleanedImport = beforeClosingBrace.replace(/,?\s*$/, ''); appContent = appContent.replace( bulmaImportRegex, - cleanedImport + ',\n Icon$2' - ); - } - } - - // Add icon examples in the Cards section - // Find the Quick Start Card and add an icon - const quickStartRegex = - /(Card\.Header\.Title>\s*Quick Start\s*<\/Card\.Header\.Title>)/; - if (quickStartRegex.test(appContent)) { - const iconProps = this.getIconProps(iconLibrary, 'rocket'); - if (iconProps) { - appContent = appContent.replace( - quickStartRegex, - `Card.Header.Title>\n <Icon ${iconProps} size="small" mr="2" />\n Quick Start\n </Card.Header.Title>` + cleanedImport + ',\n IconText,\n$2' ); } } - // Add icon to Documentation Card - const docsRegex = - /(Card\.Header\.Title>\s*Documentation\s*<\/Card\.Header\.Title>)/; - if (docsRegex.test(appContent)) { - const iconProps = this.getIconProps(iconLibrary, 'book'); - if (iconProps) { - appContent = appContent.replace( - docsRegex, - `Card.Header.Title>\n <Icon ${iconProps} size="small" mr="2" />\n Documentation\n </Card.Header.Title>` - ); - } - } - - // Add icon to Examples Card - const examplesRegex = - /(Card\.Header\.Title>\s*Examples\s*<\/Card\.Header\.Title>)/; - if (examplesRegex.test(appContent)) { - const iconProps = this.getIconProps(iconLibrary, 'code'); - if (iconProps) { - appContent = appContent.replace( - examplesRegex, - `Card.Header.Title>\n <Icon ${iconProps} size="small" mr="2" />\n Examples\n </Card.Header.Title>` - ); - } + const cardIcons: Array<['rocket' | 'book' | 'code', string]> = [ + ['rocket', 'Quick Start'], + ['book', 'Documentation'], + ['code', 'Examples'], + ]; + for (const [iconType, title] of cardIcons) { + const iconProps = this.getIconProps(iconLibrary, iconType); + if (iconProps.length === 0) continue; + const titleRegex = new RegExp( + `^([ \\t]*)(<Card\\.Header\\.Title[^>]*>)\\s*${title}\\s*(</Card\\.Header\\.Title>)`, + 'm' + ); + appContent = appContent.replace( + titleRegex, + (_match, indent: string, open: string, close: string) => + [ + `${indent}${open}`, + `${indent} <IconText`, + `${indent} iconProps={{`, + ...iconProps.map(entry => `${indent} ${entry},`), + `${indent} }}`, + `${indent} >`, + `${indent} ${title}`, + `${indent} </IconText>`, + `${indent}${close}`, + ].join('\n') + ); } await fs.writeFile(appFilePath, appContent); @@ -639,6 +702,7 @@ export class ProjectCreator { await this.copyTemplate(template, targetPath); await updatePackageJson(targetPath, packageName); await this.updateIndexHtmlTitle(targetPath, projectName); + await this.updateReadme(targetPath, iconLibrary); await this.setupBulmaFlavor(targetPath, bulmaFlavor, template); await this.setupIconLibrary(targetPath, iconLibrary, template); await this.setupConfigProvider( diff --git a/create-bestax/templates/vite-ts/README.md b/create-bestax/templates/vite-ts/README.md index e85f36a49..ce949b92c 100644 --- a/create-bestax/templates/vite-ts/README.md +++ b/create-bestax/templates/vite-ts/README.md @@ -6,15 +6,15 @@ This template provides a minimal setup to get React working in Vite with TypeScr ```bash pnpm install -pnpm dev +pnpm run dev ``` ## Available Scripts -- `pnpm dev` - Start development server -- `pnpm build` - Build for production -- `pnpm preview` - Preview production build -- `pnpm lint` - Run ESLint +- `pnpm run dev` - Start development server +- `pnpm run build` - Build for production +- `pnpm run preview` - Preview production build +- `pnpm run lint` - Run ESLint ## Features @@ -23,7 +23,6 @@ pnpm dev - 📦 bestax-bulma component library - 📘 TypeScript for type safety - 🔧 ESLint pre-configured -- 🎯 Font Awesome icons included ## Learn More diff --git a/create-bestax/templates/vite-ts/src/App.css b/create-bestax/templates/vite-ts/src/App.css index 1beafa576..d8409818d 100644 --- a/create-bestax/templates/vite-ts/src/App.css +++ b/create-bestax/templates/vite-ts/src/App.css @@ -1 +1,14 @@ -/* App-specific styles - keep minimal, let Bulma handle the styling */ \ No newline at end of file +/* App-specific styles - keep minimal, let Bulma handle the styling */ + +/* A link inside running text needs more than its color to stand out + (WCAG 1.4.1), and no helper prop sets an underline. */ +.text-link { + text-decoration: underline; +} + +/* Bulma spaces a notification from whatever follows it in the same parent, + and each notification here is alone in its status region, so the space + between two showing at once goes between their regions instead. */ +.status-region:not(:empty) + .status-region:not(:empty) { + margin-top: var(--bulma-block-spacing); +} diff --git a/create-bestax/templates/vite-ts/src/App.tsx b/create-bestax/templates/vite-ts/src/App.tsx index bd315aad3..3d279a3b3 100644 --- a/create-bestax/templates/vite-ts/src/App.tsx +++ b/create-bestax/templates/vite-ts/src/App.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useRef, useState } from 'react'; import { Box, Button, @@ -18,127 +18,155 @@ import './App.css'; function App() { const [showNotification, setShowNotification] = useState(false); const [count, setCount] = useState(0); + // Where focus goes when the button holding it goes away (the notification's + // close button) or disables itself (Reset, at zero), so a keyboard user is + // not dropped at the top of the page. + const toggleRef = useRef<HTMLButtonElement>(null); + const countRef = useRef<HTMLButtonElement>(null); return ( - <> - <Container breakpoint="desktop" isMax> - <Section> - <Columns isCentered> - <Column isNarrow display="flex" justifyContent="center"> + <main> + <Section> + <Container breakpoint="desktop" isMax> + <Columns isCentered isMobile isMultiline> + <Column isNarrow> <Image src="/bestax.svg" alt="Bestax" size="128x128" /> </Column> - <Column isNarrow display="flex" justifyContent="center"> + <Column isNarrow> <Image src="/vite.svg" alt="Vite" size="128x128" /> </Column> - <Column isNarrow display="flex" justifyContent="center"> + <Column isNarrow> <Image src="/react.svg" alt="React" size="128x128" /> </Column> </Columns> <Title size="1" textAlign="centered"> Bestax + Vite + React - - - Get Started - - This template includes everything you need to build with - bestax-bulma - - + + Get Started + + This template includes everything you need to build with + bestax-bulma + + - - - - - Quick Start - - - Edit src/App.tsx and save to test HMR updates. - - - + + + + + Quick Start + + + Edit src/App.tsx and save to test HMR updates. + + + - - - - Documentation - - - Visit{' '} - - bestax.io - {' '} - for component docs. - - - + + + + Documentation + + + Visit{' '} + + bestax.io + {' '} + for component docs. + + + - - - - Examples - - - Check the examples below to see components in action. - - - - + + + + Examples + + + Check the examples below to see components in action. + + + + - - Interactive Example + + Interactive Example - - - - + + + + - + - - - + + + - - {showNotification && ( - setShowNotification(false)} - > - Success! Your Vite + Bestax setup is working - perfectly! - - )} + {/* A status region reads out all it holds whenever any of it + changes, so each notification has a region of its own: a + screen reader hears the one that appeared or changed, and + not the other with it. The regions stay mounted while their + content changes: one added along with its content may not + be announced. */} + +
+ {showNotification && ( + { + setShowNotification(false); + toggleRef.current?.focus(); + }} + > + Success! Your Vite + Bestax setup is + working perfectly! + + )} +
- {count > 10 && ( - - You've clicked the button {count} times! - - )} -
-
-
- - +
+ {count > 10 && ( + + You've clicked the button {count} times! + + )} +
+ + +
+ + + ); } diff --git a/create-bestax/templates/vite-ts/src/main.tsx b/create-bestax/templates/vite-ts/src/main.tsx index 8d94d1721..2976638a8 100644 --- a/create-bestax/templates/vite-ts/src/main.tsx +++ b/create-bestax/templates/vite-ts/src/main.tsx @@ -1,10 +1,11 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; -import App from './App.tsx'; -// Import Bestax CSS (Bulma + extras) +// Bestax's stylesheet loads before the app's own CSS, so a named class in +// src/App.css wins over a Bulma rule of the same weight. import '@allxsmith/bestax-bulma/bestax.css'; import './index.css'; +import App from './App.tsx'; createRoot(document.getElementById('root')!).render( diff --git a/create-bestax/templates/vite-ts/tsconfig.app.json b/create-bestax/templates/vite-ts/tsconfig.app.json new file mode 100644 index 000000000..bd272261e --- /dev/null +++ b/create-bestax/templates/vite-ts/tsconfig.app.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} \ No newline at end of file diff --git a/create-bestax/templates/vite-ts/tsconfig.json b/create-bestax/templates/vite-ts/tsconfig.json index eeab595c5..1ffef600d 100644 --- a/create-bestax/templates/vite-ts/tsconfig.json +++ b/create-bestax/templates/vite-ts/tsconfig.json @@ -1,26 +1,7 @@ { - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo", - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["src"], - "references": [{ "path": "./tsconfig.node.json" }] -} \ No newline at end of file + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/create-bestax/templates/vite-ts/tsconfig.node.json b/create-bestax/templates/vite-ts/tsconfig.node.json index b81d2f6ba..38db7a62e 100644 --- a/create-bestax/templates/vite-ts/tsconfig.node.json +++ b/create-bestax/templates/vite-ts/tsconfig.node.json @@ -1,13 +1,13 @@ { "compilerOptions": { - "composite": true, "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", "skipLibCheck": true, "module": "ESNext", "moduleResolution": "bundler", "allowSyntheticDefaultImports": true, + "noEmit": true, "strict": true, "types": ["vite/client"] }, "include": ["vite.config.ts"] -} \ No newline at end of file +} diff --git a/create-bestax/templates/vite/README.md b/create-bestax/templates/vite/README.md index a3b3f2c32..e7566099a 100644 --- a/create-bestax/templates/vite/README.md +++ b/create-bestax/templates/vite/README.md @@ -6,15 +6,15 @@ This template provides a minimal setup to get React working in Vite with HMR and ```bash pnpm install -pnpm dev +pnpm run dev ``` ## Available Scripts -- `pnpm dev` - Start development server -- `pnpm build` - Build for production -- `pnpm preview` - Preview production build -- `pnpm lint` - Run ESLint +- `pnpm run dev` - Start development server +- `pnpm run build` - Build for production +- `pnpm run preview` - Preview production build +- `pnpm run lint` - Run ESLint ## Features @@ -22,7 +22,6 @@ pnpm dev - 🎨 Bulma CSS framework - 📦 bestax-bulma component library - 🔧 ESLint pre-configured -- 🎯 Font Awesome icons included ## Learn More diff --git a/create-bestax/templates/vite/src/App.css b/create-bestax/templates/vite/src/App.css index 1beafa576..d8409818d 100644 --- a/create-bestax/templates/vite/src/App.css +++ b/create-bestax/templates/vite/src/App.css @@ -1 +1,14 @@ -/* App-specific styles - keep minimal, let Bulma handle the styling */ \ No newline at end of file +/* App-specific styles - keep minimal, let Bulma handle the styling */ + +/* A link inside running text needs more than its color to stand out + (WCAG 1.4.1), and no helper prop sets an underline. */ +.text-link { + text-decoration: underline; +} + +/* Bulma spaces a notification from whatever follows it in the same parent, + and each notification here is alone in its status region, so the space + between two showing at once goes between their regions instead. */ +.status-region:not(:empty) + .status-region:not(:empty) { + margin-top: var(--bulma-block-spacing); +} diff --git a/create-bestax/templates/vite/src/App.jsx b/create-bestax/templates/vite/src/App.jsx index 6866f7d15..b5a8cc183 100644 --- a/create-bestax/templates/vite/src/App.jsx +++ b/create-bestax/templates/vite/src/App.jsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useRef, useState } from 'react'; import { Box, Button, @@ -18,127 +18,155 @@ import './App.css'; function App() { const [showNotification, setShowNotification] = useState(false); const [count, setCount] = useState(0); + // Where focus goes when the button holding it goes away (the notification's + // close button) or disables itself (Reset, at zero), so a keyboard user is + // not dropped at the top of the page. + const toggleRef = useRef(null); + const countRef = useRef(null); return ( - <> - -
- - +
+
+ + + Bestax - + Vite - + React Bestax + Vite + React -
- - Get Started - - This template includes everything you need to build with - bestax-bulma - - + + Get Started + + This template includes everything you need to build with + bestax-bulma + + - - - - - Quick Start - - - Edit src/App.jsx and save to test HMR updates. - - - + + + + + Quick Start + + + Edit src/App.jsx and save to test HMR updates. + + + - - - - Documentation - - - Visit{' '} - - bestax.io - {' '} - for component docs. - - - + + + + Documentation + + + Visit{' '} + + bestax.io + {' '} + for component docs. + + + - - - - Examples - - - Check the examples below to see components in action. - - - - + + + + Examples + + + Check the examples below to see components in action. + + + + - - Interactive Example + + Interactive Example - - - - + + + + - + - - - + + + - - {showNotification && ( - setShowNotification(false)} - > - Success! Your Vite + Bestax setup is working - perfectly! - - )} + {/* A status region reads out all it holds whenever any of it + changes, so each notification has a region of its own: a + screen reader hears the one that appeared or changed, and + not the other with it. The regions stay mounted while their + content changes: one added along with its content may not + be announced. */} + +
+ {showNotification && ( + { + setShowNotification(false); + toggleRef.current?.focus(); + }} + > + Success! Your Vite + Bestax setup is + working perfectly! + + )} +
- {count > 10 && ( - - You've clicked the button {count} times! - - )} -
-
-
- - +
+ {count > 10 && ( + + You've clicked the button {count} times! + + )} +
+ + +
+ +
+ ); } diff --git a/create-bestax/templates/vite/src/main.jsx b/create-bestax/templates/vite/src/main.jsx index c013955c0..03ecd5ffb 100644 --- a/create-bestax/templates/vite/src/main.jsx +++ b/create-bestax/templates/vite/src/main.jsx @@ -1,10 +1,11 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; -import App from './App.jsx'; -// Import Bestax CSS (Bulma + extras) +// Bestax's stylesheet loads before the app's own CSS, so a named class in +// src/App.css wins over a Bulma rule of the same weight. import '@allxsmith/bestax-bulma/bestax.css'; import './index.css'; +import App from './App.jsx'; createRoot(document.getElementById('root')).render( diff --git a/skills/bestax-icons/references/icon-libraries.md b/skills/bestax-icons/references/icon-libraries.md index 6e9c4dac3..56042324b 100644 --- a/skills/bestax-icons/references/icon-libraries.md +++ b/skills/bestax-icons/references/icon-libraries.md @@ -67,8 +67,11 @@ Set it once on `ConfigProvider` and omit `library` everywhere else. ## Google Material Icons — `'material-icons'` -- **Setup:** `npm install material-icons` and `import 'material-icons';`. -- **Scaffold flag:** `--icon material-icons`. +- **Setup:** `npm install material-icons` and `import 'material-icons/iconfont/filled.css';`, the + style `Icon` renders by default. Add `outlined.css`, `round.css`, `sharp.css` or `two-tone.css` + from `material-icons/iconfont/` for those variants; the bare `import 'material-icons';` loads + all five fonts. +- **Scaffold flag:** `--icon material-icons` (imports `filled.css` only). - **Names:** snake_case ligature text: `home`, `rocket_launch`, `shopping_cart`. A kebab-case name will not match a ligature and renders as raw text. - **Variants:** `filled` (default) / `outlined` / `round` / `sharp` — note **`round`**, not @@ -77,8 +80,10 @@ Set it once on `ConfigProvider` and omit `library` everywhere else. ## Material Symbols — `'material-symbols'` -- **Setup:** `npm install material-symbols` and `import 'material-symbols';`. -- **Scaffold flag:** `--icon material-symbols`. +- **Setup:** `npm install material-symbols` and `import 'material-symbols/outlined.css';`, the + style `Icon` renders by default. Add `rounded.css` or `sharp.css` for those variants; the bare + `import 'material-symbols';` loads all three fonts. +- **Scaffold flag:** `--icon material-symbols` (imports `outlined.css` only). - **Names:** snake_case ligature text, same as Material Icons: `rocket_launch`. - **Variants:** `outlined` (default) / `rounded` / `sharp` — note **`rounded`** here vs Material Icons' `round`. diff --git a/skills/bestax-optimize/references/modular-build.md b/skills/bestax-optimize/references/modular-build.md index cf1e63cf0..9dabf7fe9 100644 --- a/skills/bestax-optimize/references/modular-build.md +++ b/skills/bestax-optimize/references/modular-build.md @@ -153,11 +153,13 @@ not `flexbox` — `flexbox` holds only the alignment props. Verified failure mod ## Worked example — the create-bestax starter app The `npm create bestax` starter renders `Container`, `Section`, `Columns`/`Column`, `Image`, -`Title`/`SubTitle`, `Box`, `Card`, `Buttons`/`Button`, and `Notification` — all stock Bulma, -no bestax extras. Its helper props are `display`/`justifyContent` (visibility + flexbox), -`textAlign` (typography), and `textColor` (color); it uses no spacing props. That inventory -compiles to the following — verified pixel-equivalent to the prebuilt `complete` flavor in -headless Chromium (light and dark), at 453 KB raw / 40 KB gzip vs 813 KB / 83 KB: +`Title`/`SubTitle`, `Box`, `Card`, `Buttons`/`Button`, and `Notification` with its close +button: all stock Bulma, no bestax extras. Its helper props are `display` (visibility), +`flexGrow` (flexbox), and `textAlign` (typography); it uses no spacing or color props. That +inventory compiles to the following, verified pixel-equivalent to the prebuilt `complete` +flavor in headless Chromium (light and dark, phone and desktop width, notification open), at +274 KB raw / 30 KB gzip vs 819 KB / 84 KB. Scaffolded with an icon library, the starter also +needs `bulma/sass/elements/icon` for its `IconText` card titles: ```scss // src/styles.scss @@ -170,6 +172,7 @@ headless Chromium (light and dark), at 453 KB raw / 40 KB gzip vs 813 KB / 83 KB @use 'bulma/sass/elements/box'; @use 'bulma/sass/elements/button'; // Button + Buttons +@use 'bulma/sass/elements/delete'; // Notification's close button @use 'bulma/sass/elements/image'; @use 'bulma/sass/elements/notification'; @use 'bulma/sass/elements/title'; // Title + SubTitle @@ -179,9 +182,8 @@ headless Chromium (light and dark), at 453 KB raw / 40 KB gzip vs 813 KB / 83 KB @use 'bulma/sass/layout/section'; @use 'bulma/sass/helpers/visibility'; // display="flex" -@use 'bulma/sass/helpers/flexbox'; // justifyContent/flexGrow/flexShrink +@use 'bulma/sass/helpers/flexbox'; // flexGrow @use 'bulma/sass/helpers/typography'; // textAlign -@use 'bulma/sass/helpers/color'; // textColor ``` ```tsx diff --git a/turbo.json b/turbo.json index 0579dfda2..2b71b2d6f 100644 --- a/turbo.json +++ b/turbo.json @@ -41,6 +41,21 @@ "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/skills/**"], "outputs": ["dist/**", "templates/skills/**"] }, + "create-bestax#test": { + "inputs": [ + "$TURBO_DEFAULT$", + "$TURBO_ROOT$/bulma-ui/package.json", + "$TURBO_ROOT$/bulma-ui/src/helpers/useBulmaClasses.tsx" + ] + }, + "create-bestax#test:coverage": { + "inputs": [ + "$TURBO_DEFAULT$", + "$TURBO_ROOT$/bulma-ui/package.json", + "$TURBO_ROOT$/bulma-ui/src/helpers/useBulmaClasses.tsx" + ], + "outputs": ["coverage/**"] + }, "bestax-migrate#test": { "dependsOn": ["@allxsmith/bestax-bulma#build"], "inputs": [