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:

  1. Keep shared CSS in global bundles.
  2. Keep page-only CSS in a page-local head include.
  3. Mark page-local stylesheet links as SPA importable.
  4. 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:

  1. Do not add that stylesheet to public_html/css/main.css.
  2. Do not add that stylesheet to shared members-wide CSS unless every members page truly needs it.
  3. Create the page-local head include beside the page.
  4. Add the stylesheet link with SPA import attributes.
  5. 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:

  1. Navbar
  2. Breadcrumb bar
  3. Header
  4. Toolbar
  5. List content
  6. 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:

  1. a nav container with an accessibility label
  2. an ordered list for the path
  3. one or more breadcrumb items
  4. separators between items
  5. 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-link in 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:

  • Home
  • Members
  • Admin
  • Dashboard
  • DNE

Avoid breadcrumb labels that are:

  • explanatory sentences
  • long descriptions
  • action verbs such as Create or Search

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:

  1. eyebrow
  2. title
  3. 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:

  1. collection action row
  2. search row
  3. 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.collect operating 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:

  1. status / action row
  2. search row
  3. 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:

  1. outer list shell
  2. list region
  3. 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/ol wrappers
  • give each repeated record card role="listitem"
  • provide a clear aria-label for 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:

  1. outer card shell
  2. action row
  3. main clickable content block
  4. title row
  5. badge row
  6. 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.

1