← Back to directory

PRESENTATION-GRID-ARCHITECTURE-BASELINE.md

implementation/PRESENTATION-GRID-ARCHITECTURE-BASELINE.md · 26.0 KB · 2026-11-02 12:48

Presentation Grid Architecture - Working Baseline

> Terminology note, 2026-11-02. This is a historical implementation baseline outside the governed authority map (PRR-048). "Principal" and principal_id below mean the User (the platform identity); principal_id is a legacy persisted identifier retained until a separately authorised migration (legacy identifiers). Current evidence is in implementation status.

Status: Generic Presentation layout implementation, focused-verified locally against a refreshed STAGING snapshot. The six-column page layout, layout persistence, drag/drop and resizing remain the outer Presentation Object contract. PO-internal content layout is derived from the effective outer span; protected STAGING release and browser acceptance remain release evidence.

Previous known-good commit: f349711 (Shared body grid and direct manipulation, 2026-10-22). The independent-content correction is the current focused-verified implementation pending protected release.

Recent improvements (post-75531aa):

Previous milestone commits:

Overview

The generic Presentation renderer uses the fixed Idiosyncrasy outer body-layout mechanism: authenticated page-level Presentation Objects use a six-column grid, with drag-and-drop and resizing on each non-repeated Presentation Object. Each grid-level PO has a generic surface. Recursive content is rendered inside that surface through an independent content grid; the outer packing path is not applied to descendants.

Key working features:

Architecture Layers

1. Application Entry Point

Canonical route: GET /objects/{dashboard_object_id}

GET / resolves the Business's exact presentation.dashboard_ref and redirects to this canonical Object route. There is no separate /dashboard route.

File: app/Runtime/Presentation/TargetHtmlBoundary.php

Contract: 1. Require authenticated User (legacy identifier principal_id) and Business context 2. Load Business object and resolve presentation.dashboard_ref (exact revision) 3. Read Dashboard Presentation Object at resolved revision 4. Extract Dashboard's data-defined children[] PO references 5. Project children through generic projector 6. Apply persisted user layout preferences 7. Pack elements into six-column grid 8. Render through generic presentation.php template

Verified flow:

Business object
  ↓ presentation.dashboard_ref (exact revision)
Dashboard PO (id: 96000000-0000-4000-8000-000000000201, revision: 1)
  ↓ children[] (data-defined child POs)
[Capability and content POs selected by the Business presentation]
  ↓ authorized projection
RenderablePresentation.elements[]
  ↓ layout preferences + packing engine
Effective grid layout
  ↓ generic HTML renderer
six-column grid on page

2. Presentation Object Configuration

The working Dashboard uses four top-level summary surfaces, each a persisted Presentation Object with the following structure:

Dashboard Root PO

{
  "id": "96000000-0000-4000-8000-000000000201",
  "revision": 1,
  "value": {
    "definition_kind": "presentation",
    "kind": "container",
    "element_id": "dashboard",
    "order": 0,
    "children": [
      { "kind": "object_reference", "id": "96000000-0000-4000-8000-000000000237", "revision": 1 },
      { "kind": "object_reference", "id": "96000000-0000-4000-8000-000000000238", "revision": 1 },
      { "kind": "object_reference", "id": "96000000-0000-4000-8000-000000000239", "revision": 1 },
      { "kind": "object_reference", "id": "96000000-0000-4000-8000-000000000240", "revision": 1 }
    ]
  }
}

Dashboard child POs (illustrative historical profile)

The following four-card examples describe the earlier demonstrator profile and remain useful as a shape example only. They do not limit the current generic profile, which may expose all eight foundational capability POs.

People Summary (ID: 96000000-0000-4000-8000-000000000237, revision 1)

{
  "kind": "container",
  "element_id": "people-summary",
  "order": 0,
  "layout": { "preferred_span": 3 },
  "children": [
    { "kind": "object_reference", "id": "96000000-0000-4000-8000-000000000241", "revision": 1 }
  ]
}
Content: Text element displaying "People (6)"

Marketing Summary (ID: 96000000-0000-4000-8000-000000000238, revision 1)

{
  "kind": "container",
  "element_id": "marketing-summary",
  "order": 1,
  "layout": { "preferred_span": 3 },
  "children": [
    { "kind": "object_reference", "id": "96000000-0000-4000-8000-000000000242", "revision": 1 }
  ]
}
Content: Text element displaying "Marketing (2)"

Sales Summary (ID: 96000000-0000-4000-8000-000000000239, revision 1)

