@orkestrel/scaffold 0.0.18 → 0.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/host/AGENTS.md +4 -2
  2. package/dist/host/CLAUDE.md +22 -10
  3. package/dist/host/agents/skills/enterprise-bootstrap/SKILL.md +267 -0
  4. package/dist/host/agents/skills/enterprise-bootstrap/agents/openai.yaml +4 -0
  5. package/dist/host/agents/skills/enterprise-bootstrap/references/bootstrap-reference.md +609 -0
  6. package/dist/host/agents/skills/enterprise-bootstrap/references/components.md +994 -0
  7. package/dist/host/agents/skills/enterprise-bootstrap/references/frontend-design.md +59 -0
  8. package/dist/host/agents/skills/enterprise-bootstrap/references/utilities.md +312 -0
  9. package/dist/host/agents/skills/orkestrel-align-packages/references/integration.md +4 -13
  10. package/dist/host/agents/skills/orkestrel-build-application/SKILL.md +78 -50
  11. package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +81 -0
  12. package/dist/host/agents/skills/orkestrel-debrief/agents/openai.yaml +4 -0
  13. package/dist/host/agents/skills/orkestrel-debrief/references/field-testing.md +75 -0
  14. package/dist/host/agents/skills/orkestrel-harden-package/SKILL.md +11 -9
  15. package/dist/host/agents/skills/orkestrel-harden-package/references/centralization.md +44 -59
  16. package/dist/host/agents/skills/orkestrel-harden-package/references/hardening.md +14 -6
  17. package/dist/host/agents/skills/orkestrel-polish-surface/SKILL.md +113 -0
  18. package/dist/host/agents/skills/orkestrel-polish-surface/agents/openai.yaml +4 -0
  19. package/dist/host/agents/skills/orkestrel-polish-surface/references/capture-harness.md +82 -0
  20. package/dist/host/claude/agents/builder.md +2 -0
  21. package/dist/host/claude/agents/codex.md +33 -26
  22. package/dist/host/claude/agents/grok.md +7 -0
  23. package/dist/host/claude/agents/implementer.md +2 -1
  24. package/dist/host/claude/agents/orkestrel.md +20 -15
  25. package/dist/host/claude/agents/planner.md +2 -1
  26. package/dist/host/claude/agents/reviewer.md +6 -0
  27. package/dist/host/claude/rules/documentation.md +1 -0
  28. package/dist/host/claude/rules/names.md +5 -7
  29. package/dist/host/claude/rules/quality.md +7 -5
  30. package/dist/host/claude/rules/styles.md +1 -0
  31. package/dist/host/claude/rules/tests.md +1 -0
  32. package/dist/host/claude/rules/typescript.md +3 -10
  33. package/dist/host/claude/rules/workspace.md +2 -5
  34. package/dist/host/claude/skills/enterprise-bootstrap/SKILL.md +12 -0
  35. package/dist/host/claude/skills/orkestrel-debrief/SKILL.md +12 -0
  36. package/dist/host/claude/skills/orkestrel-polish-surface/SKILL.md +12 -0
  37. package/dist/host/codex/agents/analyst.toml +6 -3
  38. package/dist/host/codex/agents/builder.toml +3 -2
  39. package/dist/host/codex/agents/checker.toml +4 -2
  40. package/dist/host/codex/agents/grok.toml +3 -1
  41. package/dist/host/codex/agents/implementer.toml +4 -2
  42. package/dist/host/codex/agents/opus.toml +5 -3
  43. package/dist/host/codex/agents/orkestrel.toml +6 -5
  44. package/dist/host/codex/agents/planner.toml +6 -2
  45. package/dist/host/codex/agents/reviewer.toml +7 -2
  46. package/dist/host/codex/config.toml +11 -2
  47. package/dist/host/dotfiles/prettierignore +3 -0
  48. package/dist/host/guides/src/scaffold.md +42 -12
  49. package/dist/host/manifest.json +80 -9
  50. package/dist/src/core/index.cjs +162 -14
  51. package/dist/src/core/index.cjs.map +1 -1
  52. package/dist/src/core/index.d.cts +17 -6
  53. package/dist/src/core/index.d.ts +17 -6
  54. package/dist/src/core/index.js +162 -15
  55. package/dist/src/core/index.js.map +1 -1
  56. package/dist/src/server/index.cjs +9 -3
  57. package/dist/src/server/index.cjs.map +1 -1
  58. package/dist/src/server/index.d.cts +2 -1
  59. package/dist/src/server/index.d.ts +2 -1
  60. package/dist/src/server/index.js +10 -4
  61. package/dist/src/server/index.js.map +1 -1
  62. package/package.json +1 -1
  63. package/dist/host/agents/skills/orkestrel-build-application/references/application.md +0 -129
  64. package/dist/host/claude/agents/application.md +0 -30
  65. package/dist/host/codex/agents/application.toml +0 -25
