@orkestrel/scaffold 0.0.64 → 0.0.66
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.
- package/README.md +11 -1
- package/dist/host/agents/skills/enterprise-bootstrap/SKILL.md +202 -175
- package/dist/host/agents/skills/enterprise-bootstrap/references/bootstrap-reference.md +347 -94
- package/dist/host/agents/skills/enterprise-bootstrap/references/color-modes.md +356 -0
- package/dist/host/agents/skills/enterprise-bootstrap/references/components.md +100 -45
- package/dist/host/agents/skills/enterprise-bootstrap/references/frontend-design.md +295 -101
- package/dist/host/agents/skills/enterprise-bootstrap/references/inputs.md +60 -41
- package/dist/host/agents/skills/enterprise-bootstrap/references/inspection.md +255 -132
- package/dist/host/agents/skills/enterprise-bootstrap/references/responsive-layout.md +237 -0
- package/dist/host/agents/skills/enterprise-bootstrap/references/utilities.md +111 -17
- package/dist/host/claude/agents/orkestrel.md +24 -24
- package/dist/host/claude/rules/workspace.md +2 -2
- package/dist/host/claude/settings.json +1 -1
- package/dist/host/claude/skills/enterprise-bootstrap/SKILL.md +11 -10
- package/dist/host/guides/scaffold.md +63 -22
- package/dist/host/manifest.json +26 -14
- package/dist/host/scripts/ollama.sh +322 -13
- package/dist/src/core/index.cjs +34 -13
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +31 -9
- package/dist/src/core/index.d.ts +31 -9
- package/dist/src/core/index.js +33 -14
- package/dist/src/core/index.js.map +1 -1
- package/package.json +5 -5
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Bootstrap 5 Deep Reference — Theming, Forms, JS, Accessibility, Enterprise Patterns
|
|
2
2
|
|
|
3
|
-
> Part of the `enterprise-bootstrap`
|
|
3
|
+
> Part of the `enterprise-bootstrap` skill. Bootstrap **5.3.x**.
|
|
4
4
|
> Component markup lookups: [components.md](components.md). Utility classes: [utilities.md](utilities.md).
|
|
5
5
|
> This file holds what those do not: setup, color modes, theming/tokens, forms in
|
|
6
6
|
> production, the JS lifecycle, accessibility depth, and enterprise app patterns.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
- [Quick start](#quick-start)
|
|
11
11
|
- [Breakpoints & layout](#breakpoints--layout)
|
|
12
12
|
- [Color modes (light / dark / custom)](#color-modes-light--dark--custom)
|
|
13
|
-
- [Theming & design tokens](#theming--design-tokens)
|
|
13
|
+
- [Theming & design tokens](#theming--design-tokens) — [Define the working scales](#define-the-working-scales) · [Elevation and depth](#elevation-and-depth) · [Layout and type extensions](#layout-and-type-extensions)
|
|
14
14
|
- [Forms in production](#forms-in-production)
|
|
15
15
|
- [JavaScript lifecycle](#javascript-lifecycle)
|
|
16
16
|
- [Accessibility](#accessibility)
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
## Quick Start
|
|
25
25
|
|
|
26
|
-
CDN (5.3.8
|
|
26
|
+
Pinned CDN example (5.3.8). Prefer the installed compatible version; this is not an upgrade instruction:
|
|
27
27
|
|
|
28
28
|
```html
|
|
29
29
|
<!doctype html>
|
|
@@ -49,6 +49,22 @@ CDN (5.3.8 is the current — and final — 5.3.x patch before 5.4):
|
|
|
49
49
|
|
|
50
50
|
## Breakpoints & Layout
|
|
51
51
|
|
|
52
|
+
Stock 5.3.8 values: breakpoints `sm` 576 · `md` 768 · `lg` 992 · `xl` 1200 · `xxl` 1400 px (`min-width`; `xs` has no infix); `.container` maxima 540 · 720 · 960 · 1140 · 1320 px; gutter and container padding 1.5 rem (296 px of content at 320 px). Only `d`, `flex`, `justify-content`, `align-*`, `order`, `float`, `gap`, spacing, text alignment, and `object-fit` utilities ship breakpoint infixes. Generate a missing responsive role only where a `$utilities` entry owns the property, per [Utilities API](#utilities-api). Full inventory and recipes: [responsive-layout.md](responsive-layout.md) → Bootstrap's responsive surface.
|
|
53
|
+
|
|
54
|
+
Start with the feature's content and narrow layout, then choose its container and breakpoints.
|
|
55
|
+
Take the region contract, content parity, and test matrix from [responsive-layout.md](responsive-layout.md).
|
|
56
|
+
Use fluid columns for content that needs to scale together; keep rails, forms, and reading measures
|
|
57
|
+
bounded where it does not. A full-width shell does not require full-width text or fields.
|
|
58
|
+
Take the missing role-based utilities from [Layout and type extensions](#layout-and-type-extensions).
|
|
59
|
+
|
|
60
|
+
Give a form, dialog, or login card a content-led maximum and let it shrink only when the viewport
|
|
61
|
+
is narrower: `w-100 mx-auto measure-form`, not `col-md-8 offset-md-2 col-lg-6 offset-lg-3`, whose
|
|
62
|
+
width changes at every breakpoint and is narrower on `lg` than at some `md` widths. Give a
|
|
63
|
+
sidebar a fixed rail (`shell-rail-lg-fixed flex-shrink-0`) beside a flexible `min-inline-0` main
|
|
64
|
+
region, not `col-3`, which grows on wide screens and collapses below its minimum on narrow ones.
|
|
65
|
+
Put supporting explanation beside a narrow form in a second column rather than widening its
|
|
66
|
+
fields. Percentage widths belong only where columns must scale together.
|
|
67
|
+
|
|
52
68
|
| Breakpoint | Class Infix | Dimensions |
|
|
53
69
|
| ----------- | ----------- | ---------- |
|
|
54
70
|
| Extra small | (none) | <576px |
|
|
@@ -85,77 +101,162 @@ The 5.3 color-mode system replaces the old per-component `*-dark` classes.
|
|
|
85
101
|
|
|
86
102
|
### Mechanics
|
|
87
103
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
- 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.
|
|
104
|
+
Read [color-modes.md](color-modes.md) before choosing color classes or repairing a theme failure.
|
|
105
|
+
It owns inheritance, adaptive-versus-fixed families, surface boundaries, and component exceptions.
|
|
106
|
+
Use the installed build's attribute or media-query strategy; do not assume every `--bs-*` variable
|
|
107
|
+
changes with the mode.
|
|
93
108
|
|
|
94
109
|
### Author rules
|
|
95
110
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
111
|
+
Preserve ordinary text inheritance and native component states. Prefer adaptive body/subtle
|
|
112
|
+
surfaces. Establish an explicit foreground only at an owned solid or mode boundary, or for a
|
|
113
|
+
measured role that requires it. Do not turn every subtle panel into a custom color pair.
|
|
99
114
|
|
|
100
115
|
### Theme toggle
|
|
101
116
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const stored = localStorage.getItem('theme')
|
|
106
|
-
const preferred =
|
|
107
|
-
stored ?? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
|
|
108
|
-
document.documentElement.setAttribute('data-bs-theme', preferred)
|
|
109
|
-
// On toggle: setAttribute + localStorage.setItem("theme", value)
|
|
110
|
-
```
|
|
117
|
+
Reuse the host controller. When implementing one, follow [Scope the mode](color-modes.md#scope-the-mode)
|
|
118
|
+
for validated preference, automatic-mode resolution, storage failure, first paint, and overlay
|
|
119
|
+
mounts. Bootstrap ships no picker; an attribute example is not a complete controller.
|
|
111
120
|
|
|
112
121
|
### Custom modes
|
|
113
122
|
|
|
114
|
-
|
|
123
|
+
Add a custom mode only when the brief requires it. Map its used body, surface, link, border,
|
|
124
|
+
validation, and component-state variables, including RGB companions. Resolve embedded component
|
|
125
|
+
images and `color-scheme` where relevant. Do not claim a complete mode from a partial token block.
|
|
126
|
+
|
|
127
|
+
In Sass, use `$enable-dark-mode`, `$color-mode-type: data` for local attribute scopes, or
|
|
128
|
+
`media-query` for system-driven mode without per-component scoping. Use `color-mode()` rather than
|
|
129
|
+
competing selectors; keep overrides in the host theme source.
|
|
130
|
+
|
|
131
|
+
## Theming & Design Tokens
|
|
132
|
+
|
|
133
|
+
### The tiered token model
|
|
134
|
+
|
|
135
|
+
Keep literal values in declared primitives, map primitives to semantic roles, and let component
|
|
136
|
+
variables consume those roles. Reuse Bootstrap's `--bs-*` semantic and component layers rather
|
|
137
|
+
than adding a parallel palette. Name semantics by purpose, not a particular shade.
|
|
138
|
+
|
|
139
|
+
Distinguish token structure from runtime behavior. Bootstrap also generates fixed values through
|
|
140
|
+
Sass; a component variable is not necessarily mode-adaptive. Map the actual consumer, including its
|
|
141
|
+
states, and resolve aliases at the scope where they must change. Take the constraints from
|
|
142
|
+
[Extend the theme](color-modes.md#extend-the-theme).
|
|
143
|
+
|
|
144
|
+
### Define the working scales
|
|
145
|
+
|
|
146
|
+
Reuse the installed theme and its scales first. Declare new values only for a role the feature
|
|
147
|
+
needs; refine one shared definition instead of accumulating per-component exceptions. Every
|
|
148
|
+
system in the following table has a Bootstrap source, a utility, and a known gap; extend the source, never the
|
|
149
|
+
markup.
|
|
150
|
+
|
|
151
|
+
| System | Sass source | Utility | Stock steps (default root) | Gap |
|
|
152
|
+
| -------------- | ----------------------------------------------------------- | --------------------------------------------- | ------------------------------------------------ | -------------------------------------------------------------------------------- |
|
|
153
|
+
| Font size | `$font-sizes`, `$h1…h6-font-size`, `$display-font-sizes` | `fs-1…6`, `.h1…h6`, `display-1…6` | 16 · 20 · 24 · 28 · 32 · 40 px; display 40–80 px | No `rem` step below 16 px; `.small` is `.875em` |
|
|
154
|
+
| Font weight | `$font-weight-*`, `$headings-font-weight` | `fw-light…bold` | 300 · 400 · 500 · 600 · 700; headings 500 | Headings barely heavier than body |
|
|
155
|
+
| Line height | `$line-height-*`, `$headings-line-height` | `lh-1`, `lh-sm`, `lh-base`, `lh-lg` | 1 · 1.25 · 1.5 · 2; headings 1.2 | — |
|
|
156
|
+
| Color | `$gray-100…900`, `$blue-100…900` …, `$theme-colors`, triads | `text-*`, `bg-*`, `border-*` families | 9 shades per hue; subtle/emphasis per role | Ramps are mechanical mixes ([color-modes.md](color-modes.md) → Extend the theme) |
|
|
157
|
+
| Spacing | `$spacers` | `m-*`, `p-*`, `gap-*`, `g-*` | 0 · 4 · 8 · 16 · 24 · 48 px | No 12 or 32 px; nothing above 48 px |
|
|
158
|
+
| Width | `$container-max-widths`, the grid | `w-*`, `mw-100`, `col-*` | 25 / 50 / 75 / 100 % | No content-led maximums |
|
|
159
|
+
| Shadow | `$box-shadow`, `-sm`, `-lg`, `-inset` | `shadow-sm`, `shadow`, `shadow-lg` | 3 steps | Single-layer; modal shares the dropdown step |
|
|
160
|
+
| Radius | `$border-radius*`, `$enable-rounded` | `rounded-0…5`, `-pill`, `-circle` | 0 · 4 · 6 · 8 · 16 · 32 px | — |
|
|
161
|
+
| Border width | `$border-widths` | `border-1…5` | 1–5 px | Sets every side at once |
|
|
162
|
+
| Opacity | — | `opacity-*`, `text-opacity-*`, `bg-opacity-*` | 0 · 10 · 25 · 50 · 75 · 100 | Not a text tier |
|
|
163
|
+
| Letter-spacing | — | none | — | Generate ([Layout and type extensions](#layout-and-type-extensions)) |
|
|
164
|
+
|
|
165
|
+
- **Color:** neutral, brand, and required status/categorical ramps. Pick base, light surface, and
|
|
166
|
+
dark text shades in real components, then fill the gaps. Use HSL when it helps tune related
|
|
167
|
+
shades; keep the project's existing format. Review fixed shade pairs in each theme rather than
|
|
168
|
+
generating a new `lighten`, `darken`, or `color-mix` result at each use site. Stock ramps and
|
|
169
|
+
triads are tint/shade mixes with a fixed hue; override the shade variables and triad
|
|
170
|
+
variables per brand hue, and the greys as one temperature-matched set
|
|
171
|
+
([color-modes.md](color-modes.md) → Extend the theme).
|
|
172
|
+
- **Type:** display/body/utility roles, finite `rem` sizes, working weights, and line-height per
|
|
173
|
+
role. Roles may share a font. RFS scales sizes above 1.25 rem down below a 1200 px viewport
|
|
174
|
+
(`h1`–`h4`, `display-*`, `fs-1`–`fs-4`); body, `fs-5`, `fs-6`, `.lead`, and controls hold — do not
|
|
175
|
+
fight it with `em` heading sizes. Take 14 px and 12 px roles from generated `fs-sm`/`fs-xs`, not
|
|
176
|
+
nested `.small`. Never globally scale body text down to make a display treatment fit.
|
|
177
|
+
- **Space and size:** internal, group, panel, and section gaps; control sizes; reading/form widths;
|
|
178
|
+
rail width. Start with Bootstrap's shipped scale. Add a missing step through the utilities API
|
|
179
|
+
only where the adjacent steps cannot express the intended relationship. Button sizes already
|
|
180
|
+
scale padding faster than font (4/8 px at 14 px, 6/12 at 16, 8/16 at 20); use the shipped
|
|
181
|
+
sizes rather than deriving one with `em` padding.
|
|
182
|
+
- **Radius and elevation:** a small consistent family, assigned to real component/layer roles.
|
|
183
|
+
Set `$border-radius` once and let components inherit it; do not hand-mix `rounded-*` per
|
|
184
|
+
element. Reuse component variables and shadow utilities ([Elevation and depth](#elevation-and-depth)).
|
|
185
|
+
No shadow is a valid surface role.
|
|
186
|
+
|
|
187
|
+
Record these roles in the existing token source or a compact design contract, not a second design
|
|
188
|
+
system. Keep literal colors and raw scale values in named primitive definitions; component rules
|
|
189
|
+
consume semantic or component tokens. [inspection.md](inspection.md) → Token discipline checks
|
|
190
|
+
that boundary; [frontend-design.md](frontend-design.md) owns the visual choices.
|
|
191
|
+
|
|
192
|
+
### Elevation and depth
|
|
193
|
+
|
|
194
|
+
Bootstrap ships `--bs-box-shadow-sm` (`0 .125rem .25rem` at .075), `--bs-box-shadow`
|
|
195
|
+
(`0 .5rem 1rem` at .15), and `--bs-box-shadow-lg` (`0 1rem 3rem` at .175), plus
|
|
196
|
+
`--bs-box-shadow-inset`. Assign by z-position: `sm` for raised cards and controls, base for
|
|
197
|
+
floating menus and a dragged item, `lg` for dialogs. Stock dropdowns, popovers, toasts, and
|
|
198
|
+
modals all sit on `--bs-box-shadow` (modal: `-sm` below 576 px), which puts a blocking dialog at
|
|
199
|
+
dropdown elevation. Lift it at the extension rung, in the project stylesheet after Bootstrap's so
|
|
200
|
+
the rule wins the `sm`-up media rule:
|
|
115
201
|
|
|
116
202
|
```css
|
|
117
|
-
|
|
118
|
-
--bs-
|
|
119
|
-
--bs-body-color: #dfe4f2;
|
|
120
|
-
--bs-tertiary-bg: #131a30;
|
|
121
|
-
--bs-border-color: #26304f;
|
|
122
|
-
}
|
|
123
|
-
[data-bs-theme='midnight'] .dropdown-menu {
|
|
124
|
-
--bs-dropdown-bg: var(--bs-tertiary-bg);
|
|
203
|
+
.modal {
|
|
204
|
+
--bs-modal-box-shadow: var(--bs-box-shadow-lg);
|
|
125
205
|
}
|
|
126
206
|
```
|
|
127
207
|
|
|
128
|
-
|
|
208
|
+
Two-part shadows — a broad cast plus a tight contact shadow that fades with elevation — are a
|
|
209
|
+
token change: redefine `$box-shadow-sm`, `$box-shadow`, and `$box-shadow-lg` as two-layer values
|
|
210
|
+
and every consumer follows.
|
|
129
211
|
|
|
130
|
-
|
|
212
|
+
`$enable-shadows: true` (off by default) paints light-from-above on controls: buttons take
|
|
213
|
+
`inset 0 1px 0 rgba(#fff, .15), 0 1px 1px rgba(#000, .075)` (lit top edge, tight cast shadow),
|
|
214
|
+
inputs `inset 0 1px 2px rgba(#000, .075)` (recessed), and an active button `inset 0 3px 5px`
|
|
215
|
+
(pressed). Enable it when the direction wants tactile controls, and verify every declared theme; the alphas
|
|
216
|
+
are fixed white and black. Without the flag the `box-shadow` mixin emits nothing, so
|
|
217
|
+
`--bs-btn-box-shadow` and `--bs-box-shadow-inset` have no consumer and an extension-rung override
|
|
218
|
+
does nothing; the recipe is then a proposed rule.
|
|
131
219
|
|
|
132
|
-
|
|
220
|
+
Flat depth: a `bg-body` panel on `bg-body-tertiary` reads raised and `bg-body-secondary` inside
|
|
221
|
+
`bg-body` reads inset, both mode-adaptive with no shadow. A hard offset shadow is a `$box-shadow`
|
|
222
|
+
override.
|
|
133
223
|
|
|
134
|
-
|
|
224
|
+
Overlap: `position-relative translate-middle-y`, or `mt-n*` after `$enable-negative-margins`.
|
|
225
|
+
Ring overlapping images in the surface color through the border variable so the ring follows the
|
|
226
|
+
mode, where `border-white` does not:
|
|
135
227
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
228
|
+
```css
|
|
229
|
+
.ring-body {
|
|
230
|
+
--bs-border-color: var(--bs-body-bg);
|
|
231
|
+
}
|
|
232
|
+
```
|
|
139
233
|
|
|
140
|
-
|
|
234
|
+
Then `rounded-circle border border-3 ring-body`.
|
|
141
235
|
|
|
142
236
|
### The CSS-variables-only path (no Sass build)
|
|
143
237
|
|
|
144
|
-
|
|
238
|
+
When the deliverable is one self-contained HTML file, the project stylesheet is a single `<style>` block in `<head>` — tokens, declared role utilities, and extension-rung variable overrides, in that order — loaded after the Bootstrap `<link>` so equal-specificity rules win by source order. It is still one stylesheet: no `style` attribute on authored markup and no second `<style>` block beside a component ([SKILL.md](../SKILL.md) → The styling ladder owns that placement rule).
|
|
239
|
+
|
|
240
|
+
Use native components and adaptive utilities before adding overrides. For a recurring component
|
|
241
|
+
surface role, use its local variable rather than repainting the whole component. This optional
|
|
242
|
+
project-defined class changes the card background without assigning a foreground:
|
|
145
243
|
|
|
146
244
|
```css
|
|
147
|
-
|
|
148
|
-
--bs-
|
|
149
|
-
--bs-primary-rgb: 111, 66, 193; /* time need the -rgb partner updated too */
|
|
150
|
-
}
|
|
151
|
-
.btn-brand {
|
|
152
|
-
--bs-btn-bg: var(--bs-primary);
|
|
153
|
-
--bs-btn-color: #fff;
|
|
154
|
-
--bs-btn-hover-bg: color-mix(in srgb, var(--bs-primary), black 10%);
|
|
245
|
+
.card-quiet {
|
|
246
|
+
--bs-card-bg: var(--bs-tertiary-bg);
|
|
155
247
|
}
|
|
156
248
|
```
|
|
157
249
|
|
|
158
|
-
|
|
250
|
+
Use `class="card card-quiet"` only after declaring that extension in the host theme stylesheet.
|
|
251
|
+
Do not add it when `card bg-body-tertiary` already expresses the requirement. Measure the card's
|
|
252
|
+
inherited foreground and its header/footer layers in the loaded skin.
|
|
253
|
+
|
|
254
|
+
When a custom button variant is required, define rest, hover, focus, active/checked, and disabled
|
|
255
|
+
component variables as one contract. Include borders and the focus-ring RGB value; test busy
|
|
256
|
+
content without changing geometry. Do not generate a custom tinted button merely to distinguish a
|
|
257
|
+
secondary action, and do not assume reversing a subtle/emphasis pair produces valid states.
|
|
258
|
+
Changing root `--bs-primary` alone does not rebuild Sass-generated button states or utility RGB
|
|
259
|
+
consumers; follow [Extend the theme](color-modes.md#extend-the-theme).
|
|
159
260
|
|
|
160
261
|
### The Sass path (compiled builds)
|
|
161
262
|
|
|
@@ -175,7 +276,7 @@ Import order matters — override maps **before** the files that consume them:
|
|
|
175
276
|
@import 'bootstrap/scss/utilities/api'; // generates utilities — keep LAST
|
|
176
277
|
```
|
|
177
278
|
|
|
178
|
-
Feature flags worth knowing: `$enable-dark-mode`, `$enable-rounded`, `$enable-shadows
|
|
279
|
+
Feature flags worth knowing: `$enable-dark-mode`, `$enable-rounded`, `$enable-shadows` (light-from-above button/input/active shadows, [Elevation and depth](#elevation-and-depth)), `$enable-gradients` (a white fade on every `bg-*`; not a two-hue gradient), `$enable-rfs` (sizes above 1.25 rem shrink below 1200 px), `$enable-validation-icons`, `$enable-negative-margins` (`mt-n*` for overlap), `$enable-important-utilities`, `$enable-reduced-motion`. For added theme colors, extend the light/dark emphasis and subtle maps and inspect the generated utility-value maps; see [Extend the theme](color-modes.md#extend-the-theme). Do not infer a generated class from a token alone.
|
|
179
280
|
|
|
180
281
|
### Utilities API
|
|
181
282
|
|
|
@@ -191,7 +292,7 @@ $utilities: map-merge(
|
|
|
191
292
|
class: cursor,
|
|
192
293
|
values: auto pointer grab,
|
|
193
294
|
),
|
|
194
|
-
//
|
|
295
|
+
// Modify an existing utility: make width responsive.
|
|
195
296
|
'width': map-merge(
|
|
196
297
|
map-get($utilities, 'width'),
|
|
197
298
|
(
|
|
@@ -205,23 +306,131 @@ $utilities: map-merge(
|
|
|
205
306
|
|
|
206
307
|
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 (for example, a `vh-50` the design truly needs).
|
|
207
308
|
|
|
309
|
+
**`responsive: true` reaches a utility family and nothing else.** It adds breakpoint infixes to one
|
|
310
|
+
entry in the `$utilities` map, so it generates `w-md-auto` from the `width` entry and `ls-lg-tight`
|
|
311
|
+
from an added `letter-spacing` entry. A component threshold (`navbar-expand-*`, `offcanvas-*`,
|
|
312
|
+
`table-responsive-*`, `modal-fullscreen-*-down`), a grid class, and a helper (`visually-hidden`,
|
|
313
|
+
`stretched-link`, `ratio`, `vstack`) are not `$utilities` entries, so the key cannot reach them —
|
|
314
|
+
change the component's own breakpoint class instead, and never write a responsive helper name the
|
|
315
|
+
map cannot produce. Read the generated selector out of the compiled output before using it.
|
|
316
|
+
|
|
317
|
+
### Layout and type extensions
|
|
318
|
+
|
|
319
|
+
These are **project-generated classes**, not stock Bootstrap utilities. Use the existing project
|
|
320
|
+
roles when present. Otherwise add only the needed entries; the values in the following block are illustrative role
|
|
321
|
+
definitions, not universal sizes.
|
|
322
|
+
|
|
323
|
+
Scale steps go in the map-override slot (after `variables-dark`, before `maps`). Keys `0`–`5`
|
|
324
|
+
keep their shipped meaning; an intermediate 12 px or 32 px step is a new key, never a decimal.
|
|
325
|
+
`$font-sizes` feeds only the `fs-*` utility in 5.3.8, and RFS leaves values at or below 1.25 rem
|
|
326
|
+
alone, so extending it is safe:
|
|
327
|
+
|
|
328
|
+
```scss
|
|
329
|
+
$spacers: map-merge(
|
|
330
|
+
$spacers,
|
|
331
|
+
(
|
|
332
|
+
6: $spacer * 4,
|
|
333
|
+
7: $spacer * 6,
|
|
334
|
+
)
|
|
335
|
+
); // 64px section gap, 96px page rhythm
|
|
336
|
+
$font-sizes: map-merge(
|
|
337
|
+
$font-sizes,
|
|
338
|
+
(
|
|
339
|
+
sm: 0.875rem,
|
|
340
|
+
xs: 0.75rem,
|
|
341
|
+
)
|
|
342
|
+
); // fs-sm 14px captions, fs-xs 12px eyebrows only
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Role utilities go after importing `utilities` and before `utilities/api`:
|
|
346
|
+
|
|
347
|
+
```scss
|
|
348
|
+
$utilities: map-merge(
|
|
349
|
+
$utilities,
|
|
350
|
+
(
|
|
351
|
+
'content-measure': (
|
|
352
|
+
property: max-inline-size,
|
|
353
|
+
class: measure,
|
|
354
|
+
values: (
|
|
355
|
+
prose: 65ch,
|
|
356
|
+
form: 36rem,
|
|
357
|
+
),
|
|
358
|
+
),
|
|
359
|
+
'shell-rail': (
|
|
360
|
+
property: inline-size,
|
|
361
|
+
class: shell-rail,
|
|
362
|
+
responsive: true,
|
|
363
|
+
values: (
|
|
364
|
+
fixed: 16rem,
|
|
365
|
+
),
|
|
366
|
+
),
|
|
367
|
+
'inline-minimum': (
|
|
368
|
+
property: min-inline-size,
|
|
369
|
+
class: min-inline,
|
|
370
|
+
values: (
|
|
371
|
+
0: 0,
|
|
372
|
+
),
|
|
373
|
+
),
|
|
374
|
+
'table-viewport': (
|
|
375
|
+
property: max-block-size,
|
|
376
|
+
class: max-block,
|
|
377
|
+
responsive: true,
|
|
378
|
+
values: (
|
|
379
|
+
table: 70vh,
|
|
380
|
+
),
|
|
381
|
+
),
|
|
382
|
+
'tabular-figures': (
|
|
383
|
+
property: font-variant-numeric,
|
|
384
|
+
class: figures,
|
|
385
|
+
values: (
|
|
386
|
+
tabular: tabular-nums,
|
|
387
|
+
),
|
|
388
|
+
),
|
|
389
|
+
'letter-spacing': (
|
|
390
|
+
property: letter-spacing,
|
|
391
|
+
class: ls,
|
|
392
|
+
values: (
|
|
393
|
+
tight: -0.02em,
|
|
394
|
+
wide: 0.05em,
|
|
395
|
+
),
|
|
396
|
+
),
|
|
397
|
+
)
|
|
398
|
+
);
|
|
399
|
+
// Generate once, after all utility-map additions:
|
|
400
|
+
@import 'bootstrap/scss/utilities/api';
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
Use `w-100 measure-form` for a bounded form, `measure-prose` for a reading column,
|
|
404
|
+
`shell-rail-lg-fixed flex-shrink-0` for an inline desktop rail, `min-inline-0` for its flexible
|
|
405
|
+
sibling, `max-block-lg-table` only for a warranted wide-screen bounded table scroller, `figures-tabular` for comparable
|
|
406
|
+
quantities, `ls-tight` on `display-*` and `fs-1`, and `ls-wide` with `text-uppercase` labels (`em`
|
|
407
|
+
is correct for tracking: it follows the element's own size). Verify those selectors in the
|
|
408
|
+
compiled output before using those classes. The font must support tabular figures. A `ch`
|
|
409
|
+
measure is a starting width, not a character-count proof.
|
|
410
|
+
|
|
411
|
+
Without a Sass build, take an existing equivalent; otherwise propose the smallest stylesheet rule
|
|
412
|
+
under [SKILL.md](../SKILL.md) → When custom CSS is justified. Never ship an unresolved utility name.
|
|
413
|
+
|
|
208
414
|
## Forms in Production
|
|
209
415
|
|
|
210
416
|
### Layout & labels
|
|
211
417
|
|
|
212
|
-
- **Top-aligned labels by default** —
|
|
418
|
+
- **Top-aligned labels by default** — keep a consistent single-column scan. Reserve side labels for a deliberate dense layout that still reads correctly at narrow widths.
|
|
213
419
|
- Visible label or `.form-floating` — never placeholder-only (disappears on input, fails accessibility).
|
|
214
|
-
- **
|
|
215
|
-
-
|
|
420
|
+
- **Name the form without duplicating its heading.** Associate the form with an existing visible title through `aria-labelledby` where useful. Keep each control's own visible label; a form name does not label its fields.
|
|
421
|
+
- Use one field column by default; add columns only for genuinely related fields. At wide widths,
|
|
422
|
+
put supporting explanation beside the form rather than stretching its fields.
|
|
423
|
+
- Keep each label, control, help text, and error in one group. Use a smaller internal gap than the
|
|
424
|
+
gap to the next field group, and recheck the relationship when errors or long labels wrap.
|
|
216
425
|
|
|
217
426
|
```html
|
|
218
427
|
<form class="row g-3">
|
|
219
|
-
<div class="col-
|
|
428
|
+
<div class="col-12">
|
|
220
429
|
<label for="inputEmail4" class="form-label">Email</label>
|
|
221
430
|
<input type="email" class="form-control" id="inputEmail4" aria-describedby="emailHelp" />
|
|
222
431
|
<div id="emailHelp" class="form-text">Work address preferred.</div>
|
|
223
432
|
</div>
|
|
224
|
-
<div class="col-
|
|
433
|
+
<div class="col-12">
|
|
225
434
|
<label for="inputPassword4" class="form-label">Password</label>
|
|
226
435
|
<input type="password" class="form-control" id="inputPassword4" />
|
|
227
436
|
</div>
|
|
@@ -234,8 +443,9 @@ Remove with `map-remove($utilities, "width")` or set the key to `null`. This is
|
|
|
234
443
|
### Validation timing (the rules that matter)
|
|
235
444
|
|
|
236
445
|
- 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).
|
|
237
|
-
-
|
|
238
|
-
- Always re-check everything on submit. Keep the submit button **enabled** — a disabled submit hides _what is_ wrong; a validating submit shows it.
|
|
446
|
+
- After a field enters an error state, re-validate as the user types so they see the fix land.
|
|
447
|
+
- Always re-check everything on submit. Keep the submit button **enabled** while fields are invalid — a disabled submit hides _what is_ wrong; a validating submit shows it.
|
|
448
|
+
- **Separate invalid from pending.** While a submit is in flight, mark the button busy — `aria-busy="true"`, a `spinner-border spinner-border-sm` inside it, its label held so the geometry does not move — and refuse a second submit from the handler. Blocking a duplicate submit of a form that already validated is a pending state; the preceding rule bars only the disable that stands in for validation. Clear the busy state on both the resolved and the failed path, and put the failure in the error summary.
|
|
239
449
|
- 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.
|
|
240
450
|
- Error style = color + icon + text, stating what is wrong and how to fix it. Wire message to field with `aria-describedby`, mark the field `aria-invalid="true"`. Never report errors through a hover tooltip.
|
|
241
451
|
|
|
@@ -325,7 +535,7 @@ el.addEventListener('hidden.bs.modal', () => {
|
|
|
325
535
|
})
|
|
326
536
|
```
|
|
327
537
|
|
|
328
|
-
- **
|
|
538
|
+
- **In a virtual-DOM app, take the framework-native implementation** — React Bootstrap, BootstrapVueNext, ng-bootstrap — which reuses Bootstrap's CSS and owns the DOM. Use raw `bootstrap.*` JS there only for a leaf widget the component fully controls, and dispose it on unmount. Bootstrap's JS and the framework mutating the same nodes produces stuck dropdowns and ghost backdrops.
|
|
329
539
|
|
|
330
540
|
### Popper
|
|
331
541
|
|
|
@@ -344,23 +554,26 @@ Hold the baseline in [SKILL.md](../SKILL.md) → Accessibility baseline. Its Boo
|
|
|
344
554
|
|
|
345
555
|
**Measuring the bars:**
|
|
346
556
|
|
|
347
|
-
- Hold the bars from [SKILL.md](../SKILL.md) → Surfaces, color, contrast
|
|
348
|
-
- Measure
|
|
557
|
+
- Hold the bars from [SKILL.md](../SKILL.md) → Surfaces, color, contrast, which owns them. WCAG 2.2 permits 3:1 for large text; this skill does not — size grants no lower tier.
|
|
558
|
+
- Measure each declared theme from the compiled cascade, never from token names. A light-theme result does not establish a dark-theme result, and a skin's values are its own.
|
|
349
559
|
- Focus rings and hover fills are UI graphics: they are in scope for the 3:1 bar.
|
|
350
560
|
- Disabled controls are exempt from the bars by the spec. That exemption covers legibility, not meaning — see [Destructive actions](#destructive-actions) for the one disabled state that still has to change color.
|
|
351
561
|
|
|
352
|
-
**The instrument.** Bootstrap paints in translucent layers
|
|
562
|
+
**The instrument.** Refuse a reading from a reader that stops at the first painted ancestor and drops its alpha. Bootstrap paints in translucent layers — a card header and footer are a 3% tint of the body color over the card's own background — so a flattening reader passes an unreadable pairing and fails a readable one. Use a reader that:
|
|
353
563
|
|
|
354
564
|
- collects every painted layer from the element upward to the first opaque one, then composites them top over bottom (Porter-Duff `over`) onto that opaque base;
|
|
355
565
|
- composites a translucent foreground over that result before taking the ratio, rather than reading the declared color;
|
|
356
|
-
- measures
|
|
566
|
+
- measures each declared theme in the same run, because a theme swap re-points the tokens under every layer;
|
|
357
567
|
- carries a negative control drawn from outside the population it covers — a pairing known to fail — and voids the run if that negative control passes.
|
|
358
568
|
|
|
359
|
-
|
|
569
|
+
Include ancestor opacity in the painted stack. For images, gradients, masks, or blend modes the
|
|
570
|
+
reader does not support, use a suitable rendered-background measurement or leave the pairing open;
|
|
571
|
+
never flatten a variable background to its average color. Name reached states beside every result.
|
|
572
|
+
Wire the reader into the suite after it has settled a question.
|
|
360
573
|
|
|
361
|
-
### WCAG 2.2
|
|
574
|
+
### WCAG 2.2 requirements for app UI
|
|
362
575
|
|
|
363
|
-
- **Target size
|
|
576
|
+
- **Target size (2.5.8, AA) — this section owns the skill's target dimensions.** Hold every applicable target at ≥ 24×24 CSS px: icon buttons, row actions, close buttons, sort carets, checkbox hit-areas, and color swatches. A smaller visual target passes only where a 24px spacing circle around it stays undisturbed — so in tight `table-sm` toolbars, pad the hit area rather than enlarging the glyph. Prefer 44×44 CSS px for a primary mobile control. Measure the rendered hit area; never infer it from a size class such as `btn-sm`. Enlarge the button or its associated label, not the icon's surrounding decoration.
|
|
364
577
|
- **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.
|
|
365
578
|
- **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.
|
|
366
579
|
- **Accessible authentication (3.3.8, AA).** Never block paste in password/OTP fields; support password managers; no puzzle as the only way in.
|
|
@@ -383,9 +596,9 @@ Wire the reader into the suite once it has settled a question.
|
|
|
383
596
|
Browsers handle focus on full page loads; in an SPA **you** do:
|
|
384
597
|
|
|
385
598
|
- On route change, move focus to the new view's `h1` (or the `<main>` with `tabindex="-1"`) so SR users hear where they landed.
|
|
386
|
-
- On failed submit, focus the error summary. On destructive confirm, focus the dialog
|
|
599
|
+
- On failed submit, focus the error summary. On a compact destructive confirm, focus the safe action; in a scrolling or structured dialog, focus a static heading at the start when an action would scroll its context away.
|
|
387
600
|
- After deleting a row, move focus to a sensible neighbor (next row / the table region), never let it fall to `<body>`.
|
|
388
|
-
- Anything focused programmatically under sticky chrome needs the `scroll-margin-top` offset (2.4.11
|
|
601
|
+
- Anything focused programmatically under sticky chrome needs the `scroll-margin-top` offset (Focus not obscured, 2.4.11, under [WCAG 2.2 requirements for app UI](#wcag-22-requirements-for-app-ui)).
|
|
389
602
|
|
|
390
603
|
### Reduced motion
|
|
391
604
|
|
|
@@ -395,7 +608,10 @@ Bootstrap wraps its transitions and animations (`.fade`, `.collapsing`, carousel
|
|
|
395
608
|
|
|
396
609
|
### App shell
|
|
397
610
|
|
|
398
|
-
**Structure:**
|
|
611
|
+
**Structure:** reuse the product's shell. For a new product, let implemented features and their
|
|
612
|
+
navigation needs decide between a sidebar and a shallow top bar; do not design the shell first.
|
|
613
|
+
Give an inline rail a content-led width and the task the remaining space. A collapsible rail can
|
|
614
|
+
reclaim width for comparison data.
|
|
399
615
|
|
|
400
616
|
The Bootstrap implementation — a responsive offcanvas that renders inline above `lg` and becomes a drawer below it, with no custom JS:
|
|
401
617
|
|
|
@@ -421,7 +637,7 @@ The Bootstrap implementation — a responsive offcanvas that renders inline abov
|
|
|
421
637
|
|
|
422
638
|
<div class="d-flex">
|
|
423
639
|
<div
|
|
424
|
-
class="offcanvas-lg offcanvas-start border-end"
|
|
640
|
+
class="offcanvas-lg offcanvas-start border-end shell-rail-lg-fixed flex-shrink-0"
|
|
425
641
|
tabindex="-1"
|
|
426
642
|
id="appSidebar"
|
|
427
643
|
aria-labelledby="appSidebarLabel"
|
|
@@ -436,7 +652,7 @@ The Bootstrap implementation — a responsive offcanvas that renders inline abov
|
|
|
436
652
|
aria-label="Close"
|
|
437
653
|
></button>
|
|
438
654
|
</div>
|
|
439
|
-
<div class="offcanvas-body d-lg-block p-lg-3"
|
|
655
|
+
<div class="offcanvas-body d-lg-block p-lg-3">
|
|
440
656
|
<nav aria-label="Primary">
|
|
441
657
|
<ul class="nav nav-pills flex-column gap-1">
|
|
442
658
|
<li class="nav-item">
|
|
@@ -448,8 +664,8 @@ The Bootstrap implementation — a responsive offcanvas that renders inline abov
|
|
|
448
664
|
</nav>
|
|
449
665
|
</div>
|
|
450
666
|
</div>
|
|
451
|
-
<main id="main" class="flex-grow-1 p-3 p-lg-4
|
|
452
|
-
<!-- min-
|
|
667
|
+
<main id="main" class="flex-grow-1 p-3 p-lg-4 min-inline-0">
|
|
668
|
+
<!-- Generated min-inline-0 lets the task shrink inside the flex row. -->
|
|
453
669
|
</main>
|
|
454
670
|
</div>
|
|
455
671
|
</body>
|
|
@@ -464,16 +680,26 @@ The Bootstrap implementation — a responsive offcanvas that renders inline abov
|
|
|
464
680
|
|
|
465
681
|
### Dense data tables
|
|
466
682
|
|
|
467
|
-
**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
|
|
683
|
+
**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 — a grid hands roving tabindex and full arrow-key cell navigation to the implementation. Never bolt `role="grid"` onto a read-only table because it "looks like a data grid": semantics follow interaction, not appearance.
|
|
468
684
|
|
|
469
685
|
**Craft rules:**
|
|
470
686
|
|
|
471
|
-
- **Align by
|
|
687
|
+
- **Align by comparison:** quantities and currency right-aligned (`text-end`, header too), with
|
|
688
|
+
consistent units and precision. Use the generated `figures-tabular` utility or the project's
|
|
689
|
+
equivalent. Keep text start-aligned; choose date alignment by its format and comparison task.
|
|
690
|
+
- **Group related content:** combine identity and supporting detail only when they do not need
|
|
691
|
+
independent column comparison or sorting. Keep key comparison columns explicit. Quiet repeated
|
|
692
|
+
labels and row actions before increasing density.
|
|
472
693
|
- **Density:** `table-sm` for compact; offer density as a user toggle (comfortable/compact) driven by one token or wrapper class, not per-cell tweaks. Do not shrink font below readability to fake density.
|
|
473
|
-
- **Sticky header**
|
|
694
|
+
- **Sticky header** when the table scrolls its own header out of view. Not built into Bootstrap — the pattern:
|
|
474
695
|
|
|
475
696
|
```html
|
|
476
|
-
<div
|
|
697
|
+
<div
|
|
698
|
+
class="table-responsive max-block-lg-table"
|
|
699
|
+
role="region"
|
|
700
|
+
aria-label="Comparison table"
|
|
701
|
+
tabindex="0"
|
|
702
|
+
>
|
|
477
703
|
<table class="table table-sm align-middle">
|
|
478
704
|
<thead class="sticky-top">
|
|
479
705
|
<tr>
|
|
@@ -485,30 +711,30 @@ The Bootstrap implementation — a responsive offcanvas that renders inline abov
|
|
|
485
711
|
</div>
|
|
486
712
|
```
|
|
487
713
|
|
|
488
|
-
|
|
714
|
+
Keep sticky header cells on an **opaque, mode-aware surface** such as `bg-body-secondary`. Stock `.table` cells use the body background; verify that a skin or override has not made them translucent. Do not substitute a `.table-*` color variant and assume it adapts. Inspect cell overlays through [Tables and overlays](color-modes.md#tables-and-overlays). 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.
|
|
489
715
|
|
|
490
716
|
- **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:
|
|
491
717
|
|
|
492
718
|
```html
|
|
493
719
|
<th scope="col" aria-sort="ascending">
|
|
494
|
-
<button type="button" class="btn btn-link p-0 fw-semibold
|
|
720
|
+
<button type="button" class="btn btn-link p-0 fw-semibold">
|
|
495
721
|
Amount <span aria-hidden="true">↑</span>
|
|
496
722
|
</button>
|
|
497
723
|
</th>
|
|
498
724
|
```
|
|
499
725
|
|
|
500
|
-
- **Row actions:**
|
|
726
|
+
- **Row actions:** keep the high-frequency actions inline and put the rest behind a per-row kebab (dropdown). Hover-only reveal fails touch and keyboard — keep at least the overflow trigger always visible and at the floor in [WCAG 2.2 requirements for app UI](#wcag-22-requirements-for-app-ui).
|
|
501
727
|
- **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 through a polite live region.
|
|
502
728
|
- **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.
|
|
503
|
-
- **Responsive,
|
|
729
|
+
- **Responsive, by task:** use a compact record list for record work, a locally scrollable semantic table for essential comparison, or priority columns with an operable detail path. Preserve identity, decision fields, and actions. Choose expansion from available container width, not `md` by habit. Keep one state model across variants; take the contract from [Keep the task intact](responsive-layout.md#keep-the-task-intact).
|
|
504
730
|
- **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.
|
|
505
731
|
|
|
506
732
|
### Filter & search bars
|
|
507
733
|
|
|
508
|
-
- One toolbar above the table: search input first (`role="search"` on the form), then the
|
|
734
|
+
- One toolbar above the table: search input first (`role="search"` on the form), then the highest-value filters as `form-select`/segmented controls, overflow filters behind a "Filters" button (offcanvas on mobile, dropdown/collapse on desktop). Promote a filter to the toolbar because the task reaches for it, not to fill the row.
|
|
509
735
|
- **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.
|
|
510
736
|
- Debounce live search; show result counts ("128 results") so feedback is immediate; filter state belongs in the URL when views are shareable.
|
|
511
|
-
-
|
|
737
|
+
- At the base, give search a full row; stack or wrap actions and filters without shrinking labels or targets. Expand with `col-12 col-md`, `col-md-auto`, or `d-grid d-sm-flex` when they fit. Reserve a horizontal scroller for a documented spatial interaction, not an ordinary toolbar. Keep active filters and the clear path outside any disclosed extras.
|
|
512
738
|
|
|
513
739
|
### Wizards & multi-step forms
|
|
514
740
|
|
|
@@ -516,7 +742,7 @@ Give header cells an **opaque background** (`bg-body-secondary` or a `.table-*`
|
|
|
516
742
|
⟨total⟩", where the wizard fills in its own runtime position and total); `list-group-numbered` or
|
|
517
743
|
a simple nav renders it honestly.
|
|
518
744
|
- Validate per step before advancing; never let a step advance carrying invalid data.
|
|
519
|
-
- Back never loses data. Persist partial state (save-and-resume) for
|
|
745
|
+
- Back never loses data. Persist partial state (save-and-resume) for a long sequence or one that crosses sessions.
|
|
520
746
|
- Never re-ask what a previous step collected (Redundant Entry, 3.3.7) — carry it forward or offer "same as above".
|
|
521
747
|
- Review step: a review summary with per-section edit links, then one clearly-named commit action ("Create account", not "Submit").
|
|
522
748
|
|
|
@@ -525,9 +751,15 @@ Give header cells an **opaque background** (`bg-body-secondary` or a `.table-*`
|
|
|
525
751
|
Design **every one** for every data surface: ideal (populated), empty, loading, partial, error. A component is not done until all of them exist.
|
|
526
752
|
|
|
527
753
|
- **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).
|
|
528
|
-
- **
|
|
754
|
+
- **Wait feedback:** acknowledge the action promptly, avoid flashing a loader for trivial waits, and keep the known layout stable. For longer work, show actual steps or measured progress when available; otherwise state that work continues and offer cancellation where supported. Never invent a percentage.
|
|
529
755
|
- **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.
|
|
530
|
-
- **
|
|
756
|
+
- **First-use empty:** name what belongs here and the useful create/import action. Drop tabs or
|
|
757
|
+
filters only when they genuinely have no data to operate on. An illustration may support that
|
|
758
|
+
action; it must not replace it.
|
|
759
|
+
- **Filtered-empty:** retain the active filters and result context, explain that nothing matched,
|
|
760
|
+
and offer a clear-filter path. Never hide the controls needed to undo the empty result.
|
|
761
|
+
- **Partial:** keep available data readable, identify the missing or stale part, and scope recovery
|
|
762
|
+
to it. Missing is not zero. Do not collapse the whole surface into an error when some data exists.
|
|
531
763
|
- **Every error state states what failed and how to fix it**, carries a keyboard-reachable retry in place, and preserves surrounding context — a body fetch failure must not blow away the toolbar and filters.
|
|
532
764
|
|
|
533
765
|
### Feedback discipline
|
|
@@ -545,10 +777,14 @@ Blocking errors are never toasts. Keep the acting verb consistent across the flo
|
|
|
545
777
|
|
|
546
778
|
Match friction to reversibility × blast radius:
|
|
547
779
|
|
|
548
|
-
1. **Undo** (soft-delete + toast with Undo) for reversible, low-stakes, frequent actions
|
|
549
|
-
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.
|
|
780
|
+
1. **Undo** (soft-delete + toast with Undo) for reversible, low-stakes, frequent actions. Prefer making actions undoable over interrupting them.
|
|
781
|
+
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 for a compact confirmation, or a static top heading when focusing an action would scroll the consequences out of view.
|
|
550
782
|
3. **Type-to-confirm** (type the entity name) only for high-blast-radius irreversible operations — delete an org, drop a dataset.
|
|
551
783
|
|
|
784
|
+
Keep action rank separate from consequence. A row-level destructive action can use a measured
|
|
785
|
+
quiet treatment; emphasize the final destructive commit where the ladder makes it the decision.
|
|
786
|
+
Do not make every row's delete button compete with the page's primary action.
|
|
787
|
+
|
|
552
788
|
Do not type-gate a single-row delete; do not one-tap a tenant wipe. Confirm only where this ladder calls for it — a confirmation on every action gets clicked through.
|
|
553
789
|
|
|
554
790
|
**Neutralize a disabled destructive control.** `btn-danger` at full saturation reads as armed whatever the `disabled` attribute says, and the contrast exemption for disabled controls does not excuse it. While the action is unavailable, drop to the neutral or outline `btn-*` class (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. Never use `title` alone — it never reaches a keyboard or screen-reader user, and it disappears on touch.
|
|
@@ -567,19 +803,19 @@ Do not type-gate a single-row delete; do not one-tap a tenant wipe. Confirm only
|
|
|
567
803
|
|
|
568
804
|
## Performance
|
|
569
805
|
|
|
570
|
-
- **
|
|
571
|
-
- **
|
|
806
|
+
- **Keep one CSS system.** Extend the installed Bootstrap theme; do not add a competing framework or a parallel palette to restyle the surface.
|
|
807
|
+
- **Ship the full compressed build, or trim it with a Sass-subset build** that imports only the parts used (see [Theming](#theming--design-tokens)). Do not reach for a CSS purge tool first. Bootstrap adds classes **at runtime** — `show`, `showing`, `fade`, `collapsing`, `modal-open`, `modal-backdrop`, `offcanvas-backdrop`, and tooltip/popover generated markup — so a purge without a safelist ships a UI whose modals stop rendering. Where the project purges anyway, safelist every JS-toggled class and drive every overlay before shipping.
|
|
572
808
|
- **Icons:** Bootstrap Icons is a separate package — prefer inline SVG or an SVG sprite (crisp, styleable through `currentColor`, no font flash) over the icon font; load only the icons used.
|
|
573
809
|
- **JS:** the bundle is small, but only load it where behavior exists; per-component ESM imports (`bootstrap/js/dist/modal`) trim further in bundlers.
|
|
574
|
-
- **Fonts:**
|
|
810
|
+
- **Fonts:** reuse the existing families and load only needed weights/scripts. Add a display face only for a distinct role; use `font-display: swap` and test fallback wrapping. Keep the data face legible before and after fonts load.
|
|
575
811
|
|
|
576
812
|
## When Not to Hand-Roll
|
|
577
813
|
|
|
578
814
|
Bootstrap has **no** combobox/autocomplete, date picker, multi-select tags input, data grid, or tree view. The boundary rule:
|
|
579
815
|
|
|
580
|
-
- **
|
|
581
|
-
- **
|
|
582
|
-
- **
|
|
816
|
+
- **Use native controls:** `<input type="date">`, `<datalist>` for light autocomplete, and `<select multiple>` where suitable.
|
|
817
|
+
- **Use an established accessible library** when native controls cannot meet the product's widget requirements; audit it against the APG contract.
|
|
818
|
+
- **Implement a custom widget** only when native controls and an established accessible library cannot satisfy the requirements; read [Accessibility](#accessibility) → Pattern contracts and cover the required keyboard behavior.
|
|
583
819
|
- 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.
|
|
584
820
|
|
|
585
821
|
## Common Layout Patterns
|
|
@@ -587,8 +823,8 @@ Bootstrap has **no** combobox/autocomplete, date picker, multi-select tags input
|
|
|
587
823
|
### Centered content
|
|
588
824
|
|
|
589
825
|
```html
|
|
590
|
-
<div class="d-flex
|
|
591
|
-
<div>Centered content</div>
|
|
826
|
+
<div class="d-flex flex-column min-vh-100 p-3">
|
|
827
|
+
<div class="my-auto">Centered content</div>
|
|
592
828
|
</div>
|
|
593
829
|
```
|
|
594
830
|
|
|
@@ -616,3 +852,20 @@ Bootstrap has **no** combobox/autocomplete, date picker, multi-select tags input
|
|
|
616
852
|
<div class="d-none d-md-block">Hidden on mobile, visible md+</div>
|
|
617
853
|
<div class="d-md-none">Visible only below md</div>
|
|
618
854
|
```
|
|
855
|
+
|
|
856
|
+
`d-none` removes the element from layout and the accessibility tree, which is what makes a dual presentation legal: only the active view exposes its controls. It is not a content strategy — anything hidden at the base must remain reachable through an operable path ([responsive-layout.md](responsive-layout.md) → Keep the task intact). A generated responsive role, for a utility-map property with no shipped infix:
|
|
857
|
+
|
|
858
|
+
```scss
|
|
859
|
+
$utilities: map-merge(
|
|
860
|
+
$utilities,
|
|
861
|
+
(
|
|
862
|
+
'width': map-merge(
|
|
863
|
+
map-get($utilities, 'width'),
|
|
864
|
+
(
|
|
865
|
+
responsive: true,
|
|
866
|
+
)
|
|
867
|
+
),
|
|
868
|
+
)
|
|
869
|
+
);
|
|
870
|
+
// generates w-md-auto, w-lg-50, … alongside the stock w-*
|
|
871
|
+
```
|