{
  "kind": "container",
  "element_id": "sales-summary",
  "order": 2,
  "layout": { "preferred_span": 3 },
  "children": [
    { "kind": "object_reference", "id": "96000000-0000-4000-8000-000000000243", "revision": 1 }
  ]
}
Content: Text element displaying "Sales (1 enquiry, 1 qualification)"

Projects Summary (ID: 96000000-0000-4000-8000-000000000240, revision 1)

{
  "kind": "container",
  "element_id": "projects-summary",
  "order": 3,
  "layout": { "preferred_span": 3 },
  "children": [
    { "kind": "object_reference", "id": "96000000-0000-4000-8000-000000000244", "revision": 1 }
  ]
}
Content: Text element displaying "Projects (3 projects, 3 milestones)"

Configuration source: database/configuration/idy-008/spearmint-demonstrator-profile.json (lines 79–95 for Dashboard, lines 700–784 for summary POs)

3. Generic Projection and Rendering

Projection Pipeline — verified implementation

File: app/Runtime/Presentation/PersistedPresentationObjectProjector.php

Verified committed path:

Persisted PO references
  ↓ authorization check (AuthorizedObjectReader)
  ↓ recursive child resolution
  ↓ content binding resolution (logic, fields, literals)
  ↓ apply user layout preferences (PresentationPreferenceService)
  ↓ derive mode/content metadata from effective outer span
  ↓ six-column packing for the current page-surface list only (LayoutPackingEngine)
  ↓ RenderablePresentation.elements[]

Only page-surface elements receive outer effective_span, row and position metadata. Descendant POs retain identity, parent containment, content and independent content-column metadata, but do not receive the parent outer-grid coordinates.

Layout Packing Engine

File: app/Runtime/Presentation/LayoutPackingEngine.php

Contract:

layout engine

Example: Four cards with preferred_span=[3, 3, 3, 3]:

Row 1: Card A (span 3) | Card B (span 3)  [6 columns filled]
Row 2: Card C (span 3) | Card D (span 3)  [6 columns filled]

HTML Renderer

File: resources/views/target/presentation.php

Contract:

4. Layout Persistence

File: app/Runtime/Presentation/PresentationPreferenceService.php

Storage: User Presentation Preference Object (generic Object model)

Contract:

The revision-2 generic preference model persists only outer order, span and height. Presentation mode and internal content columns are derived delivery metadata and are not user-selectable or persisted separately.

Persistence endpoint: POST /presentations/preference/order

Verified operation: 1. The shared Edit layout control is activated → layout-editing class added 2. Drag the top-right handle or resize from the bottom-right handle → DOM updates immediately with visual feedback 3. The shared Save layout control is activated → AJAX POST to /presentations/preference/order 4. The server updates the user's persisted position and size preferences 5. The page remains in edit mode until save completes 6. Refresh page → layout preferences reapplied from saved state

5. Front-End Interaction

Drag and Drop

File: resources/js/dashboard-drag-drop.js and target/js/dashboard-drag-drop.js

Known-good commits:

Interaction flow:

1. User clicks "Edit layout"
   → container.classList.add('layout-editing')
   → drag handles visible on all cards (⠿ icon)

2. User grabs drag handle and drags over another card
   → dragstart event fires
   → dragged item marked with 'dragging' class (50% opacity)
   → DOM reorders in real-time using insertBefore()
   → layout updates immediately with natural item flow
   → NO visual border/background indicators (cleaner UX)

3. As user continues dragging over other items
   → dragover event fires on each target
   → DOM continues to reorder (preview is live and accurate)
   → items shift smoothly in the grid

4. User releases mouse
   → dragend event fires
   → dragging class removed
   → DOM retains new order (preview was accurate—no surprises)
   → order not yet persisted to server

5. User clicks "Save layout"
   → AJAX POST captures current order, spans, heights
   → Server persists layout preference
   → UI confirms save

Visual feedback mechanism:

Design principle: The actual layout preview IS the visual feedback. No need for decorative indicators.

Resizing

File: resources/js/dashboard-drag-drop.js (pointerdown handler)

Known-good commits:

Interaction flow:

1. User clicks and holds resize handle (bottom-right corner)
   → pointerdown event fires on resize-handle element
   → resizing state captured
   → container.classList.add('resizing')  ← Prevent grid reflow
   → pointer capture engaged

2. User drags to resize
   → pointermove event fires
   → calculate new span based on horizontal movement
   → calculate new height based on vertical movement
   → preferred_height = max(1rem, measured height after drag) (no 192px floor)
   → apply new size class immediately (visual update)
   → OTHER ITEMS REMAIN STABLE (no jumping to other rows)
   → CSS rule: .dashboard-grid.resizing { grid-auto-flow: row; }

