Tell the demo-pages and ui-component skills not to add page-level styles (#3088)

This commit is contained in:
Paweł Kuna authored and GitHub committed 2026-09-18 00:28:11 +02:00
1 parent 364ccf801b
commit 06143bf617
6 files changed
+39 -10

No files matched your search

+6
View File
@@ -18,6 +18,8 @@ The preview HTML is the product: users copy a page out of `preview/dist` and exp
Always `<script is:inline>`. A plain `<script>` is bundled, hashed, and moved into a module chunk — invisible in the copied HTML — and a plain `<style>` makes Astro stamp `data-astro-cid-*` on every element of the component.
An inline script is plain JavaScript. Type annotations, `as HTMLInputElement`, `function (this: …)` — anything the browser cannot parse — is the tell that a script is still being bundled, whatever the comment markers around it say. Two modal scripts shipped that way for months: the `BEGIN` / `END` comments wrapped nothing, and the copied modal had a validator that never ran. Use `getElementById()` with a null guard instead of a cast, and check the built page: the code must sit between its markers, not in a `_astro/*.js` chunk.
## 2. Where the script ends up
Scripts render at the **end of `<body>`**, not where the component sits, so they run after their markup and after the vendor libraries in `<head>`. The route:
@@ -30,6 +32,9 @@ Scripts render at the **end of `<body>`**, not where the component sits, so they
- `shared/layouts/BaseLayout.astro` and `docs/layouts/DocsLayout.astro` drain the registry through `<PageScripts />`.
- Registration is **synchronous** in `CaptureScript`'s frontmatter on purpose: Astro renders siblings concurrently, so a script registered after an `await` can miss the drain. Do not make `CaptureScript` async.
- The registry does not dedupe. Two instances of a component emit two scripts — write them so that is harmless (see section 6).
- Both registries are keyed by `Astro.request` (a `WeakMap`, see `shared/lib/page-scripts.ts`). Astro renders pages concurrently in the build, the dev server and the `html-diff` / `check-html` crawlers, so a module-level array would hand one page's scripts to another. Keep it that way when touching the registry, and pass `Astro.request` from both the capture and the drain side.
Modals take the same route: `<CaptureModal>` → `addPageModal()` → `shared/lib/page-modals.ts` → `<PageModals />` at the end of `<body>`, before the settings panel. Same rules: synchronous registration, keyed by `Astro.request`, no dedupe — a component that wraps a modal in `CaptureModal` and is rendered twice emits two modals with the same `id`, so such a component takes an `id` prop like a script does.
## 3. The standard snippet
@@ -112,6 +117,7 @@ An inline script that throws leaves the page looking fine but dead. Never hand o
## 10. Checklist
- [ ] `<script is:inline>` inside `<CaptureScript>`, with `BEGIN`/`END` comments
- [ ] Plain JavaScript inside: no type annotations, no casts, nothing the browser cannot parse
- [ ] Required `id` prop; selectors derived from it, no class-wide queries
- [ ] Data passed via `define:vars`, not string interpolation
- [ ] `init…()` + `readyState` guard
+1
View File
@@ -48,6 +48,7 @@ Do not undo these; each has a comment at the site:
- **Build output must not be an Astro output dir.** `preview/.build/vite.config.mts` writes to `tmp-assets/js`, not inside `dist/`: `astro build` copies `public/` into `dist/`, so an `outDir` under `dist/` gets re-seeded and re-copied, growing without bound across builds.
- **No leading-dot output path.** terser's CLI `--source-map` parser rejects a path segment starting with a dot (`.build/out/…`) with a bogus "not a supported option".
- **One write per output file.** `build-css.ts` runs sass + postcss + clean-css in-process so each file is written once, fully processed; it also skips writes when the content is unchanged, to keep watchers quiet. Do not reintroduce a step that rewrites a finished file in place — that is what shifted every source-map mapping when the banner was added afterwards (#2766).
- **Two environment variables, two questions.** `NODE_ENV` answers "dev server or build?" — Vite forces it to `production` for every build, including branch previews, so it can only pick unminified assets and the dev favicon. `VERCEL_ENV` answers "which deployment?" — `production` on the main hosts, `preview` on branch deploys, unset locally — and is the only value that may open `robots.txt` or anything else that must stay off on a preview. Checking `NODE_ENV === 'preview'` is never true anywhere. Both are in `globalEnv` in `turbo.json`; a new variable read by a build must be added there or the cache ignores it.
- **Vendor copies are not pages.** `preview`'s `prettify-html` integration formats built HTML on `astro:build:done` but excludes `dist/preview/` and `dist/dist/`, which are asset copies and contain third-party HTML that breaks the parser.
## 5. Diagnosing
+2
View File
@@ -57,6 +57,8 @@ Write `--badge-bg`, not `--tblr-badge-bg`. The public `--tblr-` prefix is added
The consequence to remember: **names owned by third-party libraries must not be prefixed.** `cssVarIgnore` lists them (`--bs-`, `--fc-`, `--gl-`, `--litepicker-`, `--plyr-`, `--ts-`, …). Prefixing one detaches the theming with no error anywhere — the library keeps reading its own name and simply never sees the value. When a vendor override introduces a new foreign name, add it to `cssVarIgnore`; `core/scss/tests/css-var-prefix.test.mjs` snapshots every custom property of `tabler-vendors.scss`, so a missing entry shows up as a `--tblr-`-prefixed foreign name in the snapshot diff.
The snapshot moves in the other direction too. A vendor override that starts reading a Tabler name (`var(--success)`, `var(--form-valid-border-color)`) adds a correctly prefixed `--tblr-*` entry, and the test fails until the snapshot is refreshed. Read the diff: every new name is one Tabler owns → `pnpm --dir core exec vitest run --config vitest.scss.config.mjs -u` and commit the `.snap`; a name a library owns → `cssVarIgnore`, never `-u`.
Global properties (`--dir`, colours, fonts, spacing) live in `_props.scss`, which emits them on `:root, :host`.
## 4. Dark mode
+24 -4
View File
@@ -86,17 +86,34 @@ The pluralised class names (`.badges-list`, `.tags-list`) are deprecated aliases
Icon action buttons: `btn-action` is always combined with `btn` (`btn btn-action`, plus `btn-sm` when small) — never use `btn-action` on its own.
## 6. Data and repetition
## 6. No page-level styles
Never add a `<style>` block to a preview page or a shared component, and never import CSS from the frontmatter. The built HTML is the product: a plain `<style>` makes Astro stamp `data-astro-cid-*` on every element of the page, so a copied page carries dozens of attributes nobody wants. The block also bypasses `build-css.ts`, so a custom property written bare (`var(--table-color)`) stays unprefixed and never matches Tabler's `--tblr-*` names. `is:global` or `is:inline` avoids the stamping but not the prefix problem, and still hides styling where nobody looks for it.
If a demo needs a look the framework does not have, either use existing utilities in the markup or add the class to `core/scss/` (`core-scss` skill). If neither fits, keep the plain markup; a demo page never justifies a one-off rule.
## 7. Accessible demo markup
Demo markup is copied as-is, so a shortcut here becomes a shortcut in every user's app. The four that keep coming back:
- An action with no destination is `<button type="button" class="btn">`, never `<a class="btn">` without `href`. An anchor without `href` is not focusable, so keyboard users cannot reach it. An anchor that really links keeps `href`, and `href="#"` is fine for a demo link.
- Every control has a name. A search box, a table-row `<select>`, a switch in a card footer: give it a `<label for>` or an `aria-label`. `placeholder` is not a name.
- A `<label>` wraps exactly one control or points at one with `for`. A label wrapping a radio *and* an unrelated dropdown toggle selects the radio when the toggle is clicked.
- A link or button whose only content is an icon or a logo gets `aria-label`. `<Icon>` renders `aria-hidden`, so without the label the control has no name at all.
Dropdown toggles are `<button type="button" data-bs-toggle="dropdown">`, or `<a href="#" role="button" …>` inside a nav; never a `<label>`. One `.dropdown-menu` per toggle — Bootstrap shows only the first sibling.
## 8. Data and repetition
- Loop over `shared/data/*.json` (`site.json` colors, `people.json`, `flags.json`) instead of pasting 20 near-identical blocks.
- Cast JSON entries to a named type in the frontmatter when you index into them; `astro check` is part of the quality gates.
## 7. Registering the page
## 9. Registering the page
- Add the page to `shared/data/menu.json` so it is reachable, and use the same key in `pageMenu`.
- A new `shared/ui` component usually needs both a demo page and a docs page. After finishing here, check `docs/content/**` and consult the `write-docs` skill if it is missing.
## 8. Verify in the browser
## 10. Verify in the browser
Never hand the page to the user unchecked.
@@ -110,13 +127,16 @@ curl -s http://localhost:3000/badges | grep -o '<h[1-6]' | sort | uniq -c
A demo page should show exactly one `h1` and then `h2`s — no `h3` in the card grid.
## 9. Quality checklist
## 11. Quality checklist
- [ ] `title` and `description` set; no `pageHeader` repeating the title
- [ ] `DocsLink` present when a docs page exists; `pnpm run check:docs-links` passes
- [ ] One card per variant; `CardTitle` + `CardSubtitle` on each
- [ ] No raw `card-title` / `card-subtitle` / `page-title` markup
- [ ] List wrappers instead of per-item margins
- [ ] No `<style>` block and no CSS import; new looks go to `core/scss/`
- [ ] Actions are `<button type="button">`, controls and icon-only links have a name, every `<label>` matches one control
- [ ] Every `<Icon name>` exists in `shared/data/icons.json` (`play` does not; `player-play` does)
- [ ] `pnpm run format:prettier` and `pnpm run type-check` clean (repo root, not a tail-filtered subset)
- [ ] Page opened in the browser, console clean, heading order checked
- [ ] Changeset written with the `generate-changeset` skill
+4 -4
View File
@@ -3,7 +3,7 @@ name: navigation
description: >-
Make a page reachable and keep its links valid — the preview sidebar/navbar
menu in `shared/data/menu.json`, the docs menu in `shared/data/docs.json`,
and the redirects in `docs/astro.config.mjs`. Use whenever a page is added,
and the redirects in `docs/lib/redirects.ts`. Use whenever a page is added,
renamed, moved or removed in `preview/pages/` or `docs/content/`, whenever a
`pageMenu` key does not highlight the right entry, and whenever a docs URL
changes. Covers both menu formats, the `pageMenu` key rules and the link
@@ -56,7 +56,7 @@ An array-based tree (`menu`) plus a `links` list for the external links in the d
## 3. Renamed or removed docs URLs
Add an entry to `redirects` in `docs/astro.config.mjs`, next to the existing ones:
Add an entry to `redirects` in `docs/lib/redirects.ts`, next to the existing ones. That one table feeds `docs/astro.config.mjs` (real HTTP redirects), `docs/middleware.ts` (which must not rewrite these urls to `.md`) and the link gate, so it is the only place a redirect is written:
```js
'/ui/base/markdown': { status: 301, destination: '/ui/base/prose' },
@@ -72,7 +72,7 @@ Update in the same pass: `shared/data/docs.json`, any `related:` front matter po
pnpm run check:docs-links
```
It resolves, from source and without a build: markdown links and `related:` in `docs/content/**/*.mdx`, menu and link URLs in `shared/data/docs.json`, redirect destinations from `docs/astro.config.mjs`, `href` literals in docs components, `<DocsLink path="…">` and `getDocsUrl('…')` in the demo site, plus asset paths that really render. Anchors are checked against real heading slugs.
It resolves, from source and without a build: markdown links and `related:` in `docs/content/**/*.mdx`, menu and link URLs in `shared/data/docs.json`, redirect sources and destinations from `docs/lib/redirects.ts`, `href` literals in docs components, `<DocsLink path="…">` and `getDocsUrl('…')` in the demo site, plus asset paths that really render. Anchors are checked against real heading slugs.
It does **not** check `menu.json`: a wrong preview URL or a `pageMenu` key that matches nothing shows up only in the browser. Open the page and confirm the menu entry is highlighted (`astro-dev` skill).
@@ -80,7 +80,7 @@ It does **not** check `menu.json`: a wrong preview URL or a `pageMenu` key that
- [ ] New preview page added to `menu.json` with a `.html` url, and its `pageMenu` prop equals the dot-path of the keys
- [ ] New docs page added to `docs.json` at the right depth, url absolute and extension-free
- [ ] Renamed or removed docs URL has a 301 in `docs/astro.config.mjs`
- [ ] Renamed or removed docs URL has a 301 in `docs/lib/redirects.ts`
- [ ] `related:` entries and `<DocsLink path="…">` updated to the new path
- [ ] `pnpm run check:docs-links` clean
- [ ] Menu highlighting checked in the browser
+2 -2
View File
@@ -84,7 +84,7 @@ Props are camelCase. Booleans default to `false` — never `showClose = true`.
- Use `class:list={[...]}` with falsy entries for conditionals; it drops them and merges the caller's `className` for you. `.filter(Boolean).join(' ')` is only for a string you have to pass to a helper (`iconSvg`).
- Put `className` last so the caller wins.
- Do not emit a class for a default (`size !== 'md' && \`btn-${size}\``).
- No `<style>` blocks and no CSS imports in the frontmatter. Styling is `core/scss/`.
- No `<style>` blocks and no CSS imports in the frontmatter. Styling is `core/scss/`. A `<style>` makes Astro stamp `data-astro-cid-*` on every element the component renders, and its custom properties skip the `--tblr-` prefixing in `build-css.ts`.
## 5. Slots and containers
@@ -117,7 +117,7 @@ Heading components pick their own level (`CardTitle` renders `h2`, `as="h3"` for
- Content that may contain markup or entities (`&hellip;`) → `set:html` / `<Fragment set:html={…} />`. Entities in attribute strings must be passed as an expression (`title={"…&hellip;"}`), because JSX decodes them inside string literals.
- Boolean attributes are inconsistent: `selected={true}` renders bare, but some (e.g. `multiple`) render `="true"`. Use `multiple ? '' : undefined` when a bare attribute is required, and check the rendered HTML.
- Client-side behaviour (plugin init, event wiring) does **not** go in an ad-hoc `<script>` — see the `astro-scripts` skill.
- Icons always through `<Icon name="…" />`, never inline SVG.
- Icons always through `<Icon name="…" />`, never inline SVG. An unknown name renders nothing and only warns in the build log (`[svg] unknown Tabler icon`), so the page looks fine with a blank button. Check the name in `shared/data/icons.json` before using it: it is `player-play`, not `play`.
## 7. Logic belongs in `shared/lib`