# Designer module

Visual GUI page designer for list / add / edit / view / print / pdf / cards / import /
export / dashboard and common pages. Rebuilt as a single coherent SPA (the previous
21-JS / 17-CSS layered runtime was replaced on 2026-06-28; backup at
`storage/backups/designer-20260628-175609/`).

## Backend (unchanged contract)

- `Controllers/DesignerController.php`
  - `index`    → renders the SPA (`Views/index.php`)
  - `load`     → JSON: `{ tables, fieldsByTable, availablePageTypes, pagesState, designer }`
  - `save`     → persists the full `designer` document via `ModuleState`
  - `template` → returns a default `page` for a `{table, pageType}` (via `core/Designer` TemplateFactory)
- `Controllers/DesignerLayoutController.php` → `layout` preset insertion
- State is stored at `projects/{username}/{slug}/config/designer.json`.

## Front-end architecture (`Views/`)

`index.php` inlines one stylesheet + ordered IIFE modules on a single `window.PPD`
namespace. The **page object-model is the single source of truth**.

```
app/designer.css   one consolidated stylesheet (design tokens preserved)
app/store.js       state container: designer doc + UI/selection, pub/sub, undo/redo, dirty flag
app/api.js         fetch wrappers for /load /save /template /layout
app/model.js       model ops: activePage, find/flatten, add/remove/duplicate/move, update*
app/registry.js    component registry: type -> { label, icon, defaults, render, schema, container? }
app/renderer.js    renders active page -> #ppdCanvas; selection + drag/drop (palette add, reorder)
app/inspector.js   schema-driven property panel (page + every component type)
app/panels.js      left panel (Pages/Components/Fields), workbar (ribbon/device/preview/save), statusbar
app/app.js         boot: refresh from server -> pick page -> autosave (debounced) + Cmd/Ctrl+S + preview
partials/v3-*.php  DOM scaffolds (left panel, canvas+workbar, inspector, statusbar)
```

### Data shapes
```
designer.pages[table][pageType] = { defaultPage, pages: { <name>: PAGE } }
designer.common[commonPage]     = PAGE
PAGE      = { id, name, type, title, layout, settings, components: [ COMPONENT ] }
COMPONENT = { id, type, label, props, style, visibility, children? }
```

## Default List Page Template (shared master)

A single **Default List Page Template** lives at `designer.templates.list` and applies
to every table that has a `list` page in the Pages module. It contains: breadcrumb,
list-toolbar (3-zone), data grid (3-row), pagination, and a Page Actions bar
(Back to List / Refresh).

- Edited via the **Default List Page** item in the left panel's *Templates* section
  (`scope: 'template'`). The grid there shows representative sample columns.
- On save, `model.applyListTemplateToTables()` **propagates** it to every list-enabled
  table, binding each table's own **columns** (from Fields) and **buttons/actions**
  (filtered by that table's Pages functionality via `reconcileListPage`).
- Seeded once on first load (`ensureListTemplateSeed`), persisted in the designer doc
  (hydrated on `/load`), and auto-saved. Editing the master = editing every table's list page.

## Default Add / Edit / View Page Templates (responsive form builder)

Masters at `designer.templates.add` / `.edit` / `.view`, applied to every table
with that page type (same seed/propagate/auto-save model as the list template).

Structure: breadcrumb → section card → **12-column responsive form grid** → action buttons.
Per type: **Add** = Save / Save&New / Cancel · **Edit** = Save / Cancel / Back ·
**View** = read-only fields (label-left detail layout) + Edit / Print / Delete / Back.
Each field is a `field` component (`mode: edit|view`) with `spans: { desktop, tablet, mobile }` (out of 12).

- **Responsive**: the renderer applies `spans[device]` as `grid-column: span N`; switching the
  device (desktop/tablet/mobile) in the workbar re-lays the form. Fields are half-width on
  desktop and stack full-width on mobile by default.
- **Sizing**: choose width per breakpoint in the inspector (Full/¾/⅔/½/⅓/¼…), or **drag a
  field's right edge** on canvas to resize (updates the active device's span).
- **Placement**: drag fields to reorder / move between sections (container drop zones).
- **Rich inputs**: text/number/email/tel/url/date/time/datetime/textarea/select/checkbox/radio/toggle/file/color.
- **Propagation**: form-level defaults (`fieldSpan`, `gap`, `labelPosition`, buttons) apply to
  all tables; the template preview shows representative sample fields, while each table's page
  is generated from its own Fields (respecting per-page field visibility). `model.buildFormTemplate` /
  `applyFormTemplateToTables` / `seedTemplates`.

