Skip to content

Commit f1df8b7

Browse files
Merge pull request #253 from sumocoders/docs-card-layouts
docs: card, form and detail page layout guidelines
2 parents 475ec95 + f73c16f commit f1df8b7

7 files changed

Lines changed: 708 additions & 15 deletions

File tree

‎DESIGN.md‎

Lines changed: 126 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,10 @@ tile: `$box-shadow` on hover and `:focus-within`, a stretched `::after` link ove
203203
icon, and an `h2` forced to `$font-size-base` (`1.2rem` from `md`). `.card-collection` is the
204204
form-collection container: dashed `1px var(--bs-gray-400)` on `var(--top-color)`.
205205

206+
`overflow: hidden` clips anything positioned outside the card: an autocomplete (Tom Select) list,
207+
a `.dropdown-menu` or a popover gets cut off at the card edge. A card that holds one takes
208+
`.overflow-visible`; every other card keeps the default. See `docs/card-layouts.md`.
209+
206210
**Forms.** Theme at `templates/Form/fields.html.twig`, built on `bootstrap_5_layout.html.twig`.
207211
Rows use `.form-group` with `margin-bottom: $spacer`, replacing Bootstrap 5's `mb-3`. Required
208212
fields get an `<abbr>` styled to `var(--bs-primary-text-emphasis)` with no underline; labels are
@@ -213,6 +217,28 @@ an `input-group` with a `bi bi-calendar-fill` / `bi bi-clock-fill` addon, driven
213217
`.card.card-collection` > `.card-body` > `ul` > `li.collection-item`, with 40x40 circular
214218
add/remove/drag buttons at `left: -20px` / `right: -20px`, straddling the item edge.
215219

220+
**Form layout.** A form with more than a handful of fields splits into titled section cards
221+
(see Page composition), with one `form_start()` / `form_end()` around all of them so the
222+
toolbar submit button still sends everything. Within a card:
223+
224+
- Order fields by task flow. A field that other fields depend on comes first: the input a
225+
lookup searches on, a type selector that changes the rest of the form.
226+
- Short related fields share a `.row` with `col-md-*` columns; long text fields and textareas
227+
take the full width. Two collection widgets side by side need `.gx-5` on the row, because
228+
their buttons stick out 20px on both sides and overlap in the default gutter.
229+
- An action that works on one field (look up, generate, copy) attaches to it in an
230+
`.input-group`, with a `.form-text` below saying what it does. `form_errors()` goes below the
231+
input group, not inside it.
232+
- A field that is alone in a titled card drops its visible label (`label: false`) and keeps an
233+
`aria-label`, so the card title is not repeated.
234+
- A create page puts `autofocus` on its first field, so typing can start straight away. An edit
235+
page does not: people open it to check something as often as to change it. Merge the attribute
236+
into the form type's `attr` (see Template traps).
237+
- A checkbox in a row with labelled inputs sits above them, because it has no label on top. Put
238+
`align-self-md-end pb-md-2` on its column so it lines up with the inputs.
239+
- When create and update share a custom layout, put it in a `_form.html.twig` partial that both
240+
include. `docs/card-layouts.md` has a complete example.
241+
216242
**Tables.** `components/_tables.scss` adds a solid bottom border and bold headers on
217243
`var(--bs-body-bg)`. Cell padding is bumped to `0.75rem` (stock `0.5rem`).
218244
Row states are stock: striped, hover and active are `5%`, `7.5%` and `10%` tints of
@@ -232,7 +258,8 @@ with light text. Links inside a contextual row take the row's text color, underl
232258
primary link color has too little contrast on most tints.
233259

234260
The bundle renders no table markup itself; `docs/crud.md` prescribes
235-
`<table class="table">` with `<th class="text-end">` action columns.
261+
`<table class="table">` with `<th class="text-end">` action columns. `.table-responsive` goes
262+
on a wrapper `<div>` around the table, never on the `<table>`.
236263

237264
**Alerts and toasts.** Flash messages render as **toasts, not alerts**:
238265
`templates/notifications.html.twig` maps flashbag keys `success` -> `success`,
@@ -253,7 +280,10 @@ links are `min-height: 34px`, `margin: 2px`, bordered `var(--bs-border-color)`,
253280

254281
**Empty states.** `.data-no-results` (`components/_no-results.scss`): centered column,
255282
`1.125rem`, `var(--bs-gray-600)`, `140px` illustration. See `docs/no-results.md`.
256-
`.no-items-icons` scales `2rem` -> `4rem` (`sm`) -> `7rem` (`lg`).
283+
`.no-items-icons` scales `2rem` -> `4rem` (`sm`) -> `7rem` (`lg`). Use it when the page's main
284+
list is empty. An empty section inside a larger page (the notes or contacts of a detail page)
285+
gets one `<p class="text-body-secondary mb-0">` line instead, so it does not take more room
286+
than the section would with content in it.
257287

