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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,167 @@ This repository is `@bquery/ui`, a framework-agnostic Web Components library bui
5. Treat import side effects (`import '@bquery/ui'`) and per-component entrypoints (`@bquery/ui/components/<name>`) as the canonical registration model; `registerAll()` is deprecated compatibility-only.
6. When touching versioned install snippets or release-facing docs, keep pinned CDN examples aligned with the current package version in `package.json`.

## Component conventions

### Styles

Declare styles with the `css` tagged template from `@bquery/bquery/component` and
interpolate the shared fragments from `src/utils/styles.ts`:

```ts
import { component, css, html } from '@bquery/bquery/component';
import { baseStyles, fieldStyles, focusRing, reset, srOnly } from '../../utils/styles.js';

styles: css`
${baseStyles}
${reset}
${focusRing}
:host { display: block; }
`
```

The fragments, and when to reach for each:

| Fragment | Provides |
| -------------- | ------------------------------------------------------------------- |
| `baseStyles` | Tokens + both colour schemes. Every component. |
| `reset` | `box-sizing`, `[hidden]`, and the reduced-motion opt-out. |
| `focusRing` | The standard ring on the usual focusable elements, at zero specificity. |
| `fieldStyles` | `.field` / `.label` / `.hint` / `.error-msg` / `.required-mark` for form controls. |
| `srOnly` | `.sr-only`. |

**A backtick inside a CSS comment terminates the template literal.** Two
components have been broken this way; write ``accent-color`` without the
backticks inside `css` blocks.

This matters for more than tidiness. A `css` payload is handed to
`adoptedStyleSheets`, so the stylesheet is constructed **once per component and
shared by every instance**, and re-renders no longer rewrite it. A plain string
falls back to a per-instance `<style>` element whose ~5.6 KB of token/theme CSS
is duplicated per element and rewritten on every render.

Only `ComponentStyles` values survive interpolation intact — `css` escapes
interpolated *strings*, which would corrupt raw CSS. Use `rawCss()` from
`src/utils/styles.ts` to wrap CSS text generated at runtime.

### Lifecycle and teardown

Use the helpers in `src/utils/component.ts` rather than stashing handlers on the
host element:

```ts
connected() {
const el = host<MyState>(this); // typed setState/getState/setProp
const scope = bind(el);
scope.on(document, 'keydown', onKeyDown); // removal registered for you
scope.timeout(fn, 200); // cleared on disconnect
},
disconnected() {
release(this); // unwinds everything
},
```

Anything reaching outside the component — `document`/`window` listeners, timers,
`MutationObserver`s, form proxies, `aria-*` written onto slotted light-DOM
elements — must be registered on the scope. `tests/component-utils.test.ts`
asserts that the overlay components leave no document listeners behind.

**`connected()` runs twice.** The runtime mounts an element from
`attributeChangedCallback` during upgrade, and the `connectedCallback` that
follows sees it already mounted and takes its reconnect path — so any element
carrying an observed attribute in the initial HTML runs `connected()` a second
time. `bind()` absorbs that: calling it again unwinds the previous scope, so
exactly one set of listeners stays live. Consequences to respect:

- Call `bind()` **once**, at the top of `connected()`. Use `scopeOf(owner)` to
add teardown from another hook.
- `connected()` must be safe to run twice for everything it does *besides*
registering on the scope. Anything not on the scope — a `queueMicrotask` that
mutates light DOM, an event dispatched on connect — will happen twice.

This is what made `bq-dropdown-menu` toggle twice per click and never open.

Use `store(owner, init)` when a value created in `connected()` has to stay
reachable from `updated()`.

### Icons

`svg` is on the sanitizer's *forbidden* list, which `sanitize.allowTags` cannot
re-open — a component can never emit inline SVG. Icons therefore live in the
stylesheet, as `mask-image` data URIs painted with `currentColor`:

```ts
import { iconCss } from '../../utils/icons.js';

styles: css`
${baseStyles}
${iconCss('chevron-down', 'x')}
`,
// render: <span class="icon" data-icon="x" aria-hidden="true"></span>
```

Name only the icons the component draws; `iconCss` emits one rule each, and the
full set is far too heavy to embed everywhere (`bq-icon` is the exception).
`iconMask(name)` returns just the mask declarations, for a `::before`/`::after`
on an element the component already renders.

