@@ -203,6 +203,10 @@ tile: `$box-shadow` on hover and `:focus-within`, a stretched `::after` link ove
203203icon, and an ` h2 ` forced to ` $font-size-base ` (` 1.2rem ` from ` md ` ). ` .card-collection ` is the
204204form-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 ` .
207211Rows use ` .form-group ` with ` margin-bottom: $spacer ` , replacing Bootstrap 5's ` mb-3 ` . Required
208212fields 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
214218add/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 ` ).
218244Row 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
232258primary link color has too little contrast on most tints.
233259
234260The 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+
346431This is the standard for new templates. Existing pages in consuming projects predate it and
347432put form rows straight into ` {% block main %} ` , so treat non-carded pages as unconverted, not
348433as 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
446532in ` _imports.scss ` produces no CSS.
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
465554Do 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
0 commit comments