## Default Print Page Template

Master at `designer.templates.print` (`model.buildPrintTemplate`). Renders as a real
**paper sheet**: the canvas is sized to the chosen paper (A4/Letter/Legal/A5/A3) and
orientation at 96dpi, with the page margin as padding and no designer chrome (`.ppd-print-sheet`).
Paper **size / orientation / margin** are page-level props under `layout.paper`, edited in the
page inspector's *Paper* group.

Structure: `print-header` (org · date · title · rule) → view-mode detail fields
(label-left, document-styled) → `print-footer` (tokens `%page%` `%pages%` `%date%`).
Propagated to every table with a `print` page (view-mode fields bound from Fields).

## Default PDF Page Template

Master at `designer.templates.pdf` (`model.buildPdfTemplate`, sharing `buildPaperTemplate`
with print). Same **paper-sheet** rendering as the print template (canvas sized to the
chosen paper/orientation at 96dpi, `layout.paper` props edited in the *Paper* inspector
group), but identified as a PDF document (header/footer worded for a generated document).
Propagated to every table with a `pdf` page (view-mode fields bound from Fields). Wired
through the same form-template family as print (`isFormType`, `fillForm` view mode).

## Default Cards Page Template

Master at `designer.templates.cards` (`model.buildCardsTemplate`), an alternative to the
list grid that renders records as a **responsive card gallery**. Structure: breadcrumb →
list-toolbar (3-zone) → filter chips → selection bar → `card-grid` → pagination → Page
Actions. Propagated to every table with a `cards` page via `applyCardsTemplateToTables`
(binds breadcrumb, card fields from Fields, a human title field, and Pages-driven toolbar
buttons + card actions through `reconcileListPage`).

The `card-grid` component renders **one editable representative card** (plus dimmed ghost
cards to convey the gallery — never fake records); each row shows a field label and its
bound column slug. Props: `columns` (fields shown), `titleField` / `subtitleField`,
`showAvatar`, `selection`, `showActions` / `actions`, `density`, and per-breakpoint
`colsDesktop` / `colsTablet` / `colsMobile` (default 3 / 2 / 1) applied via the
`--cards` grid variable using the workbar device.

## Default Import / Export Page Templates

Masters at `designer.templates.import` / `.export` (`model.buildImportTemplate` /
`buildExportTemplate`), propagated to tables with those pages via `applyDataOpTemplate`
(binds breadcrumb, heading, preview-grid columns, and the field list from Fields).
Modeled on `core/Designer/Templates/{Import,Export}PageTemplate.php`.

New components: `workflow-steps` (numbered stepper), `radio-group`, `format-selector`
(file-type tiles), `field-mapper` (source→target), `summary` (stat cards), and an upgraded
`field-selector` (Select-all + checklist).

- **Import**: steps Source→Mapping→Preview→Import · source card (paste tabs + upload +
  Download Sample) · options (delimiter/mode/checkboxes) · field mapper · preview grid ·
  summary · actions (Validate & Preview / Import / Cancel).
- **Export**: steps Records→Fields→Format→Export · scope radios · field checklist ·
  format tiles (CSV/XLSX/PDF/JSON/XML/HTML) · preview grid · summary · actions (Preview / Export / Cancel).

## Pages-module integration

The Designer reads each table's config from the **Pages** module
(`config/pages.json` → `byTable[table]`), exposed to the front-end as
`PpDesignerBootstrap.pagesConfig` (and the `/load` response). On load,
`model.syncWithPages()` reconciles every configured table's list page so the
toolbar buttons and grid row-actions match what's selected in Pages:

| Pages selection | Designer element |
|---|---|
| pageType `add` | toolbar **Add New** |
| `functionality.deleteRecords` | toolbar **Delete Selected** + grid **delete** action |
| `functionality.updateSelected` | toolbar **Update Selected** |
| `functionality.inlineAdd` | toolbar **Inline Add** |
| `functionality.spreadsheet` | toolbar **Spreadsheet** |
| pageType `export` / `print` | toolbar **Export** / **Print** |
| pageType `view` / `edit` | grid **view** / **edit** action |
| `functionality.inlineEdit` / `copyRecords` | grid **inline-edit** / **copy** action |

The reconcile preserves user-added custom controls, per-control style overrides
(`config`/`actionConfig`), and rich object-actions (with their `events`). It runs
on load and persists once quietly. Pages is the source of truth for *which*
standard buttons appear; the Designer owns their layout, styling, and any extras.

## Capabilities

- **Nested page building** — drag components into containers (`section`, `card`, `form`, `row`, `tabs`);
  empty containers show a drop hint; precise insertion between siblings.
