Working with AI assistants
If an assistant is writing screens with this framework, give it the two things it cannot infer: what exists, and what was deliberately left out. A model asked for a data grid will build one — not from carelessness, but because nothing told it the question had already been answered.
Paste this into your project's instructions file
CLAUDE.md, AGENTS.md, .cursorrules — whichever
your tooling reads. It is 93 lines and it is generated from this
framework's own architecture notes, so it stays true as the framework changes.
# busy-office-ui — rules for this project
## Read this before writing markup
https://busy-office.github.io/busy-office-ui/llms.txt
Generated from the shipped CSS: every class, every JS behavior's DOM hooks, and
the legal VALUES for framework data-* attributes. If it is not in there, it does
not exist — do not invent a plausible name.
## Build a screen in this order
1. Shell — pick the app shell and its regions:
https://busy-office.github.io/busy-office-ui/concepts/layouts/
2. Pattern — let the router pick the screen shape, do not invent one:
https://busy-office.github.io/busy-office-ui/concepts/which-pattern/
3. Components — compose the pattern from what exists:
https://busy-office.github.io/busy-office-ui/components/
4. Verify — run the checker below before you call it done.
Each step narrows the next. Skipping to step 3 is how a grid gets built for a
job one of the shipped patterns already answers.
## Verify, do not trust
npx bo-check-markup dist
Fails on any bo-* class that does not exist and any framework data-* value the
CSS never switches on. Run it in CI on BUILT html. A generated project has no
build step, so its own script runs bo-check-markup .
instead.
## Choosing how users maintain data — these four, and no grid
- M1: Row-swap inline edit. Use when: Config and lookup tables, master-data
upkeep — the SM30 case.
- M2: Master-detail. Use when: A record has more fields than fit a row, or has
dependent fields and value helps. Most master-data maintenance in practice.
- M3: Mass change. Use when: "Update 200 records" — the request people reach
for cell editing to satisfy.
- M4: Excel round-trip. Use when: True bulk work. Excel beats any web grid at
being Excel; the web's job is to validate and report.
## Deliberately absent — do not build these
- A grid engine — virtual scroll, column virtualisation, cell editing
-> Server-side paging or load-more for lists; the token-themed AG Grid recipe
on /concepts/scale for the one screen in twenty that is genuinely a
spreadsheet
- An icon catalogue — the hundreds-of-glyphs set
-> The small shipped glyph set, then --bo-icon-src for anything else: point it
at Lucide, Heroicons or your own SVG and .bo-icon supplies sizing,
currentColor, density and forced-colors. Every glyph is an inlined SVG,
so cost grows strictly with set size and a 200-icon catalogue costs most
of what the rest of the framework costs — /components/icon computes the
live figures from the shipped stylesheet rather than quoting a snapshot
(the two this row used to carry, "12 glyphs" and "10.3%", were stale by
2026-08-24)
- An illustration / spot-art set
-> An <img> you supply with alt=""; for glyphs --bo-icon-src, for a person or
org .bo-avatar, for a tenant app tile the launcher's inline svg slot.
Empty states are fixed by a clear sentence and the action that resolves
them, not by a drawing that must be redrawn per brand and downloaded on a
warehouse tablet
- A client-side row virtualiser for SEARCH workflows
-> Filter server-side and paginate — fixed row heights are maintained so a
third-party virtualiser also drops in cleanly
- Tab overflow arrow buttons
-> The edge fade ships and is conditional on actual overflow; add two buttons
calling scrollBy if your users expect them
- An app-icon library inside .bo-icon
-> Inline <svg> per tenant, or an initials chip — app icons are product
content, and every consumer would ship glyphs they never use
- A "field editor" component
-> .bo-data-table + data-row-edit + the typed inputs; see
/patterns/field-editor
- A master-detail component
-> .bo-data-table + a card, dialog or offcanvas; see /patterns/master-detail
- .bo-value-help and .bo-staging-table
-> Compose combobox + dialog, and the staging pattern
- A second toolbar idiom
-> .bo-cluster already does it
## Rules a linter cannot check for you
- State is two-channel: visible non-color cue (glyph, aria-hidden) AND
programmatic (visually-hidden text like "Completed: ", aria-current, ARIA
attributes).
- Forms: aria-invalid + aria-describedby -> message id (+ role="alert" if
dynamic).
- Dialogs: aria-labelledby -> title id. Icon-only buttons: aria-label.
- Row-select checkboxes need name/value to POST. Styled lists get role="list".
- Sorting: YOUR code re-orders rows and sets aria-sort on <th>; CSS draws
indicator.
- Never user-scalable=no. Never override html font-size (rem-based framework).
- Columns hidden by container compaction (__col--secondary) must stay
reachable.
- A scrollable region must be keyboard-reachable: .bo-data-table-container
needs tabindex="0" (and an aria-label when the table has no caption). axe
reports scrollable-region-focusable otherwise, and no markup linter can
infer it.
Why these five things
- llms.txt — the surface, generated
from the shipped CSS so it cannot disagree with it. It now carries the legal values
for
data-*attributes, which is the difference betweendata-row-state="dirty"and a plausible-looking invention that silently does nothing. bo-check-markup dist— because an unknown class renders unstyled and looks like a CSS bug, and an unknown attribute value looks like nothing at all. See troubleshooting.- The four maintenance patterns — "how do users maintain this data" is the question that most often resolves to "build a grid", which is the most expensive wrong answer available.
- The two-channel rule — every state signal ships a visible non-colour cue AND a programmatic one; see the accessibility model. An assistant that colours a row red and stops has done half the job.
- The absent list — an absence is invisible. Everything on it was considered and declined, and each row carries what to use instead.
Measured rather than assumed: building a realistic screen from llms.txt
alone produced exactly one machine-detectable error — an invented
data-row-state value — which is why the values are published now. It also
produced one error no linter can catch, a scrollable table container missing
tabindex="0", which is why that rule is in the list above.