@@ -0,0 +1,609 @@
1
+ # Bootstrap 5 Deep Reference — Theming, Forms, JS, Accessibility, Enterprise Patterns
2
+
3
+ > Part of the `enterprise-bootstrap` package. Bootstrap **5.3.x**.
4
+ > Component markup lookups: [components.md](components.md). Utility classes: [utilities.md](utilities.md).
5
+ > This file holds what those don't: setup, color modes, theming/tokens, forms in
6
+ > production, the JS lifecycle, accessibility depth, and enterprise app patterns.
7
+
8
+ ## Contents
9
+
10
+ - [Quick start](#quick-start)
11
+ - [Breakpoints & layout](#breakpoints--layout)
12
+ - [Color modes (light / dark / custom)](#color-modes-light--dark--custom)
13
+ - [Theming & design tokens](#theming--design-tokens)
14
+ - [Forms in production](#forms-in-production)
15
+ - [JavaScript lifecycle](#javascript-lifecycle)
16
+ - [Accessibility](#accessibility)
17
+ - [Enterprise patterns](#enterprise-patterns) — [App shell](#app-shell) · [Dense data tables](#dense-data-tables) · [Filter & search bars](#filter--search-bars) · [Wizards & multi-step forms](#wizards--multi-step-forms) · [The five states](#the-five-states) · [Feedback discipline](#feedback-discipline) · [Destructive actions](#destructive-actions)
18
+ - [RTL](#rtl)
19
+ - [Print](#print)
20
+ - [Performance](#performance)
21
+ - [When not to hand-roll](#when-not-to-hand-roll)
22
+ - [Common layout patterns](#common-layout-patterns)
23
+
24
+ ## Quick Start
25
+
26
+ CDN (5.3.8 is the current — and final — 5.3.x patch before 5.4):
27
+
28
+ ```html
29
+ <!doctype html>
30
+ <html lang="en">
31
+ <head>
32
+ <meta charset="utf-8" />
33
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
34
+ <title>App</title>
35
+ <link
36
+ href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css"
37
+ rel="stylesheet"
38
+ />
39
+ </head>
40
+ <body>
41
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
42
+ </body>
43
+ </html>
44
+ ```
45
+
46
+ - `bootstrap.bundle.min.js` includes Popper (needed by dropdowns, tooltips, popovers). Without the bundle, load `@popperjs/core` yourself first.
47
+ - Under a strict CSP, add per-file SRI `integrity` hashes — regenerate them from the CDN for the exact version; never copy hashes across versions.
48
+ - In a project with a bundler, prefer the installed `bootstrap` package (and its Sass source) over the CDN — see [Performance](#performance).
49
+
50
+ ## Breakpoints & Layout
51
+
52
+ | Breakpoint | Class Infix | Dimensions |
53
+ | ----------- | ----------- | ---------- |
54
+ | Extra small | (none) | <576px |
55
+ | Small | `sm` | ≥576px |
56
+ | Medium | `md` | ≥768px |
57
+ | Large | `lg` | ≥992px |
58
+ | Extra large | `xl` | ≥1200px |
59
+ | XXL | `xxl` | ≥1400px |
60
+
61
+ ```html
62
+ <div class="container">Fixed-width responsive container</div>
63
+ <div class="container-fluid">Full-width container (app shells)</div>
64
+ <div class="container-md">100% until md, then fixed</div>
65
+
66
+ <div class="container">
67
+ <div class="row">
68
+ <div class="col">Auto-width column</div>
69
+ <div class="col-6">6 of 12 columns</div>
70
+ <div class="col-md-4">4 columns on md+</div>
71
+ </div>
72
+ <div class="row row-cols-1 row-cols-md-3 g-4">
73
+ <div class="col">Equal cards per row, 1 → 3 across breakpoints</div>
74
+ </div>
75
+ </div>
76
+
77
+ <div class="row g-0">No gutters</div>
78
+ <div class="row g-3">1rem gutters</div>
79
+ <div class="row gx-5 gy-3">Independent horizontal/vertical gutters</div>
80
+ ```
81
+
82
+ Mobile first: write for the smallest screen, add `sm`/`md`/`lg`/`xl`/`xxl` modifiers upward. Test every breakpoint you claim.
83
+
84
+ ## Color Modes (light / dark / custom)
85
+
86
+ The 5.3 color-mode system replaces the old per-component dark variants.
87
+
88
+ ### Mechanics
89
+
90
+ - `data-bs-theme="light|dark"` on `<html>` sets the mode globally; on any element it scopes the mode to that subtree (nested scopes win over ancestors). Default is light.
91
+ - Mode switching works by re-pointing root CSS variables — components never change their own rules. Core variables swapped per mode: `--bs-body-bg`, `--bs-body-color`, `--bs-emphasis-color`, `--bs-secondary-color`, `--bs-secondary-bg`, `--bs-tertiary-color`, `--bs-tertiary-bg`, `--bs-border-color`, `--bs-heading-color`, `--bs-link-color`.
92
+ - Each theme color also gets a mode-adaptive triplet — `--bs-{color}-text-emphasis`, `--bs-{color}-bg-subtle`, `--bs-{color}-border-subtle` — surfaced as `.text-{color}-emphasis`, `.bg-{color}-subtle`, `.border-{color}-subtle`. These are the workhorses for status UI that must read in both modes.
93
+ - **The triplet is a recipe, not a guarantee.** `text-{color}-emphasis` on `bg-{color}-subtle` is _designed_ to pass, and it usually does in stock Bootstrap — but the values are tokens, and a compatible skin redefines them. Resolve the actual computed values from the compiled cascade the page loads (the shipped CSS, dependency stylesheets included) and measure each pairing once per theme before you rely on it. A class with no rule of its own may still inherit one; documentation memory decides nothing.
94
+ - Deprecated by this system: `.navbar-dark`, `.dropdown-menu-dark`, `.btn-close-white`, `.carousel-dark` → put `data-bs-theme="dark"` on the component or an ancestor instead.
95
+
96
+ ### Author rules
97
+
98
+ - Paint custom CSS from `var(--bs-…)` — never hard-coded hex — so both modes track automatically.
99
+ - Prefer `bg-body`, `bg-body-secondary`, `bg-body-tertiary` and `text-body`, `text-body-secondary` over `bg-white`/`bg-light`/`text-dark`, which freeze a mode.
100
+ - A dark region inside a light page (or vice versa) is one attribute: `<footer data-bs-theme="dark">`.
101
+
102
+ ### Theme toggle
103
+
104
+ Bootstrap ships **no** mode picker — you build the toggle. The essentials: read the stored preference, fall back to `prefers-color-scheme`, set `data-bs-theme` on `document.documentElement`, and do it in a script early in `<head>` so the first paint doesn't flash the wrong mode.
105
+
106
+ ```js
107
+ const stored = localStorage.getItem('theme')
108
+ const preferred =
109
+ stored ?? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
110
+ document.documentElement.setAttribute('data-bs-theme', preferred)
111
+ // On toggle: setAttribute + localStorage.setItem("theme", value)
112
+ ```
113
+
114
+ ### Custom modes
115
+
116
+ A custom mode is a named scope overriding the same variables:
117
+
118
+ ```css
119
+ [data-bs-theme='midnight'] {
120
+ --bs-body-bg: #0b1020;
121
+ --bs-body-color: #dfe4f2;
122
+ --bs-tertiary-bg: #131a30;
123
+ --bs-border-color: #26304f;
124
+ }
125
+ [data-bs-theme='midnight'] .dropdown-menu {
126
+ --bs-dropdown-bg: var(--bs-tertiary-bg);
127
+ }
128
+ ```
129
+
130
+ In Sass: `$enable-dark-mode` (default true), `$color-mode-type: data` (attribute selectors) or `media-query` (`prefers-color-scheme` — loses per-component scoping), and the `@include color-mode(dark) { … }` mixin. Dark defaults live in `_variables-dark.scss`.
131
+
132
+ ## Theming & Design Tokens
133
+
134
+ ### The three-tier token model
135
+
136
+ Enterprise theming survives rebrands and dark mode only when tokens are tiered:
137
+
138
+ 1. **Primitives** — raw values (`--brand-blue-600`, a spacing scale). Never referenced by component CSS directly.
139
+ 2. **Semantic tokens** — intent (`--bs-primary`, `--bs-body-bg`, `--bs-border-color`, `--bs-danger`). Reference primitives.
140
+ 3. **Component tokens** — one component's knobs (`--bs-btn-bg`, `--bs-card-spacer-y`). Reference semantics.
141
+
142
+ **In Bootstrap, the `--bs-*` variables ARE your semantic and component layers.** Define your primitives, map them onto `--bs-*`, and let components read only `var(--bs-…)`. Dark mode then becomes a re-point of semantic tokens under `[data-bs-theme="dark"]` — if dark mode ever requires editing a component rule, the tier boundary leaked. Name semantics by role, never appearance (`--surface-sunken`, not `--gray-100`): a value change must never force a rename. Raw hex scattered in component CSS is a primitive referenced directly — the root cause of un-themable UI.
143
+
144
+ ### The CSS-variables-only path (no Sass build)
145
+
146
+ Every component exposes local `--bs-{component}-*` variables (`--bs-btn-color`, `--bs-card-bg`, `--bs-nav-link-padding-x`, `--bs-table-bg`, …). The documented no-build theming route — right for consuming the CDN build:
147
+
148
+ ```css
149
+ :root {
150
+ --bs-primary: #6f42c1; /* note: utility classes derived at build */
151
+ --bs-primary-rgb: 111, 66, 193; /* time need the -rgb partner updated too */
152
+ }
153
+ .btn-brand {
154
+ --bs-btn-bg: var(--bs-primary);
155
+ --bs-btn-color: #fff;
156
+ --bs-btn-hover-bg: color-mix(in srgb, var(--bs-primary), black 10%);
157
+ }
158
+ ```
159
+
160
+ Overriding component variables in a scope beats high-specificity override rules every time: it composes with color modes, keeps specificity flat, and documents intent.
161
+
162
+ ### The Sass path (compiled builds)
163
+
164
+ Import order matters — override maps **before** the files that consume them:
165
+
166
+ ```scss
167
+ @import 'bootstrap/scss/functions';
168
+ // your $variable overrides here ($primary, $font-family-base, $border-radius…)
169
+ @import 'bootstrap/scss/variables';
170
+ @import 'bootstrap/scss/variables-dark';
171
+ // your map overrides here ($theme-colors, $spacers, $grid-breakpoints…)
172
+ @import 'bootstrap/scss/maps';
173
+ @import 'bootstrap/scss/mixins';
174
+ @import 'bootstrap/scss/root';
175
+ // …only the parts you use…
176
+ @import 'bootstrap/scss/utilities';
177
+ @import 'bootstrap/scss/utilities/api'; // generates utilities — keep LAST
178
+ ```
179
+
180
+ Feature flags worth knowing: `$enable-dark-mode`, `$enable-rounded`, `$enable-shadows`, `$enable-gradients`, `$enable-rfs` (fluid type), `$enable-validation-icons`, `$enable-negative-margins`, `$enable-important-utilities`, `$enable-reduced-motion`. Custom theme colors added to `$theme-colors` also need entries in the subtle/emphasis maps (`$theme-colors-text`, `$theme-colors-bg-subtle`, `$theme-colors-border-subtle` — and their `-dark` twins) to get full color-mode support.
181
+
182
+ ### Utilities API
183
+
184
+ All utilities generate from the `$utilities` Sass map — extend the system instead of writing one-off CSS. Definition keys: `property`, `values` (required), plus `class`, `state`, `responsive`, `rfs`, `print`, `rtl`, `css-var`, `local-vars`.
185
+
186
+ ```scss
187
+ // After functions/variables/variables-dark/maps/mixins/utilities:
188
+ $utilities: map-merge(
189
+ $utilities,
190
+ (
191
+ 'cursor': (
192
+ property: cursor,
193
+ class: cursor,
194
+ values: auto pointer grab,
195
+ ),
196
+ // modify an existing one, e.g. make width responsive:
197
+ 'width': map-merge(
198
+ map-get($utilities, 'width'),
199
+ (
200
+ responsive: true,
201
+ )
202
+ ),
203
+ )
204
+ );
205
+ @import 'bootstrap/scss/utilities/api';
206
+ ```
207
+
208
+ Remove with `map-remove($utilities, "width")` or set the key to `null`. This is the sanctioned answer when the shipped scale is missing a step (e.g. a `vh-50` the design truly needs).
209
+
210
+ ## Forms in Production
211
+
212
+ ### Layout & labels
213
+
214
+ - **Top-aligned labels by default** — the evidence (eye-tracking form research) shows fastest completion and the cleanest single-column scan, and they survive narrow screens without reflow. Reserve left-aligned labels for dense read-back forms where vertical compression matters more than speed.
215
+ - Visible label or `.form-floating` — never placeholder-only (disappears on input, fails accessibility).
216
+ - **Don't say the same thing twice.** When the host already names the request — a card heading, a dialog title, a section header stating the question — the form associates with that name via `aria-labelledby` instead of repeating the prompt in its own label. Repetition reads as two different questions to a screen-reader user and as clutter to everyone else.
217
+ - One column beats multi-column for completion; use the form grid (`row g-3` + `col-md-*`) only for genuinely paired fields (city/state/zip).
218
+
219
+ ```html
220
+ <form class="row g-3">
221
+ <div class="col-md-6">
222
+ <label for="inputEmail4" class="form-label">Email</label>
223
+ <input type="email" class="form-control" id="inputEmail4" aria-describedby="emailHelp" />
224
+ <div id="emailHelp" class="form-text">Work address preferred.</div>
225
+ </div>
226
+ <div class="col-md-6">
227
+ <label for="inputPassword4" class="form-label">Password</label>
228
+ <input type="password" class="form-control" id="inputPassword4" />
229
+ </div>
230
+ <div class="col-12">
231
+ <button type="submit" class="btn btn-primary">Sign in</button>
232
+ </div>
233
+ </form>
234
+ ```
235
+
236
+ ### Validation timing (the rules that matter)
237
+
238
+ - Validate a field **on blur** — after the user leaves it — never on every keystroke, and never before the user has reached the field. Exception: live feedback that _helps_ while typing (password strength, username availability, character counts).
239
+ - Once a field is in an error state, re-validate as the user types so they see the fix land.
240
+ - Always re-check everything on submit. Keep the submit button **enabled** — a disabled submit hides _what's_ wrong; a validating submit shows it.
241
+ - On failed submit of a long form, render an **error summary** at the top (focus it; link each item to its field) _and_ inline messages at each field — never summary-only, never inline-only.
242
+ - Error style = color + icon + text, stating what's wrong and how to fix it. Wire message to field with `aria-describedby`, mark the field `aria-invalid="true"`. Never report errors via tooltip-on-hover.
243
+
244
+ ### Bootstrap validation mechanics
245
+
246
+ Client-side, the documented pattern:
247
+
248
+ ```html
249
+ <form class="needs-validation" novalidate>
250
+ <div class="mb-3">
251
+ <label for="name" class="form-label">First name</label>
252
+ <input type="text" class="form-control" id="name" required />
253
+ <div class="invalid-feedback">Enter your first name.</div>
254
+ </div>
255
+ <button class="btn btn-primary" type="submit">Submit</button>
256
+ </form>
257
+
258
+ <script>
259
+ ;(() => {
260
+ 'use strict'
261
+ const forms = document.querySelectorAll('.needs-validation')
262
+ Array.from(forms).forEach((form) => {
263
+ form.addEventListener(
264
+ 'submit',
265
+ (event) => {
266
+ if (!form.checkValidity()) {
267
+ event.preventDefault()
268
+ event.stopPropagation()
269
+ }
270
+ form.classList.add('was-validated')
271
+ },
272
+ false,
273
+ )
274
+ })
275
+ })()
276
+ </script>
277
+ ```
278
+
279
+ **Documented limitation (enterprise-critical):** Bootstrap's client-side validation styles and `valid/invalid-tooltip`s are **not exposed to assistive technologies**. For accessible flows use the server-side pattern — apply `.is-invalid` / `.is-valid` directly (no `.was-validated` parent needed), with `.invalid-feedback` linked via `aria-describedby` — or rely on native browser validation.
280
+
281
+ ```html
282
+ <input
283
+ type="text"
284
+ class="form-control is-invalid"
285
+ id="username"
286
+ aria-describedby="usernameFeedback"
287
+ aria-invalid="true"
288
+ required
289
+ />
290
+ <div id="usernameFeedback" class="invalid-feedback">
291
+ Choose a username — letters and digits only.
292
+ </div>
293
+ ```
294
+
295
+ Details: input groups with feedback need `.has-validation` on the group (border-radius fix). `.valid/invalid-tooltip` variants need a `position-relative` parent. Validation colors are mode-adaptive via `--bs-form-valid-color`, `--bs-form-valid-border-color`, `--bs-form-invalid-color`, `--bs-form-invalid-border-color`.
296
+
297
+ ### Autosave vs explicit save
298
+
299
+ - **Autosave** (with a visible "Saved" status) for continuous low-risk editing: drafts, preferences, notes.
300
+ - **Explicit, pessimistic save** for audited, transactional, or financial records — a deliberate server-confirmed commit, no optimistic success on data that must be validated and logged.
301
+
302
+ ## JavaScript Lifecycle
303
+
304
+ ### Initialization
305
+
306
+ - Data-attribute components initialize from markup. **Tooltips and popovers are opt-in** — construct them; toasts are hidden until shown ([components.md](components.md) → JavaScript initialization).
307
+ - Constructors accept an element or a CSS selector string: `new bootstrap.Modal('#myModal', options)`.
308
+ - Prefer `getOrCreateInstance` when an instance may already exist; `getInstance` returns `null` if none:
309
+
310
+ ```js
311
+ const modal = bootstrap.Modal.getOrCreateInstance('#confirm', { backdrop: 'static' })
312
+ modal.show()
313
+ ```
314
+
315
+ ### Events & async behavior
316
+
317
+ - Event pairs per component: infinitive fires at start and is cancelable (`show.bs.modal` → `event.preventDefault()`); past participle fires after the transition completes (`shown.bs.modal`, `hidden.bs.modal`, `shown.bs.collapse`, …).
318
+ - **All methods are asynchronous** — they return before the transition ends, and a method called on a transitioning component is ignored. Sequence work off the completion events, not timers.
319
+
320
+ ### Teardown — SPAs and dynamic views
321
+
322
+ - `dispose()` destroys the instance and its DOM data. Call it when the host element leaves the DOM (route change, list re-render), or instances and listeners leak. Dispose only after any transition finishes:
323
+
324
+ ```js
325
+ el.addEventListener('hidden.bs.modal', () => {
326
+ bootstrap.Modal.getInstance(el)?.dispose()
327
+ })
328
+ ```
329
+
330
+ - **Framework reality check:** Bootstrap's JS and a virtual-DOM framework both mutating the same nodes causes bugs (stuck dropdowns, ghost backdrops). In React/Vue/Angular apps, prefer the framework-native implementations (React Bootstrap, BootstrapVueNext, ng-bootstrap) which reuse Bootstrap's CSS but own the DOM. Use raw `bootstrap.*` JS in SPAs only for leaf widgets you fully control, and dispose them on unmount.
331
+
332
+ ### Popper
333
+
334
+ Dropdowns, tooltips, and popovers require Popper — load `bootstrap.bundle.min.js` (includes it) or `@popperjs/core` before `bootstrap.min.js`. Modal, collapse, offcanvas, toast, tab, alert don't need it.
335
+
336
+ ## Accessibility
337
+
338
+ ### Baseline (every screen)
339
+
340
+ - Skip link (`.visually-hidden-focusable`) to `<main>`; landmarks (`nav`, `main`, `aside`); heading order `h1 → h2 → h3` without skips.
341
+ - `aria-label` on icon-only controls; `aria-current="page"` on active nav; `aria-selected` on active tabs; `aria-expanded` + `aria-controls` on disclosure triggers; `aria-describedby` for help/errors.
342
+ - Visible focus everywhere — keep Bootstrap's rings; use the `.focus-ring` helper on custom interactive elements instead of removing outlines.
343
+ - Meaning never by color alone: pair status color with text, an icon, or a visually-hidden word.
344
+ - Body text contrast ≥ 4.5:1 (3:1 for large text and UI graphics). Bootstrap's own docs warn parts of the default palette can fall short — verify your combinations; don't assume framework colors pass.
345
+
346
+ **Measuring the bars (the part that gets skipped):**
347
+
348
+ - Measure in **both themes**, from the compiled cascade, not from the token names. A pairing that passes in light routinely fails in dark, and a skin's values are its own.
349
+ - **≥ 4.5:1** for everything information-bearing — including `small`, captions, meta lines, and timestamps. Small size is not a licence for a lower tier; `text-body-tertiary` is decoration, `text-body-secondary` is the floor for text a user must read.
350
+ - **≥ 3:1** for textless marks and state indicators — status glyphs, rings, dividers that carry meaning — and for the hover/focus chrome that signals state. Focus rings and hover fills are UI graphics: they are in scope, and they are the ones nobody measures.
351
+ - Disabled controls are exempt from the bars by the spec. That exemption is about legibility, not about meaning: see [Destructive actions](#destructive-actions) for the one disabled state that still has to change color.
352
+ - `.visually-hidden` for SR-only text; `.visually-hidden-focusable` for skip links (never combine the two).
353
+
354
+ ### WCAG 2.2 deltas that bite dense app UI
355
+
356
+ - **Target size ≥ 24×24 CSS px (2.5.8, AA).** Icon buttons, row actions, close buttons, sort carets, checkbox hit-areas. A smaller visual target passes if a 24px spacing circle around it stays undisturbed — so in tight `table-sm` toolbars, pad the hit area rather than enlarging the glyph.
357
+ - **Focus not obscured (2.4.11, AA).** Sticky headers/footers/action bars and toast overlays must not bury the focused element. Reserve space with `scroll-margin-top` on focusables (or `scroll-padding-top` on the scroll container) equal to the sticky chrome height.
358
+ - **Dragging alternatives (2.5.7, AA).** Any drag (row reorder, kanban, slider, resize) needs a non-drag single-pointer path: move up/down buttons, numeric input, click-to-place.
359
+ - **Accessible authentication (3.3.8, AA).** Never block paste in password/OTP fields; support password managers; no puzzle as the only way in.
360
+ - **Redundant entry (3.3.7, A).** Don't ask for the same information twice in one flow — auto-fill or offer "same as above". Governs wizards directly.
361
+ - **Consistent help (3.2.6, A).** If a help affordance repeats across pages, keep it in the same relative place everywhere.
362
+ - Housekeeping: SC 4.1.1 Parsing was removed in 2.2 — duplicate-ID lint is no longer a WCAG failure by itself (still fix it).
363
+
364
+ ### Pattern contracts (APG, compact)
365
+
366
+ - **Dialog/modal:** `role="dialog"` (`alertdialog` for destructive confirms) + `aria-modal="true"` + `aria-labelledby`. Focus moves in on open, Tab is trapped, Esc closes, focus returns to the invoker. Bootstrap's modal does this — verify you didn't break focus-return by removing the trigger.
367
+ - **Tabs:** `tablist` > `tab` (+`aria-selected`, `aria-controls`) with panels `tabpanel`. Roving tabindex: Left/Right between tabs, Home/End to ends; only the selected tab is `tabindex="0"`.
368
+ - **Disclosure:** a `<button>` with `aria-expanded` + `aria-controls`. Enter/Space toggles. That's the whole contract — Bootstrap collapse matches it.
369
+ - **Radio group:** prefer native `<input type="radio" name>` — one tab stop and arrow-selection come free.
370
+ - **Combobox:** input `role="combobox"` + `aria-expanded` + `aria-controls` + `aria-activedescendant` tracking the active option; Down opens/advances, Enter accepts, Esc closes. This is the hardest contract on the list — see [When not to hand-roll](#when-not-to-hand-roll).
371
+ - **Toolbar:** `role="toolbar"` + `aria-label`; one tab stop, arrows move between controls (roving tabindex). Use it to collapse a dense button cluster's tab-stops.
372
+ - **Table vs grid:** semantics follow _interaction_, not looks — see [Dense data tables](#dense-data-tables).
373
+
374
+ ### Focus management in SPAs
375
+
376
+ Browsers handle focus on full page loads; in an SPA **you** do:
377
+
378
+ - On route change, move focus to the new view's `h1` (or the `<main>` with `tabindex="-1"`) so SR users hear where they landed.
379
+ - On failed submit, focus the error summary. On destructive confirm, focus the dialog's safe action.
380
+ - After deleting a row, move focus to a sensible neighbor (next row / the table region), never let it fall to `<body>`.
381
+ - Anything focused programmatically under sticky chrome needs the `scroll-margin-top` offset (2.4.11 above).
382
+
383
+ ### Reduced motion
384
+
385
+ Bootstrap wraps its transitions and animations (`.fade`, `.collapsing`, carousel slide, spinner speed) in `prefers-reduced-motion: reduce` handling — transitions are disabled or slowed automatically when `$enable-reduced-motion` is on (default). Your obligations: wrap **custom** animation in `@media (prefers-reduced-motion: no-preference)`, don't auto-play movement for reduced-motion users, and keep any purely decorative motion cuttable.
386
+
387
+ ## Enterprise Patterns
388
+
389
+ ### App shell
390
+
391
+ **Structure:** persistent left sidebar for dense apps with many top-level destinations (it scales, nests, and stays stable while content changes); top-bar-only nav for shallow apps (≤ ~5 destinations). A collapsible sidebar reclaims width for data.
392
+
393
+ The Bootstrap implementation — a responsive offcanvas that renders inline above `lg` and becomes a drawer below it, with no custom JS:
394
+
395
+ ```html
396
+ <body>
397
+ <a class="visually-hidden-focusable" href="#main">Skip to main content</a>
398
+ <header class="navbar bg-body-tertiary border-bottom sticky-top">
399
+ <div class="container-fluid">
400
+ <button
401
+ class="btn btn-outline-secondary d-lg-none"
402
+ type="button"
403
+ data-bs-toggle="offcanvas"
404
+ data-bs-target="#appSidebar"
405
+ aria-controls="appSidebar"
406
+ aria-label="Open navigation"
407
+ >
408
+
409
+ </button>
410
+ <a class="navbar-brand" href="/">Product</a>
411
+ <div class="d-flex align-items-center gap-2"><!-- search, account --></div>
412
+ </div>
413
+ </header>
414
+
415
+ <div class="d-flex">
416
+ <div
417
+ class="offcanvas-lg offcanvas-start border-end"
418
+ tabindex="-1"
419
+ id="appSidebar"
420
+ aria-labelledby="appSidebarLabel"
421
+ >
422
+ <div class="offcanvas-header">
423
+ <h5 class="offcanvas-title" id="appSidebarLabel">Navigation</h5>
424
+ <button
425
+ type="button"
426
+ class="btn-close"
427
+ data-bs-dismiss="offcanvas"
428
+ data-bs-target="#appSidebar"
429
+ aria-label="Close"
430
+ ></button>
431
+ </div>
432
+ <div class="offcanvas-body d-lg-block p-lg-3" style="width: 260px;">
433
+ <nav aria-label="Primary">
434
+ <ul class="nav nav-pills flex-column gap-1">
435
+ <li class="nav-item">
436
+ <a class="nav-link active" aria-current="page" href="#">Dashboard</a>
437
+ </li>
438
+ <li class="nav-item"><a class="nav-link" href="#">Accounts</a></li>
439
+ <li class="nav-item"><a class="nav-link" href="#">Reports</a></li>
440
+ </ul>
441
+ </nav>
442
+ </div>
443
+ </div>
444
+ <main id="main" class="flex-grow-1 p-3 p-lg-4" style="min-width: 0;">
445
+ <!-- min-width: 0 lets tables shrink instead of blowing out the flex row -->
446
+ </main>
447
+ </div>
448
+ </body>
449
+ ```
450
+
451
+ **Navigation rules:**
452
+
453
+ - Breadcrumbs only for genuinely hierarchical models (org → account → contact); in flat or tabbed apps they're noise. Current item: `aria-current="page"`, not a link.
454
+ - A command palette (Ctrl/Cmd-K) is an accelerator **on top of** visible nav, never a replacement — everything it exposes needs a discoverable UI route too.
455
+ - Keyboard shortcuts: use conventional bindings (Ctrl/Cmd-K palette, `/` focuses search, `?` opens the shortcut cheat-sheet), surface them in tooltips, and never let single-key shortcuts fire while an input has focus.
456
+ - Dashboard composition follows the same craft as any screen: state the screen's single job, lead with the numbers that answer it, and keep every widget to one job — a dashboard is not a place to exhibit every chart type.
457
+
458
+ ### Dense data tables
459
+
460
+ **Semantics first — table vs grid.** Default to a static `<table>`: links and buttons inside cells ride the natural tab order and screen readers get real table navigation free. Reserve `role="grid"` for _editable, cell-interactive_ spreadsheet-like UIs — grid means you now own roving tabindex and full arrow-key cell navigation. Never bolt `role="grid"` onto a read-only table because it "looks like a data grid": semantics follow interaction, not appearance.
461
+
462
+ **Craft rules:**
463
+
464
+ - **Align by type:** numbers, currency, dates right-aligned (`text-end`, header too); text left. Use tabular figures so digits stack into comparable columns: `font-variant-numeric: tabular-nums` on numeric cells (one small custom rule that earns its place).
465
+ - **Density:** `table-sm` for compact; offer density as a user toggle (comfortable/compact) driven by one token or wrapper class, not per-cell tweaks. Don't shrink font below readability to fake density.
466
+ - **Sticky header** once the table meaningfully scrolls (roughly a viewport / ~15+ rows). Not built into Bootstrap — the pattern:
467
+
468
+ ```html
469
+ <div class="table-responsive" style="max-height: 70vh;">
470
+ <table class="table table-sm align-middle">
471
+ <thead class="sticky-top">
472
+ <tr>
473
+ <th scope="col" class="bg-body-secondary">…</th>
474
+ </tr>
475
+ </thead>
476
+
477
+ </table>
478
+ </div>
479
+ ```
480
+
481
+ Give header cells an **opaque background** (`bg-body-secondary` or a table variant) — table backgrounds are transparent by default, so rows show through a sticky header otherwise. Sticky chrome is the prime Focus-Not-Obscured offender: add `scroll-margin-top` on row focusables equal to the header height. Sticky first column only when row identity is lost on horizontal scroll — it costs paint and complexity.
482
+
483
+ - **Sorting:** the whole header is a button (not a bare caret), with a visible direction indicator, and `aria-sort="ascending|descending"` on the active `<th>` only:
484
+
485
+ ```html
486
+ <th scope="col" aria-sort="ascending">
487
+ <button type="button" class="btn btn-link p-0 fw-semibold text-body text-decoration-none">
488
+ Amount <span aria-hidden="true">↑</span>
489
+ </button>
490
+ </th>
491
+ ```
492
+
493
+ - **Row actions:** 1–3 high-frequency actions inline; the rest behind a per-row kebab (dropdown). Hover-only reveal fails touch and keyboard — keep at least the overflow trigger always visible and ≥24px.
494
+ - **Selection & bulk actions:** header checkbox with indeterminate state for partial selection; per-row checkboxes with `aria-label` naming the row ("Select INV-1042"). When selection > 0, swap the toolbar's content in place for a contextual bar — "3 selected", the batch actions, and a clear-selection escape — never push the layout down (layout-shifting chrome is an anti-pattern). Announce the count via a polite live region.
495
+ - **Pagination vs scrolling:** paginate when users need position, totals, deep links, and "go to page N" — most enterprise CRUD. Virtualize (windowed rendering) for long uniform lists where scrolling is natural. True infinite scroll is for exploratory feeds only — never where users need a footer or a findable end.
496
+ - **Responsive, ranked:** (1) _priority columns_ — hide low-value columns per breakpoint (`d-none d-lg-table-cell`), always keeping the identifying + decision columns; (2) _horizontal scroll_ (`table-responsive`) when every column matters — remember it clips dropdowns; (3) _card-ify_ into label:value stacks below `md` for low row counts. Never card-ify a wide comparison table — comparison is the point.
497
+ - **Table states:** loading → **skeleton rows** matching the real column count/widths (a centered spinner collapses the layout); empty → distinguish _no data yet_ (invite the first action) from _no results for these filters_ (offer "Clear filters"); error → inline retry inside the table region, header and toolbar preserved.
498
+
499
+ ### Filter & search bars
500
+
501
+ - One toolbar above the table: search input first (`role="search"` on the form), then the 2–4 highest-value filters as `form-select`/segmented controls, overflow filters behind a "Filters" button (offcanvas on mobile, dropdown/collapse on desktop).
502
+ - **Active filters must be visible and dismissible** — chips/badges with an ✕ and a "Clear all" — users must see _why_ the list is short. A filtered-empty state repeats the escape hatch.
503
+ - Debounce live search; show result counts ("128 results") so feedback is immediate; filter state belongs in the URL when views are shareable.
504
+ - Toolbars that overflow: `flex-nowrap overflow-auto` beats wrapping into a two-row toolbar mid-task — don't crush icon targets below 24px.
505
+
506
+ ### Wizards & multi-step forms
507
+
508
+ - Show step progress: current position, total, and step names ("Step 2 of 4 — Billing"); `list-group-numbered` or a simple nav renders it honestly.
509
+ - Validate per-step before advancing — failing forward compounds; failing at the end is worse.
510
+ - Back never loses data. Persist partial state (save-and-resume) for anything beyond ~3 steps or that crosses sessions.
511
+ - Never re-ask what a previous step collected (Redundant Entry, 3.3.7) — carry it forward or offer "same as above".
512
+ - Last step: a review summary with per-section edit links, then one clearly-named commit action ("Create account", not "Submit").
513
+
514
+ ### The five states
515
+
516
+ Design **all five** for every data surface: ideal (populated), empty, loading, partial, error. A component isn't done until all five exist — empty and error are where trust is won or lost.
517
+
518
+ - **Skeleton vs spinner:** skeleton (`placeholder` + `placeholder-glow`) when you know the content's shape and it fills a region — tables, cards, detail panes — because it holds layout and shortens perceived wait. Spinner for short, indeterminate, or in-control waits (inside a button, a small inline fetch).
519
+ - **Thresholds (guidance):** under ~1s show nothing — a flashed loader is worse than none; ~1–10s show a spinner or skeleton; beyond ~10s show determinate progress (percent or step) so it doesn't feel hung.
520
+ - **Optimistic vs pessimistic:** apply UI immediately and reconcile (rolling back loudly on failure) for reversible high-frequency actions — toggles, stars, reorders. Await confirmation for money, audited records, and anything a rollback would confuse.
521
+ - **Empty states** invite the next action (button + one line of why), and filtered-empty differs from never-had-data.
522
+ - **Every error state carries a keyboard-reachable retry** and preserves surrounding context — a body fetch failure must not blow away the toolbar and filters.
523
+
524
+ ### Feedback discipline
525
+
526
+ | Channel | Use for | Never for |
527
+ | ----------------------------- | -------------------------------------------------------------------------------- | -------------------------------------------------- |
528
+ | **Toast** | Transient confirmation of a just-completed action; auto-dismiss; `role="status"` | Errors needing action; anything the user must read |
529
+ | **Inline alert** | Feedback tied to a specific field/section/action; persists in context | App-wide conditions |
530
+ | **Banner** (page-level alert) | Persistent page/app conditions — outage, trial expiring, permissions | Action confirmations |
531
+ | **Modal / alertdialog** | Blocking decisions the user must resolve now | FYIs, success messages |
532
+
533
+ Blocking errors are never toasts. Keep the acting verb consistent across the flow: the "Publish" button confirms with "Published".
534
+
535
+ ### Destructive actions
536
+
537
+ Match friction to reversibility × blast radius:
538
+
539
+ 1. **Undo** (soft-delete + toast with Undo) for reversible, low-stakes, frequent actions — least friction, best experience. Prefer making actions undoable over interrupting them.
540
+ 2. **Confirm dialog** for irreversible-but-scoped operations. Restate the specific consequence ("This permanently deletes 3 invoices"), verb-labeled buttons ("Delete invoices" / "Cancel" — never Yes/No), destructive action visually separated from safe; `alertdialog` semantics; focus lands on the safe action.
541
+ 3. **Type-to-confirm** (type the entity name) only for high-blast-radius irreversible operations — delete an org, drop a dataset.
542
+
543
+ Don't type-gate a single-row delete; don't one-tap a tenant wipe. Overused confirmations train the click-through reflex that defeats them.
544
+
545
+ **A disabled destructive control stops looking destructive.** `btn-danger` at full saturation reads as armed and available whatever the `disabled` attribute says — the loudest control on the screen is the one the user cannot use, and the contrast exemption for disabled controls does not excuse it. While the action is unavailable, neutralize the variant (drop to the neutral/outline variant, or let the disabled state mute the fill) so the color stops promising an action, and say _why_ it is unavailable in text the assistive layer reaches: `aria-describedby` pointing at the reason, with `title` only as the pointer-user convenience on top. A `title` alone is not an explanation — it never reaches a keyboard or screen-reader user, and it disappears on touch.
546
+
547
+ ## RTL
548
+
549
+ - Enable per page: `<html lang="ar" dir="rtl">` + the RTL stylesheet `bootstrap.rtl.min.css` (built from the same source via RTLCSS). RTL support is documented as experimental.
550
+ - The logical properties model is why the utilities say start/end: `ms-*`/`me-*`, `ps-*`/`pe-*`, `text-start`/`text-end`, `float-start`/`float-end`, `offcanvas-start`/`end` all flip automatically. **Never write `left`/`right` positioning or physical margins in custom CSS** — use logical properties (`margin-inline-start`, `inset-inline-end`) so your custom rules flip too.
551
+ - Caveats: shipping LTR+RTL simultaneously costs significant extra CSS; the breadcrumb divider needs `$breadcrumb-divider-flipped`; source Sass can embed RTLCSS directives (`/* rtl: … */`) for value swaps like font stacks.
552
+
553
+ ## Print
554
+
555
+ - Hide chrome, keep the data: `d-print-none` on nav, sidebars, toolbars, action buttons; the report/table itself stays printable.
556
+ - `d-print-block`/`d-print-table` can resurface content hidden on screen (a print-only header with report title/date).
557
+ - Print-check data screens users will export: collapse interactive affordances (sort carets, checkboxes) via `d-print-none`, and prefer `table-bordered` legibility over hover/stripe effects that may not print.
558
+
559
+ ## Performance
560
+
561
+ - **Ship one CSS system, not two.** Bootstrap plus a second framework (or a parallel bespoke layer) doubles payload and guarantees specificity fights.
562
+ - **Compressed, the full build is cheap; incomplete builds are not.** Trimming via a Sass-subset build (import only the parts used — see [Theming](#theming--design-tokens)) is the sanctioned diet. Aggressive purge tools are the risky one: Bootstrap adds classes **at runtime** (`show`, `showing`, `fade`, `collapsing`, `modal-open`, `modal-backdrop`, `offcanvas-backdrop`, tooltip/popover generated markup) — purging without safelisting them ships UIs whose modals silently stop rendering. If you purge, safelist every JS-toggled class and test every overlay.
563
+ - **Icons:** Bootstrap Icons is a separate package — prefer inline SVG or an SVG sprite (crisp, styleable via `currentColor`, no font flash) over the icon font; load only the icons used.
564
+ - **JS:** the bundle is small, but only load it where behavior exists; per-component ESM imports (`bootstrap/js/dist/modal`) trim further in bundlers.
565
+ - **Fonts:** each display face is a payload decision; subset and `font-display: swap` characterful faces, and let the data face fall back to the system stack when the brief allows.
566
+
567
+ ## When Not to Hand-Roll
568
+
569
+ Bootstrap has **no** combobox/autocomplete, date picker, multi-select tags input, data grid, or tree view. The boundary rule:
570
+
571
+ - **Reach for native first:** `<input type="date">`, `<datalist>` for light autocomplete, `<select multiple>` where acceptable. Native widgets bring keyboard and AT behavior free.
572
+ - **Reach for an established accessible library second** when the product genuinely needs the richer widget (combobox with async search, spreadsheet grid, drag-reorder tree). Budget for auditing it against the APG contract.
573
+ - **Hand-roll last**, only with the APG contract in hand ([Accessibility](#accessibility) → Pattern contracts) and time to implement the _keyboard_ half — the visual half is the easy 20%.
574
+ - Never fake it: a `.dropdown-menu` posing as a select, a `<div>` grid with click handlers, or a scroll-anchor "wizard" each break keyboard and AT users in ways a demo never shows.
575
+
576
+ ## Common Layout Patterns
577
+
578
+ ### Centered content
579
+
580
+ ```html
581
+ <div class="d-flex justify-content-center align-items-center vh-100">
582
+ <div>Centered content</div>
583
+ </div>
584
+ ```
585
+
586
+ ### Sticky footer
587
+
588
+ ```html
589
+ <body class="d-flex flex-column min-vh-100">
590
+ <main class="flex-grow-1">Content</main>
591
+ <footer>Footer</footer>
592
+ </body>
593
+ ```
594
+
595
+ ### Equal height columns
596
+
597
+ ```html
598
+ <div class="row">
599
+ <div class="col-md-6"><div class="card h-100">Equal height</div></div>
600
+ <div class="col-md-6"><div class="card h-100">Equal height</div></div>
601
+ </div>
602
+ ```
603
+
604
+ ### Responsive visibility
605
+
606
+ ```html
607
+ <div class="d-none d-md-block">Hidden on mobile, visible md+</div>
608
+ <div class="d-md-none">Visible only below md</div>
609
+ ```