`iconCss` defines a global `.icon` box inside the shadow root. A component that
already uses `.icon` for something else — a slot wrapper, say — must rename it,
or the wrapper inherits the mask box. `bq-banner` and `bq-file-upload` hit this.

### Sanitizer allowlist

Rendered markup is sanitized. The framework's base allowlist covers `part`,
`aria-*`, `data-*` and the usual form attributes, but not everything — `accept`,
`datetime`, `inputmode`, `scope`, `colspan`, `spellcheck` and `style` each
needed an explicit `sanitize.allowAttributes` entry. If an attribute silently
disappears from rendered output, this is why.

`tests/sanitizer-allowlist.test.ts` parses every render template and fails on an
attribute the sanitizer would drop, so this can no longer ship unnoticed.

### Focus across re-renders

Rendering assigns `shadowRoot.innerHTML`, so every render destroys the node the
user is in. Any component with a focusable element inside its shadow root wants
the packaged treatment:

```ts
connected() { trackFocus(host(this), bind(this)); },
beforeUpdate() { markFocus(this); },
updated() { restorePreservedFocus(this); },
```

`trackFocus` records the focus position from a handful of events in the
**capture** phase, so the snapshot is taken before the component's own handler
runs. `beforeUpdate` covers attribute-driven renders, which no event precedes.
`setState` renders skip `beforeUpdate` entirely, which is why the event-driven
half is needed at all.

### Tinted surfaces

Never paint a background from a 50/100/200 palette step. Those are *light*
colours in both schemes, so a badge or an alert built on them stays near-white
on a dark page — which is how alerts, badges, chips, avatars and tags all
shipped broken in dark mode. Use `--bq-intent-{primary|success|danger|warning|
info|neutral}-{bg|fg|border}`, which flip with the scheme.
`tests/component-css.test.ts` fails the build on a violation.

### Theming across the shadow boundary

Semantic tokens resolve through a scheme channel — `--bq-bg-base:
var(--bq-scheme-bg-base, #fff)` — so a document-level `data-theme` can reach
into shadow roots. Do not "simplify" that away: a plain `:host` definition
cannot be overridden by an ancestor, and `:host-context()` only exists in
Chromium. See `src/theme/scheme.ts`.

### Shadow-DOM state and re-renders

Every render replaces the shadow root's contents. Anything written into the
shadow root imperatively (for example the file-upload file list, or the
avatar-group overflow counter, which depend on light-DOM children that `render`
cannot see) must be repainted from `updated()`.

## Setup and validation

Install dependencies with Bun:
Expand Down
45 changes: 27 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ It is designed to give teams the kind of component coverage, polish, accessibili
- **Reusable UI components** spanning actions, forms, navigation, data display, overlays, and feedback
- **Framework-agnostic** usage in plain HTML, React, Vue, Angular, Svelte, and other Custom Element-capable runtimes
- **Accessible by default** with keyboard support, ARIA roles, focus management, and screen reader announcements
- **Themeable via design tokens** and CSS custom properties
- **Themeable via design tokens** and CSS custom properties, switchable from the document in every browser
- **A built-in icon set** rendered as CSS masks, so glyphs inherit text colour and stay crisp at any size
- **Tree-shakeable ESM exports** with per-component imports
- **Browser-ready UMD and IIFE bundles** for direct CDN delivery
- **Built-in dark mode, i18n, and event-driven APIs**
Expand All @@ -28,14 +29,14 @@ It is designed to give teams the kind of component coverage, polish, accessibili

The current library covers the core component categories developers expect from modern UI libraries:

| Category | Components |
| ---------------- | -------------------------------------------------------------- |
| **Actions** | Button, Icon Button |
| **Forms** | Input, Textarea, Select, Checkbox, Radio, Switch, Slider, Chip |
| **Navigation** | Tabs, Accordion, Breadcrumbs, Pagination |
| **Data Display** | Card, Badge, Avatar, Table, Divider, Empty State, Stat Card |
| **Feedback** | Alert, Progress, Spinner, Skeleton, Tooltip, Toast |
| **Overlays** | Dialog, Drawer, Dropdown Menu |
| Category | Components |
| ---------------- | --------------------------------------------------------------------------------------------------------------------- |
| **Actions** | Button, Icon Button, Button Group, Copy Button |
| **Forms** | Input, Number Input, Textarea, Select, Combobox, Tag Input, Pin Input, Date Picker, Segmented Control, Checkbox, Radio, Switch, Slider, Chip, Rating, File Upload |
| **Navigation** | Tabs, Accordion, Breadcrumbs, Pagination, Stepper, Tree |
| **Data Display** | Card, Badge, Avatar, Avatar Group, Table, Divider, Empty State, Stat Card, Timeline, Kbd, Icon, Meter |
| **Feedback** | Alert, Banner, Progress, Spinner, Skeleton, Tooltip, Toast |
| **Overlays** | Dialog, Drawer, Dropdown Menu, Popover |

