CRUD Interface
Purpose
This document captures the generic structure of a CRUD interface, separating reusable interface patterns from application-specific record details.
1. SPA-Scoped Page CSS
Generic Rule
Page-specific CSS must be loaded only by the page that needs it. It must not be placed in global bundles such as main.css or other shared site-level stylesheets. In a single-page application, this prevents style leakage when navigating from one screen to another.
Structural Intent
The structural pattern is:
- Keep shared CSS in global bundles.
- Keep page-only CSS in a page-local head include.
- Mark page-local stylesheet links as SPA importable.
- Let SPA navigation unload the prior page's imported styles and load the new page's imported styles.
Implementation In This Codebase
For a page such as assets/pages/members/home.php, create a sibling head include named assets/pages/members/home.head.php. The template renderer automatically looks for page-level head files using the pattern:
head.php<page-stem>.head.php
That lookup is assembled in assets/require/template.php.
Inside the page head include, add the stylesheet link in the document head and mark it with the SPA attributes:
<link
rel="stylesheet"
href="/css/site/document-directory.css?<?=rand()?>"
data-spa-importable
data-spa-imported="true">
Why These Attributes Matter
data-spa-importable tells the SPA loader that this head node belongs to the page contract and may be copied into the live document during navigation.
data-spa-imported="true" ensures the node is removable by the SPA cleanup pass. This matters because the SPA loader removes previously imported head nodes before appending the next page's head imports.
SPA Runtime Contract
The SPA loader scans fetched pages for:
head [data-spa-importable]
It removes current live nodes matching:
head [data-spa-imported="true"]
It then clones the fetched page's importable head nodes into the live document head and tags them as imported. This logic lives in public_html/shared/script/at-conf/spa-link/customizations.js.
Required Practice
When a CRUD interface needs a page-only visual treatment:
- Do not add that stylesheet to
public_html/css/main.css. - Do not add that stylesheet to shared members-wide CSS unless every members page truly needs it.
- Create the page-local head include beside the page.
- Add the stylesheet link with SPA import attributes.
- Keep only reusable structural rules in shared bundles.
Example Separation
Generic structural rules:
- toolbar spacing patterns used by many CRUD interfaces
- modal shell rules used by many CRUD interfaces
- list card rules used by many CRUD interfaces
Application- or page-specific rules:
- a document-directory visual skin for dashboard landing pages
- a one-off section header treatment used only by directory-style pages
- page-specific spacing, color, or background rules that should not affect other CRUD screens
2. General Page Structure
Generic Layout
The CRUD interface page is a vertical workbench layout inside the site shell.
Shell-owned areas:
- navbar
- footer
CRUD interface page areas:
- breadcrumb bar
- header
- toolbar
- list content
- list item cards inside the list content area
Diagram
flowchart TD
A["Navbar<br/>shell-owned, out of scope"]
B["Breadcrumb Bar"]
C["Header"]
D["Toolbar"]
E["List Content"]
I1["List Item Card"]
I2["List Item Card"]
F["Footer<br/>shell-owned, out of scope"]
A --> B --> C --> D --> E --> F
E --> I1
E --> I2
Structural Reading
From top to bottom, the page should read as:
- Navbar
- Breadcrumb bar
- Header
- Toolbar
- List content
- Footer
Inside list content, the page renders one or more list item card records.
Purpose Of Each Layer
breadcrumb bar
Provides page location and return navigation within the broader application structure.
header
Introduces the current interface, giving the page title and short explanatory text.
toolbar
Hosts immediate collection controls such as create, status filters, search, paging, refresh, or alert placement.
list content
Acts as the collection viewport for the current result set.
list item card
Represents one record from the collection in a compact, scan-friendly summary shape.
Scope Note
This document treats the navbar and footer as shell concerns, not CRUD interface concerns. They frame the page, but they are not part of the CRUD workbench itself.
3. Breadcrumb Bar
Purpose
The breadcrumb bar is a generic wayfinding component placed above the CRUD interface header. Its job is to show the user's current location in the application hierarchy and provide a short path back to the parent surface.
It is not a CRUD control. It is a navigation aid.
Structural Role
The breadcrumb bar sits between the shell navigation and the page header:
Navbar
-> Breadcrumb Bar
-> Header
-> Toolbar
-> List Content
Generic Anatomy
The component is made from:
- a
navcontainer with an accessibility label - an ordered list for the path
- one or more breadcrumb items
- separators between items
- a final current-page item
Canonical Markup Shape
<nav class="breadcrumb breadcrumb-bar" aria-label="Breadcrumb">
<ol class="breadcrumb__list">
<li class="breadcrumb__item">
<a class="breadcrumb__link spa-link" href="/members">Dashboard</a>
</li>
<span class="breadcrumb__sep" aria-hidden="true">/</span>
<li class="breadcrumb__item">
<span class="breadcrumb__link" aria-current="page">DNE</span>
</li>
</ol>
</nav>
Component Rules
nav
Must use aria-label="Breadcrumb" so assistive technology can identify the region correctly.
ol
Represents the ordered path from broader location to current page.
breadcrumb item
Each non-current item represents a parent location that the user may return to.
separator
Separators are visual only and should be hidden from assistive meaning with aria-hidden="true" when appropriate.
current page item
The final item is not a link. It should use aria-current="page" and should not behave like a navigable control.
Behavioral Contract
Parent breadcrumb segments:
- are clickable
- usually use
spa-linkin this codebase - navigate upward or sideways within the application hierarchy
Current breadcrumb segment:
- is plain text or non-interactive inline content
- communicates location, not an action
Content Guidance
Breadcrumb labels should be:
- short
- stable
- structurally meaningful
Good breadcrumb labels:
HomeMembersAdminDashboardDNE
Avoid breadcrumb labels that are:
- explanatory sentences
- long descriptions
- action verbs such as
CreateorSearch
Relationship To The CRUD Interface
The breadcrumb bar belongs to the page frame around the CRUD interface, not to the collection manager itself.
It tells the user:
- where this CRUD interface lives
- what parent area owns it
- how to return to that parent area
It does not:
- filter records
- create records
- edit records
- change list state
Styling Expectations
As a generic component, the breadcrumb bar should:
- span the available page width
- visually separate itself from the content below
- support truncation for long labels
- remain readable on smaller screens
The shared implementation in this codebase already provides:
.breadcrumb.breadcrumb__list.breadcrumb__item.breadcrumb__sep.breadcrumb__link.breadcrumb__link--current
Generic Reuse Pattern
Every CRUD interface page may reuse the same breadcrumb component shape while changing only:
- the parent link target
- the parent label
- the current page label
That makes the breadcrumb bar a true generic component rather than an application-specific one.
4. Header
Purpose
The header is the static identity block for the CRUD interface page.
It tells the user:
- what area they are in
- what interface they are using
- what the interface is for
It does not change in response to list state, search state, paging state, filter state, selection state, modal state, or action results.
Non-Negotiable Rule
The header is not a live status surface.
For a given CRUD interface page, the header content is fixed for that page. Once the page is defined, the header should remain the same while the user:
- searches
- filters
- pages through records
- opens modals
- creates records
- edits records
- deletes or archives records
- receives success or error messages
All changing interface state belongs elsewhere, usually in the toolbar, alert area, list content, or modal.
Structural Role
The header sits directly below the breadcrumb bar and directly above the toolbar.
Breadcrumb Bar
-> Header
-> Toolbar
Generic Anatomy
The header is composed of three parts:
- eyebrow
- title
- description
Canonical Markup Shape
<header class="section-lander--compact">
<span class="section-lander__eyebrow">Members</span>
<h2>DNE</h2>
<p>Manage DNE records from one shared list and modal shell.</p>
</header>
Component Rules
header
Defines the page introduction block for the CRUD interface.
eyebrow
Names the broader surface or section that owns the page.
title
Names the CRUD interface itself. This should be short and stable.
description
Explains the purpose of the interface in one compact sentence or short paragraph.
Content Rules
The header should contain only stable identity content.
Allowed content:
- section label
- page title
- short description
Disallowed content:
- result counts
- active filter summaries
- search terms
- paging controls
- create buttons
- refresh buttons
- success messages
- error messages
- selected-record details
Why It Must Not Change
The header is the user's anchor.
When the rest of the CRUD interface changes state, the header should remain constant so the user always has a stable answer to:
- where am I
- what page is this
- what does this page manage
If the header changes with operational state, it stops behaving like a header and starts behaving like a status or control surface, which creates ambiguity.
Behavioral Contract
The header is typically non-interactive.
It is a content block, not a control block. If controls are required, they belong in the toolbar below it, not inside the header.
Reuse Pattern
Every CRUD interface page can reuse the same header shape while swapping only:
- the eyebrow label
- the page title
- the short description
The structure stays the same even when the managed record type changes.
Practical Test
A good CRUD page header passes this test:
If the list becomes empty, filtered, paged, edited, refreshed, or erroring, the header does not need to change.
5. Toolbar
Purpose
The toolbar is the operational control surface for the CRUD interface collection.
It is the first interactive layer of the page and sits directly below the header. Its job is to let the user act on the collection as a whole rather than on any one specific record.
Versioning Rule
The toolbar is versioned.
This means:
- a CRUD interface may use
toolbar version 1,toolbar version 2, or another approved variant - one toolbar version may be swapped for another at page-design time
- once a toolbar version is chosen, its internal structure does not change
In other words:
toolbar => one selected version
but:
selected toolbar version => fixed internal structure
Toolbar Variant Used Here
This CRUD interface uses the paginated searchable toolbar variant.
This variant always includes:
- collection action row
- search row
- pager row
Its structure is fixed and should not be rearranged ad hoc from page to page.
Structural Role
The toolbar sits between the static header and the dynamic list content.
Header
-> Toolbar
-> List Content
High-Level Construction
The paginated searchable toolbar is built as:
Toolbar Container
-> Optional Heading Row
-> Toolbar Form
-> Hidden State Inputs
-> Status / Action Row
-> Search Row
-> Pager Row
Canonical Markup Shape
<div class="app-registration-toolbar drop-list-toolbar">
<div class="app-registration-toolbar__heading" style="display:none">
<div><h2>Collection Title</h2></div>
<button type="button">Refresh</button>
</div>
<form class="drop-list-form">
<input name="status" type="hidden" value="">
<div class="drop-list-statusbar">
<button type="button">Create</button>
<nav aria-label="Status filter">
<button type="button">All</button>
<button type="button">Soft</button>
<button type="button">Hard</button>
</nav>
</div>
<div class="drop-list-searchbar">
<div class="form-group">
<input type="search" name="search" placeholder="Search">
</div>
<button type="button">Search</button>
</div>
<div class="drop-list-pager">
<button type="button">Previous</button>
<input type="number" min="1" value="1">
<span>/ 1</span>
<button type="button">Next</button>
<span>0 results of 0</span>
</div>
</form>
</div>
Fixed Internal Sections
1. Toolbar Container
The outer toolbar container groups the variant as a single reusable component.
Responsibilities:
- establish the toolbar layout
- separate toolbar styling from list-card styling
- provide a stable mount point for one toolbar variant
2. Optional Heading Row
This row is part of the variant contract even when hidden.
Responsibilities:
- provide a place for a collection heading if required
- provide a place for a refresh or utility action if required
This row may be shown or hidden, but it should not be structurally redefined.
3. Toolbar Form
The toolbar controls are wrapped in a form-shaped structure even when some buttons proxy other actions.
Responsibilities:
- hold shared collection state
- group all list-level controls together
- provide a stable contract for active-tag or JS wiring
Active-tag structural rule:
- place hidden clickers or hidden proxy-target buttons inside the form they belong to
- if a visible control outside the form uses
data-button-proxy, it should proxy into a hidden button that lives inside the owning form - this keeps
form.collectoperating on the correct form scope instead of an unrelated outer node
4. Hidden State Inputs
Hidden inputs store collection state that may not always have a visible control.
Examples:
- current status filter
- additional query flags
- future sort or scope values
These inputs belong near the top of the toolbar form so the current collection state is easy to serialize.
5. Status / Action Row
This is the first visible operational row.
Responsibilities:
- primary collection action such as
Create - collection-scoped quick filters such as
All,Soft,Hard
This row is not for record-specific actions. Record-specific actions belong inside each list item card or its modal.
6. Search Row
This row contains the search input and the explicit search trigger.
Responsibilities:
- accept query text
- submit or proxy a search action
The search row should remain its own row instead of being mixed into pager controls or list cards.
7. Pager Row
This row controls navigation through paginated result sets.
Responsibilities:
- previous page button
- current page input or indicator
- total page indicator
- next page button
- result count summary
This row manages collection navigation state, not filtering state and not record editing state.
Behavior Boundaries
The paginated searchable toolbar may control:
- create action entry point
- collection filters
- search query
- current page
- result navigation
- refresh behavior
The paginated searchable toolbar must not contain:
- record detail fields
- record edit fields
- modal body inputs
- per-record destructive controls
- record summary content
Layout Discipline
This toolbar variant should always read top-to-bottom as:
- status / action row
- search row
- pager row
Do not collapse these into a single improvised row unless a different toolbar version is explicitly being used.
Relationship To Other Toolbar Versions
Another CRUD interface may use a different toolbar version, for example:
- simple create-only toolbar
- filter-only toolbar
- searchable non-paginated toolbar
- paginated searchable toolbar
Those are different interchangeable versions.
What must stay consistent is this rule:
- choose a version
- keep that version's internal structure intact
Practical Test
A valid paginated searchable toolbar passes this test:
If someone reads the markup without application context, they can still clearly identify:
- where to create
- where to filter
- where to search
- where to change pages
6. List Container
Purpose
The list container is the collection viewport of the CRUD interface.
It is the page area that holds the repeated record summaries for the current result set. If the toolbar controls collection state, the list container shows the visible outcome of that state.
Structural Role
The list container sits directly below the toolbar and directly above any shell-owned footer.
Toolbar
-> List Container
-> List Item Card
-> List Item Card
-> List Item Card
Non-Negotiable Rule
The list container is responsible for collection rendering, not page identity and not collection controls.
It should not own:
- breadcrumb behavior
- header identity content
- toolbar control layout
- modal edit fields
It should own:
- the list region itself
- repeated list item card instances
- empty-state or result-state presentation when applicable
Generic Anatomy
The list container is composed of:
- outer list shell
- list region
- repeated list item card children
Canonical Markup Shape
<section class="drop-list-shell">
<div
class="children-list"
role="list"
aria-label="Record list">
<div role="listitem">...</div>
<div role="listitem">...</div>
<div role="listitem">...</div>
</div>
</section>
Component Rules
outer list shell
Provides the structural mount point for the list content area as one reusable page section.
list region
Defines the actual record collection viewport. In this codebase, it should expose list semantics with role="list" when the children are card-shaped rather than semantic li items.
list item children
Each direct child should represent one record summary and should expose role="listitem" when using card markup.
Accessibility Contract
The list container should identify itself as a list-like region.
Required practices:
- give the collection region
role="list"when using non-ul/olwrappers - give each repeated record card
role="listitem" - provide a clear
aria-labelfor the list region
This ensures assistive technology can understand that the page is presenting a collection of records rather than a random stack of panels.
Behavioral Contract
The list container reflects current collection state.
That means it may change when the user:
- searches
- filters
- pages
- creates a record
- updates a record
- archives or deletes a record
- refreshes the collection
What changes is the content of the list container, not its structural contract.
The structure remains:
list shell -> list region -> repeated list items
Relationship To The Toolbar
The toolbar changes collection state.
The list container displays the visible collection after that state is applied.
The toolbar and the list container are adjacent but distinct concerns:
- toolbar = controls
- list container = results
Relationship To The List Item Card
The list container does not describe a record in detail. It only hosts record summaries.
Each child card is one record.
Therefore:
- list container manages repetition
- list item card manages per-record summary presentation
Empty And Populated States
The list container must support both:
- populated state with one or more cards
- empty state with no visible records
Even when empty, the container contract should remain intact so the page still has a predictable mount point for later rendering.
Layout Expectations
As a generic component, the list container should:
- stack repeated cards in a clear reading order
- preserve enough spacing between cards for scanability
- scale to zero, one, or many items without changing markup strategy
- remain visually subordinate to page identity and toolbar controls
Practical Test
A good list container passes this test:
If the record type changes from DNE entries to another managed object, the container structure does not need to change. Only the card content and data wiring change.
7. List Item Card
Purpose
The list item card is the per-record summary unit inside the list container.
It gives the user a compact, scan-friendly view of one record and provides the entry point into the shared create/edit modal flow.
Structural Role
The list item card is a child of the list container.
List Container
-> List Item Card
-> Action Row
-> Main Card Content
-> Title Row
-> Badge Row
-> Key/Value Pairs
Non-Negotiable Rules
1. The action row must remain separate
This structure must be preserved:
<div class="btn-delete-row">
<button
class="btn-x icon-close"
type="button"
title="Archive item"
aria-label="Archive item"></button>
</div>
That row does not belong inside the main card content stack.
It must not be merged into:
- the title row
- the badge row
- the key/value pair region
It is a separate action rail and must stay separate because more than one top-right action may be added there later.
2. Badges follow on their own row
The title of the card is often a UUID or another long identifier.
Because that title can consume most of the available width, badges must not compete for space on the same row. Badges belong on the next row.
This preserves:
- readable long titles
- stable badge wrapping
- predictable card rhythm
Generic Anatomy
The list item card is composed of:
- outer card shell
- action row
- main clickable content block
- title row
- badge row
- key/value summary region
Canonical Markup Shape
<div class="entity-list-card" role="listitem">
<div class="btn-delete-row">
<button
class="btn-x icon-close"
type="button"
title="Archive item"
aria-label="Archive item"></button>
</div>
<div class="item-card__clicker">
<div class="item-card__summary">
<div class="item-card__title-row">
<div class="item-card__title">019E06F8-BADA-7339-BF64-C538816F41E1</div>
</div>
<div class="item-card__badge-row">
<span class="pill">Soft</span>
<span class="pill">Manual</span>
</div>
</div>
<div class="item-card__details">
<div class="item-card__detail">
<strong>Domain:</strong>
<span>example.org</span>
</div>
<div class="item-card__detail">
<strong>Reason Code:</strong>
<span>mailbox_disabled</span>
</div>
</div>
</div>
</div>
Component Rules
outer card shell
Defines one record summary and should expose role="listitem" when used inside a non-semantic list wrapper.
action row
Owns top-right record actions. It is structurally independent from the content stack.
main clickable content block
Owns the part of the card that opens the modal or detail view.
title row
Displays the primary record identifier.
badge row
Displays status or categorical pills on its own line below the title.
key/value summary region
Displays compact supporting fields for scanability.
Why The Title And Badges Must Be Split
Primary identifiers are not always short names.
In many CRUD interfaces the title may be:
- a UUID
- a CID
- a hash
- a long email address
- another machine-shaped identifier
If badges are forced into the same row, one or more bad things happen:
- the title truncates too aggressively
- badges wrap awkwardly
- spacing becomes unstable
- the card header becomes visually noisy
Putting badges on the next row avoids this.
Why The Action Row Must Stay Outside
The action row is not content.
It is a utility rail for per-record actions such as:
- archive
- delete
- pin
- inspect
- overflow menu
Because that rail may grow over time, it must remain a standalone sibling of the main card content rather than being embedded into the content hierarchy.
This keeps the internal card structure clean:
- content hierarchy = title, badges, key/value pairs
- action hierarchy = top-right utility buttons
Behavior Boundaries
The list item card may do:
- open the shared modal when the main card body is clicked
- expose top-right utility actions through the separate action row
- summarize record fields
The list item card must not do:
- become the toolbar
- contain modal form fields inline
- collapse the action row into the summary content
- place long-title badges on the same header row
Practical Test
A good list item card passes this test:
If the primary title becomes a long UUID and an extra top-right action is added later, the card structure still works without needing to be reorganized.