# 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](architecture.md#legacy-principal-identifiers)). Current evidence is in [implementation status](status.md). **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):** - `92f97c7` - Clean drag-drop UX, remove visual indicators (2026-10-09) - `c8f2e9c` - Stable resize, prevent grid reflow during drag (2026-10-09) - `f349711` - Apply the fixed six-column body grid to all authenticated main-body Presentation surfaces and remove per-PO action buttons (2026-10-22) **Previous milestone commits:** - `2be9f72` - Render Dashboard summary cards in grid - `b97fee8` - Restore Dashboard summary surfaces - `45f10b6` - Restore generic presentation object surfaces ## 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:** - All generic Dashboard, capability and Object page-level surfaces use the same six-column outer grid with persisted spans restricted to 2, 3, 4 and 6; responsive spanning remains available at narrow viewports - Each grid-level PO owns an independent content surface - Compact/Regular/Expanded and one-, two- or three-column internal delivery are derived from outer span - Drag-and-drop reordering through the top-right handle with live visual feedback - Resizable surfaces through the bottom-right handle with persistent width and height preferences - Layout persistence per user and Business - Generic presentation renderer (no domain-specific renderers) - Full inspection and Object identity tracking ## 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 ```json { "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) ```json { "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) ```json { "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) ```json { "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) ```json { "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:** - Grid width: 6 columns - Valid element spans: 1, 2, 3, 4, or 6 columns - New elements default to 1 column - Packing strategy: Fill rows left-to-right, prefer user-preferred spans where they fit - Never produce gaps or unused columns - Compute effective span when preferred span cannot fit in remaining row space - Applies only to the current page-surface list; it is not an internal content 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:** - Generic recursive renderer accepting `RenderablePresentation` objects - No domain knowledge of Dashboard, Capability, People, Marketing, etc. - CSS class application based on `kind`, `role`, and delivered `layout.delivery.effective_span` - Size class mapping: - `span: 2` → `size-33` (33% of grid, ~2 columns) - `span: 3` → `size-50` (50% of grid, ~3 columns) - `span: 4` → `size-66` (66% of grid, ~4 columns) - `span: 6` → `size-100` (100% of grid, ~6 columns) - Inline `style="min-height: Xpx"` applied from `layout.preferred_height` * 16px only when a PO or saved preference declares one (any integer of 1 or more; no system floor since 2026-10-28) - Renders `data-presentation-object-id` and `data-presentation-object-revision` for inspection ### 4. Layout Persistence **File:** `app/Runtime/Presentation/PresentationPreferenceService.php` **Storage:** User Presentation Preference Object (generic Object model) **Contract:** - Keyed by (principal_id, business_id, presentation_context_reference) - Sparse: only stores non-default preferences - Persisted properties per summary PO: - `preferred_span` (2|3|4|6) — user's chosen width - `preferred_order` (integer) — user's chosen position - `preferred_height` (integer units, 16px per unit) — user's chosen height - Applied post-projection, before packing - Existing Presentation Objects never mutated 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:** - `92f97c7` - Clean drag-drop UX, remove border indicators (2026-10-09) - `a9d3d58` - Drag n drop fix (2026-10-09) - `36bac03` - Restore Dashboard layout controls **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:** - **Dragged item:** `.dragging` class applies opacity 0.5 so user knows which card they're moving - **Other items:** Naturally flow and shift position due to DOM reordering (no additional styling) - **Grid layout:** CSS Grid automatically reflows items as DOM order changes - **No artificial indicators:** Removed `.drop-target` and `.drop-zone-before` classes for cleaner interface **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:** - `c8f2e9c` - Stable resize, prevent grid reflow during drag (2026-10-09) - `92f97c7` - Clean drag-drop UX (2026-10-09) **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. - Render time: inline `min-height` only when a preferred height is declared - Resize (pointermove handler): clamps to 1 rem; layout saves omit `preferred_height` for surfaces that never declared one - Applied as CSS base rule for fallback - Prevents cards from rendering too small **Grid stability during resize:** - CSS Grid normally uses `grid-auto-flow: row dense` for optimal packing - During resize, we switch to `grid-auto-flow: row` (strict row placement, no backfilling) - Result: Items stay in their current rows while card width changes - Once resize ends, standard layout resumes and grid may reflow to optimal arrangement ## Container Layout and Grid System ### Container Element **HTML class:** `presentation-elements dashboard-grid` **CSS grid:** - `display: grid` - `grid-template-columns: repeat(6, 1fr)` — 6 equal columns - Responsive breakpoints managed via media queries - Gap between elements: `var(--gap, 1rem)` ### Item Elements **HTML class:** `presentation-item widget` + size class **Size classes:** ```css .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:** - Drag handle: `.layout-drag-handle` (top-right, visible in edit mode) - Resize handle: `.resize-handle` (bottom-right, visible in edit mode) ### 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: - Main-body page-level Presentation surfaces use the shared six-column grid - Each grid-level PO has a separate content container whose effective internal layout is independent of its outer span - Each non-repeated PO has only its separate top-right drag handle and bottom-right resize handle (visible in edit mode) - Dragging updates card order in real-time - Resizing updates card size in real-time - Edit mode can be toggled on/off - The shared "Save layout" control persists position and size changes - Refresh page retains saved layout 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 - [Core Object Model and Platform Authority](../architecture/core-object-model-and-authority.md) — Presentation Object semantic role and contract - [Views and Content](../architecture/views-and-content.md) — Projection and rendering contract - [UI System](ui-system.md) — Grid layout and renderer boundaries - [Objects and Types](../architecture/objects-and-types.md) — Object model and revision semantics ## 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: - Dashboard loads without errors - Data-defined Dashboard child surfaces visible and independent - Cards have independent drag and resize handles - Edit mode toggles on/off - Drag-and-drop reorders cards smoothly - Resizing updates card height with minimum constraint - "Save layout" persists changes - Refresh page retains saved layout 2. **When extending presentation.** Consult [Views and Content](../architecture/views-and-content.md) 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: - Six-column span system - Valid spans (2, 3, 4, 6) - No gaps or orphaned columns - Responsive behavior on smaller screens - No system minimum card height; content never clipped 4. **When updating demonstrator profile.** Distinguish profile changes (provisioning template) from Business configuration (persisted state). Use the governed mutation mechanism to update existing Businesses.