258288
### Full partial inventory
259289

@@ -313,6 +343,8 @@ background.
313343
- A page with several forms or sections -> **one card per section**, never a single card
314344
wrapping them all.
315345
- An overview -> **one card per item**, not one card around the whole list.
346+
- Related items on a detail page -> **one card holding a responsive list**, not a grid of item
347+
cards. See Detail pages below.
316348
- A table -> `.card` > `.card-body` like anything else. Keep the `card-body` rather than
317349
letting the table run edge to edge against the card: a list section usually carries a
318350
title or intro text alongside the table, and `card-body` gives that room.
@@ -343,6 +375,59 @@ and take `gy-3` on the containing `.row` instead, so the gutter does the spacing
343375
{% endblock %}
344376
```
345377

378+
A section with a title puts it in `.card-header`, not as a heading inside `.card-body`.
379+
380+
**Overview pages** follow one order: a filter card, the results, then `{{ pagination() }}` only
381+
when `hasToPaginate` is true. In the filter card, every field has a visible label (`form_row()`);
382+
a placeholder is at most an example value. There is one primary button; secondary actions such
383+
as export are `btn-outline-secondary`, pushed right. The chosen filters stay filled in, so a
384+
small filter card needs no reset button. A filter with more than four inputs gets a reset button
385+
(`btn-outline-secondary`) next to its filter button while a filter is active. Overviews never
386+
show a result count. "Nothing yet" and "no matches for this filter" are separate empty states.
387+
Filter fields get fixed `col-*` widths, never `col-auto` (see Template traps), and a filter row
388+
that has to fit on one line does so from `xl`, not `lg`. When the filter has default values, the
389+
controller runs the query with those defaults on the first visit, so the page never opens on an
390+
empty list that only fills after pressing the filter button:
391+
392+
```php
393+
$form->handleRequest($request);
394+
if (!$form->isSubmitted() || $form->isValid()) {
395+
$items = $this->itemRepository->findFiltered($filterData);
396+
}
397+
```
398+
399+
**Item cards** in an overview grid:
400+
401+
- Title as `<h2 class="h5 card-title">`, since the page `<h1>` lives in the header bar. When the
402+
item has a detail page, the title is a `link-body-emphasis` link to it.
403+
- Status badges sit next to the title, not on their own line.
404+
- Show a field only when it has a value; never a label with nothing after it. Prefer a
405+
`list-unstyled` list with one Bootstrap Icon per field over "Label: value" rows, and
406+
`.text-truncate` on long values. The icon gets `aria-hidden="true"` and a `.visually-hidden`
407+
label next to it, so screen readers know what the value is.
408+
- Actions go in `.card-footer.d-flex.gap-2` as `btn-sm` buttons. Which actions an item has depends
409+
on the entity; leave the footer out when there are none. An icon-only button carries `title`
410+
and `data-controller="tooltip"` on the button itself plus `.visually-hidden` text.
411+
- Cards in a row stretch to the tallest one. When items can have little content, give the card
412+
body a `min-height` through a project class so a sparse row does not collapse to title height.
413+
414+
**Detail pages** keep the record compact, because its related lists can grow long:
415+
416+
- Two columns from `xl`: a main column (`col-xl-9`) with the record's own fields and its related
417+
lists, and a side column (`col-xl-3`) for secondary information such as history. Below `xl` the
418+
side column stacks under the main one. Put `align-items-start` on the `.row` (see Template
419+
traps).
420+
- The record's fields go in a `dl.row`, label and value side by side, and only the fields that
421+
have a value.
422+
- Each related collection gets its own titled card, with its "add" action as a `btn-sm` in the
423+
same header. No count next to the title: like overviews, detail pages never show counts. The list itself is a responsive list: a
424+
`list-group-flush` whose items are grid rows. From `md` up the columns line up under a header
425+
row and read as a table; below `md` each item stacks into its own block and empty fields drop
426+
out. Bootstrap's grid, order and display utilities do this without custom CSS.
427+
428+
`docs/card-layouts.md` has complete, copyable examples of section cards, an overview page, an
429+
item card and a detail page.
430+
346431
This is the standard for new templates. Existing pages in consuming projects predate it and
347432
put form rows straight into `{% block main %}`, so treat non-carded pages as unconverted, not
348433
as counter-examples.
@@ -441,6 +526,7 @@ color modes; `error.scss` includes it because `templates/base_error.html.twig` p
441526
| Layout / shell | `assets/scss/layouts/` |
442527
| Interactive behaviour | a Stimulus controller in `assets-public/controllers/` |
443528
| Email styling | `assets/scss/mail.scss` (Inky + Foundation for Emails) |
529+
| One-off styling in a project | a Bootstrap utility class, else a class in the project's `assets/styles/` |
444530

445531
**Partials must be underscore-prefixed** and reached through an `@import`. A partial not listed
446532
in `_imports.scss` produces no CSS.
@@ -461,6 +547,9 @@ Do:
461547
- Keep `base/_no-sidebar.scss` last in `_imports.scss`.
462548
- Wrap every `{% block main %}` section in `.card` > `.card-body`, one card per section or
463549
per overview item, separated with `mb-3` or a `gy-3` row.
550+
- Put a section title in `.card-header`.
551+
- Add `.overflow-visible` to a card that holds an autocomplete, dropdown or popover.
552+
- Give every icon-only button an accessible name and put its tooltip on the button.
464553

465554
Do not:
466555

@@ -473,6 +562,16 @@ Do not:
473562
- Put content straight into `{% block main %}` with no card around it.
474563
- Wrap several unrelated sections in one shared card, or let two cards touch with no
475564
`mb-3` / `gy-3` between them.
565+
- Render a field label with no value after it, in a card or anywhere else.
566+
- Put `.table-responsive` on the `<table>` itself; it only works on a wrapper.
567+
- Write inline styles: no `style="..."` attribute and no `<style>` block in a template. Use a
568+
Bootstrap utility class, and when none fits, add a named class to the project's SCSS. The CSP
569+
that `sumocoders/application-skeleton` configures allows `style-src 'self'` only, so the
570+
browser blocks every `style` attribute and every `<style>` block without a nonce: they look
571+
fine while writing a template and do nothing in the page. Inline styles also escape the
572+
dark-mode tokens and stylelint. In JavaScript, toggle a
573+
class rather than setting `element.style`; set a style property only for a value that has to
574+
be measured at runtime, such as an element's height.
476575
- Edit compiled CSS in the application's `public/assets/`.
477576

478577
## Known quirks
@@ -494,6 +593,31 @@ Things that look like bugs but are deliberate. Each was checked against compiled
494593
- **The focus model is Bootstrap 5.2's**, not 5.3's `$focus-ring-*` tokens. See
495594
Elevation and shape. Deliberate, and revisiting it is its own piece of work.
496595

596+
## Template traps
597+
598+
Mistakes that render without an error but break behavior.
599+
600+
- **`form_widget(field, {attr: {...}})` replaces the form type's `attr`.** Stimulus
601+
`data-controller`, `data-action` and `data-*-target` attributes set in the form type are gone.
602+
Merge instead: `form_widget(field, {attr: field.vars.attr|merge({autofocus: true})})`. `merge()`
603+
replaces a key, so a class the form type already sets is lost the same way; append to it instead:
604+
`{class: (field.vars.attr.class|default('') ~ ' btn-outline-primary')|trim}`.
605+
- **Display utilities are `!important`.** `.d-flex`, `.d-block` and friends override any
606+
`display: none` that CSS uses to show or hide an element. Put the utility on an inner element.
607+
- **Collection widgets overlap side by side.** Their buttons stick out 20px past each item, more
608+
than half the default `1.875rem` gutter. Use `.gx-5` on the row (see Form layout).
609+
- **Stacked cards in a column disappear.** `.card` has `height: 100%`, and a `.col` in a `.row`
610+
stretches to the height of the tallest column. The first card then fills its whole column and
611+
pushes the cards below it out of view. Put `align-items-start` on the `.row`, so each column
612+
keeps the height of its own content.
613+
- **Breakpoints ignore the sidebar.** Bootstrap's breakpoints read the viewport width, but the
614+
open sidebar takes `$sidebar-width-open` (242px) of it. At a 1000px viewport, `lg` columns share
615+
about 750px of content width. Layouts that need room, such as a one-line filter or a table next
616+
to a side column, belong on `xl`.
617+
- **An autocomplete in `col-auto` grows to its longest option.** A Tom Select or plain `<select>`
618+
in a `col-auto` column is as wide as its widest option text, and pushes the rest of the row onto
619+
new lines. Give it a fixed `col-*` width.
620+
497621
## Peer dependencies
498622

499623
`style.scss` imports Bootstrap and Bootstrap Icons straight out of `vendor/twbs/`, five levels

‎docs/button-locations.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,21 @@ Put them in {% block header_navigation %}
1414
{% endblock %}
1515
```
1616

17+
### Back to the parent record
18+
19+
A page for a record that belongs to another one (a note of an item, a line of an order) links back to that
20+
parent here, with its name. It tells the user which record they are working on, and gives them a way back
21+
without the sidebar or the browser's back button:
22+
23+
```twig
24+
{% block header_navigation %}
25+
<a class="btn btn-outline-secondary" href="{{ path('item_detail', {item: item.id}) }}">
26+
<i class="bi bi-arrow-left"></i>
27+
{{ item.name }}
28+
</a>
29+
{% endblock %}
30+
```
31+
1732
## Fixed toolbar on the bottom
1833

1934
Buttons in the fixed toolbar on the bottom are used for primary actions that users frequently need to access.

0 commit comments

Comments
 (0)