For the full catalog and feature coverage, see [`docs/components/index.md`](./docs/components/index.md).

Expand Down Expand Up @@ -69,24 +70,25 @@ import '@bquery/ui';

```html
<!-- UMD -->
<script src="https://cdn.jsdelivr.net/npm/@bquery/ui@1.10.0/dist/index.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@bquery/ui@1.15.0/dist/index.umd.js"></script>

<!-- IIFE -->
<script src="https://cdn.jsdelivr.net/npm/@bquery/ui@1.10.0/dist/index.iife.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@bquery/ui@1.15.0/dist/index.iife.js"></script>
```

The UMD and IIFE bundles register all components on load and expose the library on `window.BQueryUI`.
For ESM-based app builds, prefer importing `@bquery/ui` from your bundler or other module-aware build tool.

## Current release snapshot

The current release (`1.10.0`) emphasizes:
The current release (`1.15.0`) emphasizes:

- **Import-based registration as the canonical integration path.** Import `@bquery/ui` once to register all components, or import `@bquery/ui/components/<name>` to register only the wrappers you need.
- **A clearer shared package surface.** `@bquery/ui/tokens`, `@bquery/ui/theme`, `@bquery/ui/i18n`, `@bquery/ui/utils`, and `@bquery/ui/register` are all explicit entry points.
- **Broader accessibility and localization coverage.** Recent releases improved accordion semantics, live form-field counters, localized table states, chip keyboard behavior, and reduced-motion-aware overlay transitions.
- **A larger production-ready catalog.** The package now spans 31 web components across actions, forms, navigation, data display, feedback, and overlays, including `bq-dropdown-menu`, `bq-stat-card`, and the imperative toast API.
- **Aligned docs and browser bundles.** Version-pinned CDN snippets, migration guidance, and Storybook/VitePress references now target `1.10.0`.
- **A built-in icon set.** Interface glyphs are rendered as CSS masks rather than text characters, so they inherit `currentColor`, scale with `font-size`, and look the same on every platform. Available as `<bq-icon>` and to your own components through `iconCss()`.
- **Dark mode that works in every browser.** The scheme now travels through an inherited custom-property channel instead of `:host-context()`, which exists only in Chromium — and which, sitting in a selector list, previously took the whole dark theme down with it in Firefox and Safari. `data-theme` also works on any subtree, not just the root.
- **A deeper token layer.** Semantic surfaces (`--bq-surface-raised`, `--bq-surface-overlay`), translucent interaction states, per-intent focus rings, a shared control-height scale, and dark-mode elevation.
- **Focus that survives a re-render.** Rendering replaces the shadow tree; text fields, calendars and tree views now keep focus and the caret across the renders their own interactions cause.
- **A larger production-ready catalog.** The package spans 49 web components across actions, forms, navigation, data display, feedback, and overlays — including `bq-date-picker`, `bq-tree`, `bq-pin-input`, `bq-meter`, `bq-copy-button`, and `bq-icon`.
- **Aligned docs and browser bundles.** Version-pinned CDN snippets, migration guidance, and Storybook/VitePress references target `1.15.0`.

## Tree-Shakeable Usage

Expand Down Expand Up @@ -153,9 +155,16 @@ For migration guidance from older registration patterns, see [`docs/guide/migrat
### Theming

- CSS custom properties for colors, spacing, typography, radius, shadows, motion, and z-index
- Light and dark themes
- Semantic surface, interaction, and focus-ring tokens on top of the raw palette
- Light and dark themes, switchable per document *or* per subtree with `data-theme`
- `::part()` support for targeted customization

### Icons

- A built-in set of interface glyphs, available as `<bq-icon name="…">`
- Rendered through CSS masks, so an icon inherits `currentColor` and scales with `font-size`
- `iconCss()` lets your own components pull in only the glyphs they draw

### Internationalization

- User-facing strings run through the library i18n system
Expand Down
Loading
Loading