3. User releases mouse
   → pointerup event fires
   → container.classList.remove('resizing')  ← Allow reflow
   → persist resized span and height to data attribute
   → save preference via AJAX
   → layout settles to final arrangement

Size snap points: [2, 3, 4, 6] columns — nearest snap point chosen during drag

Minimum height: none imposed by the system (changed 2026-10-28; previously 192px / 12 units). preferred_height is an optional floor of 1 rem or more declared by a PO or saved preference; content always grows the surface.

Grid stability during resize:

Container Layout and Grid System

Container Element

HTML class: presentation-elements dashboard-grid

CSS grid:

Item Elements

HTML class: presentation-item widget + size class

Size classes:

.size-33 { grid-column: span 2; }  /* 2 of 6 columns = 33% */
.size-50 { grid-column: span 3; }  /* 3 of 6 columns = 50% */
.size-66 { grid-column: span 4; }  /* 4 of 6 columns = 66% */
.size-100 { grid-column: span 6; } /* 6 of 6 columns = 100% */

Minimum height: no base CSS floor (the former min-height: 192px rule was removed 2026-10-28); inline style="min-height: Xpx" is emitted only for a declared preferred_height

Handles:

PO Surface and Internal Content

The outer .dashboard-grid (or equivalent page-surface mount) is the only container to which the six-column packing contract applies. Each grid-level PO must render its own generic surface, and its child content must be contained inside that surface without inheriting outer size-*, row or position metadata. Internal content is one column for Compact span 1, two columns for Regular span 2 and three columns for Expanded spans 3, 4 and 6. A narrow viewport may reduce the effective count, but must not rewrite the persisted outer preference or alter the persisted PO graph.

Architectural Invariants (Regression Guardrails)

These invariants must be preserved in all future development:

1. Do not replace the generic renderer. The recursive persisted Presentation Object path with generic presentation.php is the only authoritative rendering path. Domain-specific Dashboard, Capability, People, Marketing, Sales or Projects renderers must not be introduced.

2. Do not equate recursive PO references with visibility. A Presentation Object that is reachable through recursive relationships is not automatically rendered on the Dashboard. Only POs explicitly referenced in Dashboard.children[] are Dashboard surfaces.

3. Do not turn data-resolution constituents into surfaces. Content resolved from a field binding, logic evaluation, or collection enumeration does not automatically become an independent grid surface. Only elements with presentation_surface: true and presentation_object_id are rendered as separate cards.

4. Do not disable recursive resolution. The generic projector must continue resolving child POs recursively. If a rendering defect appears, fix the specific PO configuration or projection logic, not the recursion itself.

5. Do not delete collection or record POs. Collection and record Presentation Objects (used for People, Marketing, Sales, Projects list and detail pages) must remain available even if they don't appear on the Dashboard. Deleting them breaks list and detail navigation.

6. Do not modify the grid engine for PO configuration problems. If the grid layout looks wrong, the issue is in persisted PO configuration (preferred spans, order, binding), not in LayoutPackingEngine. Grid algorithm changes must have specific evidence of defects in packing logic.

7. Do not assume demonstrator profile updates apply to existing Businesses. The spearmint-demonstrator-profile.json is a provisioning template. Changing it does not update already-provisioned Businesses. Layout changes must be made through the governed extension/mutation mechanism or database update.

8. Preserve drag-and-drop and resizing. Layout interaction (drag, resize, persistence, visual feedback) must remain functional when changing presentation projection, PO configuration or rendering. Verify the actual staging UI after deployment; tests alone do not establish visual correctness.

9. Verify staged layout. After deployment, visit the resolved /objects/{dashboard_object_id} route in staging and confirm:

10. Keep outer and internal layout independent. The outer packing engine may run only for page-surface POs. An outer span of two, three, four or six columns select only the derived Compact/Regular/Expanded delivery mode; nested content remains inside its parent surface and never enters the outer packing engine.

11. Preserve semantic failure distinctions. Do not fabricate missing Presentation Objects, definitions, targets or latest revisions. Focused verification must distinguish genuine empty authorized data from branches suppressed because exact resolution, authorization, definition validation or cycle checks failed.

Known-Good Configuration

Business Presentation Root

Business
  ↓ presentation.dashboard_ref
