{# Workspace shell composition — heading, context selector, edit chrome, card grid, add-card, and detail drawer singleton. Contract: ~/.claude/skills/ux-architect/components/workspace-shell.md v0.62 CSS refactor: Tailwind utility soup → semantic .dz-workspace-* / .dz-card-* / .dz-drawer-* classes (components/dashboard.css). Alpine state grammar (saveState, isDragging, isResizing, showPicker) preserved verbatim — :class bindings now toggle .is-dragging / .is-resizing / data-dz-save-state and CSS resolves the visual delta. #} {# #948: cards are server-rendered HTML (no JSON island, no x-for). Alpine state contracts to ephemeral fields only (drag/resize/save/picker); the DOM is the source of truth for layout. The workspace name and the catalog flow through `data-*` attributes that JS reads on demand rather than capturing reactively at init time. #}
{# Workspace heading — title + primary actions row. `purpose:` is developer intent (e.g. "Personal dashboard for support agents") and leaks into user-facing copy when rendered verbatim, with no awareness of who's viewing it. See #805. Title (the DSL's first positional string) is the user-facing name. Primary-action buttons are framework-inferred from region entities that have CREATE surfaces and for which the current persona has create permission — closes #827. #}

{{ workspace.title or workspace.name.replace('_', ' ').title() }}

{% if primary_actions %}
{% for action in primary_actions %} {{ action.label }} {% endfor %}
{% endif %}
{% if workspace.context_options_url %}
{% endif %} {# ── Toolbar ── #}
{# Reset button #} {# Save button — reflects saveState via data-dz-save-state attribute. #}
{# ── Card Grid ── #} {# Pointer drag/resize listeners are registered imperatively in the dashboard Alpine component's init()/destroy() (same lifecycle pattern used by dzTable for issue #795) — using @.window here leaks the listener across HTMX morph navigations and throws ReferenceError on the next pointer event. #} {# items-start: CSS Grid defaults to align-items: stretch, which sizes every row to its tallest card and leaves dead whitespace in shorter ones. Fixed in #844 — each cell now collapses to intrinsic content height. dashboard-builder.js only manipulates grid-column spans, so drag/resize behaviour is unaffected. #}
{# #948: server-rendered cards. The DOM is the source of truth for layout — drag/resize/add/delete mutate it directly. Card events wire via delegation on the grid container (see dashboard-builder.js init), so neither this server-rendered iteration nor any runtime-added card needs per-element Alpine `@` directives. Invariants preserved across the #948 migration: - data-test-id="dz-card-drag-handle" still anchors INTERACTION_WALK - card-title-{id} still id-matches the card chrome - notice band shape preserved including data-dz-notice-tone keyed off dz-tones.css (#906) - eager vs lazy hx-trigger split (#864) honoured per-card via {{ loop.index0 < (fold_count or 0) }} Whitespace-stripped via `{%- -%}` to keep the rendered HTML compact — each card was 1.6 KB pre-strip; ~30% of that was indentation + blank lines. On a 50-card workspace the saving is ~25 KB on the wire, ~15 ms at 4G transfer speeds. #} {%- for r in workspace.regions %} {%- set _card_id = 'card-' ~ loop.index0 %} {%- set _eager = loop.index0 < (fold_count or 0) %} {%- set _trigger = 'load' if _eager else 'intersect once' %} {%- if workspace.sse_url %} {%- set _trigger = _trigger ~ ', sse:entity.created, sse:entity.updated, sse:entity.deleted' %} {%- endif %}
{%- if r.eyebrow %}{{ r.eyebrow }}{% endif %}

{{ r.title or r.name.replace('_', ' ').title() }}

{%- if r.notice and r.notice.title %}
{{ r.notice.title }}
{%- if r.notice.body %}
{{ r.notice.body }}
{% endif %}
{%- endif %}
{%- endfor %}
{# ── Add Card ── #}
{% include 'workspace/_card_picker.html' %}
{# Detail drawer (preserved unchanged) #}