- **Field-aware drops** — dragging a DB field onto the canvas creates a bound `field`
  component with the input type inferred from the column type.
- **Selection & editing** — click to select, double-click to rename inline, per-node
  Duplicate/Delete tools, multi-page-type ribbon.
- **Schema-driven inspector** — page properties + per-component property panels, live edits.
- **Persistence** — debounced autosave + `Cmd/Ctrl+S` + explicit Save → `/save`.
- **History** — undo/redo (`Cmd/Ctrl+Z` / `Cmd/Ctrl+Y`) over all model mutations.
- **Preview** — renders the active page without the designer chrome.
- **Devices** — desktop / tablet / mobile canvas widths; zoom controls.

### Component library (registry)
Content: `heading`, `text`, `button`, `divider`, `alert`, `image` ·
Layout: `section`, `card`, `row`, `form`, `tabs` ·
Navigation: `breadcrumb`, `toolbar`, `search`, `pagination` ·
Data: `table-grid`, `card-grid`, `list-toolbar`, `stat-card`, `kpi-row`, `chart` ·
Form: `field`, `buttons`, `upload`, `field-selector`

### Data grid (3-row design template)
The grid renders as an editable template, not sample records:
`Field name` (header) · `Field value` (body) · `Aggregate` (footer). A left gutter
labels each row and selects the whole row level; every cell is individually
selectable at its level (`head:` / `cell:` / `foot:`), so a header can be left-aligned
while its value cell is centered. Per-cell config lives under `columns[].header|cell|footer`;
row defaults under `props.rows.{header,cell,footer}` (cell overrides row).

**Field placeholder selection** — inside a value cell, the field token is itself a
selectable sub-part (`field:<col>`), independent of the surrounding cell (`cell:<col>`).
Selecting it exposes a **Field** editor: View/Edit **control plugins** (+ their settings)
and a **field style** (text / background / bold) applied to the placeholder only — so a
field can be visually marked up separately from its cell. Stored per column
(`columns[].viewControl` / `editControl` / `*Settings` / `fieldStyle`); a small chip on
the token shows an assigned view control.

**Flexible columns** — drag a column **header** left/right to reorder (the whole
column — header, filter, value, aggregate cells — moves together, since they all map
over `props.columns`). Drag a **field** from the Fields panel onto a header to insert
it as a new column (or re-show it if hidden). Hide via the Columns dropdown or the
column inspector. Width/align/format/pin are per-column in the inspector.

**Modern grid options** (grid props, editable in the inspector): `density`
(compact/normal/comfortable), `striped`, `hover`, `bordered`, `stickyHeader`,
`rowNumbers`, `headerFilters` (adds a per-column filter row → `columns[].filterConfig`),
`wrapText`, `responsive` (scroll/stack), `selectionMode` (none/single/multiple).
Per-column `pin` (left/right) freezes columns. The renderer is backward-compatible with
the backend column shape (flat `align`/`format`/`aggregate`) and normalizes `actions`
whether they are strings or `{type,text,icon}` objects (bare icon names auto-prefixed `bi-`).

**Conditional formatting** — `props.conditionalRules[]` = `{ scope:'row'|'cell', column?, condition:{field,op,value}, style:{background,color,bold} }`.
Edited in the grid inspector; evaluated by the **Events module** (a separate SPA page) at runtime to colour matching rows/cells.

### Table Toolbar (`list-toolbar`)
Three zones, every control individually selectable/editable (`tb:<key>`):
- **left**: `add`, `delete_selected`, `update_selected`
- **center**: `search`, `filter`, `custom` (placeholder for a user button)
- **right**: `export` (dropdown: xlsx/word/pdf/csv…), `page_size` (10/20/50/all), `print`

Controls are keys per zone (`props.left|center|right`); overrides in `props.config[key]`;
export formats / page sizes / default / search text are top-level props. The backend
`ListPageTemplate` emits this toolbar by default for new list pages.

### Backend component catalogue (live palette)

The full `core/Designer/Registries/ComponentRegistry` catalogue (65 components across
Layouts / Structure / Data / Actions / Navigation / Content / Feedback / Dashboard
Widgets / Advanced) is passed to the SPA as `PpDesignerBootstrap.components`
(`DesignerController::componentCatalogue()`). On load, `registry.js` **ingests** it: any
catalogue type not already hand-defined is auto-registered so it appears in the
**Components** tab and is droppable on any page. High-value components ship real renders
(`kpi-trend-card`, `progress-card`, `status-badge`, `avatar`, `rating`, `timeline`,
`kanban`, `mini-table`, `quick-links`, `recent-activity`, `notification-list`,
`task-list`, `accordion`, `modal`, `offcanvas`, `progress`, `toast`, `qr-code`,
`html`/`*-snippet`, `advanced-search`, `filter-panel`, `totals`, `master-detail`,
layout grids, …); the remainder fall back to a polished generic card. All are
schema-driven and editable in the inspector like any native component.