Dashboard PO (96000000-0000-4000-8000-000000000201 @ revision 1)
  ├── People Summary PO (96000000-0000-4000-8000-000000000237 @ revision 1)
  │   └── Text: "People (6)"
  ├── Marketing Summary PO (96000000-0000-4000-8000-000000000238 @ revision 1)
  │   └── Text: "Marketing (2)"
  ├── Sales Summary PO (96000000-0000-4000-8000-000000000239 @ revision 1)
  │   └── Text: "Sales (1 enquiry, 1 qualification)"
  └── Projects Summary PO (96000000-0000-4000-8000-000000000240 @ revision 1)
      └── Text: "Projects (3 projects, 3 milestones)"

Navigation Layer (Separate)

Each summary container PO (People, Marketing, Sales, Projects) has a corresponding navigation entry:

People Container PO (96000000-0000-4000-8000-000000000202)
  ↓ navigation: { navigable: true, label: "People", icon: "people", order: 0 }
  └── Children: People list and detail POs

Marketing Container PO (96000000-0000-4000-8000-000000000205)
  ↓ navigation: { navigable: true, label: "Marketing", icon: "marketing", order: 1 }
  └── Children: Campaign list and detail POs

Sales Container PO (96000000-0000-4000-8000-000000000207)
  ↓ navigation: { navigable: true, label: "Sales", icon: "sales", order: 2 }
  └── Children: Enquiry and qualification list POs

Projects Container PO (96000000-0000-4000-8000-000000000210)
  ↓ navigation: { navigable: true, label: "Projects", icon: "projects", order: 3 }
  └── Children: Project and milestone list POs

Important: The Dashboard uses summary POs (IDs 237–240). Navigation uses container POs (IDs 202, 205, 207, 210). They are separate POs and must not be conflated.

Implementation Status

| Component | Status | Evidence | |-----------|--------|----------| | Generic Presentation Renderer | ✓ Verified | resources/views/target/presentation.php renders correctly; no domain knowledge | | Presentation Object Projection | ✓ Verified | PersistedPresentationObjectProjector.php resolves children and applies preferences | | Layout Packing Engine | ✓ Verified | LayoutPackingEngine.php packs current page-surface children into the six-column grid without gaps | | Layout Persistence | ✓ Verified | User preferences saved and reapplied; verified on refresh | | PO surface/internal content separation | ✓ Focused-verified | The projector packs only the page surface; recursive content uses presentation-content-grid without outer positions or spans | | Derived internal content layout | ✓ Focused-verified | Effective outer spans derive Compact/Regular/Expanded mode and one-/two-/three-column content delivery; no separate preference is persisted | | Drag and Drop | ✓ Improved | Cards reorder smoothly with clean UX; no decorative borders (post-92f97c7) | | Resizing | ✓ Improved | Cards resize without item jumping; grid stable during drag (post-c8f2e9c) | | Minimum Height | ✓ Changed 2026-10-28 | No system floor; declared preferred_height (≥1rem) is honoured at render and resize, content never clipped | | Dashboard Configuration | ✓ Verified | Data-defined Dashboard children remain separate generic PO surfaces | | Edit Mode Toggle | ✓ Verified | Handles visible/hidden on toggle; save button appears | | Navigation | ✓ Verified | Data-defined navigation entries resolve through the authorized Presentation graph |

Related Documentation

Commit Timeline

| Commit | Date | Purpose | |--------|------|---------| | c8f2e9c | 2026-10-09 | Stable resize, prevent grid reflow during drag | | 92f97c7 | 2026-10-09 | Clean drag-drop UX, remove visual indicators | | 75531aa | 2026-10-09 | Resize fix (baseline reference) | | a9d3d58 | 2026-10-09 | Drag n drop fix | | 232f2f7 | 2026-10-09 | Route Dashboard layout saves through HTML boundary | | f7ff5a9 | 2026-10-08 | Bust Dashboard layout script cache | | 2d8ec11 | 2026-10-08 | Fix Dashboard layout save CSRF | | a2cee89 | 2026-10-08 | Add explicit Dashboard layout save | | f80e399 | 2026-10-08 | Align responsive Dashboard breakpoints | | cbfa438 | 2026-10-08 | Restore vertical Dashboard resizing | | 2be9f72 | 2026-10-07 | Render Dashboard summary cards in grid | | b97fee8 | 2026-10-07 | Restore Dashboard summary surfaces | | 45f10b6 | 2026-10-07 | Restore generic presentation object surfaces |

Next Steps for Future Development

1. Verify this baseline. Run this checklist after any future deployment:

2. When extending presentation. Consult Views and Content for the contract. Add data-defined child POs to Dashboard.children[], not as separate navigation pages or capability definitions.

3. When modifying the grid. Ensure changes preserve:

4. When updating demonstrator profile. Distinguish profile changes (provisioning template) from Business configuration (persisted state). Use the governed mutation mechanism to update existing Businesses.