### Icons (FontAwesome Pro 7.2 · Bootstrap · Material)

Icon-pack stylesheets are injected in the head via
`SpaAssetManager::renderIconCss($baseUrl)` (`IconManager::assets()` → FontAwesome Pro
7.2 solid/regular/brands, Bootstrap Icons, Material Symbols).

> **All assets are served locally — no CDN** (so a built project is hostable offline).
> Icon/font packs point at `public/assets/`: FontAwesome → `assets/vendor/fontawesome/7.2/`,
> Bootstrap Icons → `assets/icons/`, Material Symbols → `assets/vendor/material-symbols/`,
> Inter → `assets/vendor/inter/`. Google-hosted fonts were localized with
> `php tools/localize-google-font.php <name> "<css2-url>"` (downloads the CSS + every
> referenced webfont and rewrites `url()` to local paths). Re-run that tool to refresh or
> add a font pack. The universal `icon()`
helper in `registry.js` renders any family: `bi-x` / `bi bi-x`, `fa-solid fa-x` / bare
`fa-x`, `material-symbols-outlined name`, or any full class list. Every **Icon**
property field is a compact single-row preview + free-text input + **Browse** button that
opens a full **icon-browser modal** (`inspector.js` `openIconModal`/`buildIconModal`): a
large labeled grid, a live search box that queries `GET /designer/icons?q=&pack=&limit=`
(`DesignerController::icons()` → `IconManager::picker()`, **~10,120 icons**: FontAwesome
Pro 3,771 + Bootstrap 2,078 + Material 4,262). **Pack filter tabs** — *All* returns a
balanced mix of every pack, a pack tab returns that whole pack; results are **windowed**
(rendered in batches on scroll) so thousands of cells stay smooth. Plus **recently-used**
icons (localStorage), section headers with counts, hover names, and Esc / backdrop close.

> The Bootstrap and Material packs load their full icon sets from local data files
> (`resources/Icons/BootstrapIcons/bootstrap-icons.json`,
> `resources/Icons/MaterialSymbols/material-symbols.codepoints`); FontAwesome loads from
> its bundled index. All render locally (no CDN). Selecting one stores its full
class string; free-text entry of any class is also supported.

### Field controls (View / Edit / Filter plugins)

The platform's field-control plugins (`plugins/{View,Edit,Filter}/*/Plugin.php`, discovered
by `core/PluginRegistry`) are wired into the designer's `field` component:

- **Runtime** — `core/Designer/FieldControl/FieldControlRuntime` is the single consumer of
  the plugins: it resolves a control id within a category and calls the plugin's `render()`
  with the correct signature (`view: render($value,$settings)`, `edit: render($field,$value,$settings)`,
  `filter: render($field,$settings)`), returning HTML + assets. Used by both the designer
  live-preview and any generated-page runtime.
- **Exposure** — `DesignerController` passes `FieldControlRuntime::catalogue()` as
  `PpDesignerBootstrap.plugins {view,edit,filter}`, and enriches `fieldsByTable()` with each
  field's assigned control (Fields module `viewAs`/`editAs` → `viewControl`/`editControl`
  + settings).
- **Field component** — `field.props` gains `editControl`/`viewControl` (+ `*Settings`).
  The render shows a front-end **approximation** of the control (instant, offline); the
  renderer's `upgradeFieldControls()` then fetches `POST /designer/field-preview`
  (`DesignerController::fieldPreview` → runtime) and swaps in the **real plugin HTML**
  (hybrid preview, cached). The inspector's *Field control* group is a plugin dropdown per
  category plus the selected plugin's `settingsSchema`. `model.fillForm` seeds each field's
  control from the Fields module default.

> Note: `PluginRegistry` now resolves a plugin's class from the class actually declared in
> `Plugin.php` when the folder name isn't a legal class name — this fixes the **Switch**
> edit control (`Switch` is a reserved word → class `SwitchPlugin`), which was silently
> skipped before.

### Adding a new component type
Add one `PPD.registry.def('my-type', { label, icon, category, defaults(), render(node, ctx), schema(node), container? })`
in `app/registry.js`. It immediately appears in the palette, renders on the canvas,
and gets a property panel — no other file needs to change.
