Frontier Agent Briefing — Dazzle v0.55.47¶
📜 Historical snapshot — not current docs
Captured 2026-04-16 during Dazzle's autonomous-improvement cycles. It records the framework as it was then and the gap being worked at the time; it may not describe current behaviour. Start from the documentation home, or see Project Evolution for how these fit together.
For: Agents operating dev environments for Penny Dreadful, Aegismark, Cyfuture As of: 2026-04-16 Covers: v0.55.35 → v0.55.47 (12 patch releases, 30 commits, 15 cycles of autonomous UX work)
Pull the latest Dazzle before starting. This briefing covers what changed, what you need to know, and what patterns to follow.
1. What changed (executive summary)¶
The UX layer underwent a systematic modernisation arc. The three headlines:
-
DaisyUI is gone from the hot path. ~210+ DaisyUI class instances migrated to design-token Tailwind. Only 3 remain (stepper
step-primary). Every status badge, empty state, metric tile, tooltip, filter dropdown, and form field now renders throughhsl(var(--token))fromdesign-system.css. Dark mode adapts automatically. -
Persona-variant overrides now work end-to-end. DSL
for <persona>:blocks withempty:,hide:, andread_only:declarations actually affect the rendered UI. Before this arc they parsed but silently dropped. Also: newbacked_by/link_viaconstruct on personas for entity binding. -
8 new component contracts govern the template menagerie. Status badge, metrics region, empty state, tooltip, toggle group, plus 6 parking-lot primitives (breadcrumbs, alert banner, accordion, context menu, skeleton patterns, date range picker).
2. New DSL constructs you should use¶
2.1 Per-persona empty copy¶
surface task_list "Tasks":
uses entity Task
mode: list
ux:
empty: "No tasks yet"
for member:
empty: "You have no assigned tasks"
for admin:
empty: "No tasks yet — create one to get started"
How it works: The compiler collects per-persona overrides into a dict. At request time, page_routes.py matches the current user's role and swaps the empty message before rendering. First matching persona wins.
When to use: Any surface where different personas see different empty-state copy — especially when one persona can create and another can only view.
2.2 Per-persona field hiding¶
surface ticket_create "Create Ticket":
uses entity Ticket
mode: create
section main:
field title "Title"
field description "Description"
field assigned_to "Assigned To"
ux:
for customer:
hide: assigned_to
How it works: On list surfaces, hidden columns get column.hidden=True on the per-request copy. On form surfaces (create AND edit), hidden fields are removed from req_form.fields, every section's field list, AND req_form.initial_values (prevents hidden-field injection via pre-filled POST bodies).
When to use: When a persona shouldn't see or interact with a field. This is the direct fix for "customer sees the Assigned To field" (the EX-029 class of issue).
2.3 Per-persona read-only¶
How it works: On list surfaces: create_url=None, bulk_actions=False, inline_editable=[]. On form surfaces: returns HTTP 403 (forms are inherently mutation affordances).
2.4 Persona-entity binding¶
How it works: Declares that persona tester maps to entity Tester via the email field. At dazzle validate time, the linker checks: (1) entity exists, (2) link_via field exists on entity, (3) no two personas claim the same backing entity. At runtime (create handler only), when a ref Tester field is missing from the request body, the framework auto-injects the backing entity's ID by looking up SELECT id FROM tester WHERE email = ?.
When to use: Any app where a persona corresponds to a domain entity (Teacher/teacher, Agent/agent, Tester/tester). This replaces the fragile convention of matching auth user IDs to entity IDs in demo seeds.
Default link_via: email. Override to id if entity IDs match auth user IDs by convention (zero-cost, no DB lookup).
3. Template patterns that changed¶
3.1 Status badges — use the macro, never inline¶
{% from 'macros/status_badge.html' import render_status_badge %}
{{ render_status_badge(value=item.status) }}
{{ render_status_badge(value=item.priority, size='sm') }}
{{ render_status_badge(value=item.severity, bordered=true) }}
5 tones: neutral | success | warning | info | destructive. Derived automatically from the value via badge_tone filter (~30 mapped values covering status/priority/severity axes). Never write inline <span class="... badge_class ...">.
3.2 Empty states — two canonical shapes¶
Full (for list/grid/kanban/tree surfaces):
Renders SVG icon + copy + optional Create-first CTA (only whencreate_url is set).
Dense (for inline card regions — metrics, charts, timelines):
<p class="dz-empty-dense text-[13px] text-[hsl(var(--muted-foreground))]" role="status">
{{ empty_message | default("No data available.") }}
</p>
3.3 Ref fields — auto-select from entity list¶
Plain ref Entity fields on create/edit surfaces automatically render as <select> dropdowns that fetch options from the entity's list API. No explicit widget: search-select needed. Works for every ref field across every app.
3.4 Metric tiles — auto-display for aggregate regions¶
DSL regions with aggregate: blocks now auto-infer display: summary if no explicit display: is set. Previously they silently rendered as empty list regions.
workspace admin_dashboard "Admin Dashboard":
metrics:
source: Task
aggregate:
total_tasks: count(Task)
done: count(Task where status = done)
This will now render as a KPI tile grid (was previously blank).
3.5 Number formatting on metric tiles¶
All aggregate values pass through the metric_number Jinja filter: 1234 → 1,234. Applied automatically via the metrics region template.
4. Design token compliance¶
Rule: every colour in a template must resolve through hsl(var(--token)).
Available tokens (from design-system.css):
| Token | Purpose |
|---|---|
--background / --foreground |
Page body / text |
--card |
Card surfaces |
--muted / --muted-foreground |
Subdued surfaces / text |
--border |
Borders |
--primary / --primary-foreground |
Primary actions |
--destructive / --destructive-foreground |
Destructive actions |
--success / --success-foreground |
Positive states |
--warning / --warning-foreground |
Attention states |
--info / --info-foreground |
Informational states |
--ring |
Focus ring |
--popover |
Popover/dropdown surfaces |
Never use: bg-base-100, text-base-content, btn-primary, badge-success, alert-info, card-body, or any other DaisyUI class name. The framework has migrated away from DaisyUI; using legacy classes will produce unstyled or mis-styled output.
Exception: step-primary in the experience wizard stepper (3 instances, pending a dedicated stepper component rewrite).
5. Canonical class markers for automation¶
Every governed component now stamps a dz-* class marker and data-dz-* attributes:
| Component | Marker | Data attributes |
|---|---|---|
| Status badge | dz-status-badge |
data-dz-status-tone |
| Metric tile | dz-metric-tile |
data-dz-metric-key, data-dz-tile-count |
| Empty state (full) | dz-empty-state |
data-dz-empty-kind="actionable\|read-only", data-dz-empty-cta |
| Empty state (dense) | dz-empty-dense |
— |
| Tooltip (rich) | dz-tooltip |
data-dz-position, data-dz-tooltip-panel |
| Toggle group | dz-toggle-group |
data-dz-toggle-item, data-dz-value |
| Breadcrumbs | dz-breadcrumbs |
— |
| Alert banner | dz-alert-banner |
data-dz-alert-level |
| Accordion | dz-accordion |
data-dz-accordion-item, data-dz-section-id |
| Context menu | dz-context-menu |
data-dz-context-menu-panel, data-dz-context-menu-item |
| Skeleton | dz-skeleton |
— |
| Date range picker | dz-date-range-picker |
— |
| Ref entity select | — | data-dz-ref-entity, data-dz-ref-api |
| Region wrapper | — | data-dz-region, data-dz-region-name |
Use these for Playwright selectors, fitness-engine walkers, and contract assertions.
6. ARIA and keyboard accessibility¶
Additions in this arc:
role="status"on every status badge, empty state, and alert banneraria-labelon breadcrumb nav, toggle groups, and icon-only buttonsaria-pressedon toggle-group buttons@keydown.left/@keydown.rightarrow-key navigation on toggle groups@keydown.escape.windowon context menusfocus-visible:ring-1 focus-visible:ring-[hsl(var(--ring))]on toggle group buttons (keyboard-only ring, no mouse flash)[x-cloak] { display: none !important; }CSS rule indazzle-layer.cssprevents first-paint flash on Alpine components
7. Security posture changes¶
Three latent XSS vectors were closed:
-
fragments/tooltip_rich.html—{{ content | safe }}removed. Content is now HTML-escaped via Jinja autoescape. Trigger blocks still use| safe(callers supply markup for the trigger element). -
fragments/accordion.html—{{ section.content | safe }}removed. Static content is now autoescaped. Structured content should lazy-load from a template endpoint viasection.endpoint. -
.badge-errorCSS rule — referenced undefined--ervariable instead of--destructive. Every rendered destructive badge was silently mis-coloured. Fixed.
Rule: Never pipe user-supplied or DSL-authored content through | safe in a template. Jinja autoescape is the security posture. Only {% block %} overrides where the caller explicitly supplies HTML markup should use | safe.
8. PersonaVariant resolver pattern (for framework-level work)¶
If you need to wire a new PersonaVariant field through to the runtime:
- Add a dict to
TableContext(orFormContext) insrc/dazzle_page/runtime/template_context.py - Populate it in
_compile_list_surface(or_compile_form_surface) fromux.persona_variantsinsrc/dazzle_page/converters/template_compiler.py - Resolve it in
_apply_persona_overrides(tables) or_apply_persona_form_overrides(forms) insrc/dazzle_page/runtime/page_routes.py
Both helpers use first-wins role matching with role_ prefix stripping. The pattern is documented in the helper docstrings.
Currently wired: empty_message, hide, read_only. Remaining unwired (no template consumer exists yet): purpose, show, show_aggregate, action_primary, defaults, focus.
9. What to watch for¶
When building apps on v0.55.47:
-
Aggregate regions now render. If your workspace has
aggregate:blocks withoutdisplay:, they'll now render as metric tiles (was previously blank). If you intended a list with aggregates, add explicitdisplay: summary. -
Ref fields are now selects. Every
ref Entityfield on create/edit surfaces renders as a<select>dropdown fetching from the entity's list API (max 100 items). If your entity has >100 records, the dropdown will truncate. For large collections, use explicitsource:with a search endpoint. -
backed_bychanges scope-rule semantics. If you declarebacked_by: Tester, the create handler will auto-inject the tester's ID forref Testerfields. This may change behaviour for apps that previously relied on the field being empty or manually filled. Test your create flows after addingbacked_by. -
Per-persona
hide:removes fields from forms. Hidden fields are removed from the POST body, not just visually hidden. If your backend has arequiredconstraint on a hidden field and no default/auto-injection, you'll get a 422 validation error. Either: (a) add adefaults:block on the persona variant, (b) make the field optional in the DSL, or (c) useinject_current_user_refs/backed_byauto-injection.
10. Quick reference: file locations¶
| Concern | File |
|---|---|
| Component contracts | ~/.claude/skills/ux-architect/components/*.md |
| Design tokens | src/dazzle_page/runtime/static/css/design-system.css |
| Status badge macro | src/dazzle_page/templates/macros/status_badge.html |
| Empty state fragment | src/dazzle_page/templates/fragments/empty_state.html |
| Region wrapper macro | src/dazzle_page/templates/macros/region_wrapper.html |
| Persona resolver (tables) | src/dazzle_page/runtime/page_routes.py → _apply_persona_overrides |
| Persona resolver (forms) | src/dazzle_page/runtime/page_routes.py → _apply_persona_form_overrides |
| Backed-by auto-injection | src/dazzle_http/runtime/route_generator.py → resolve_backed_entity_refs |
| Badge tone map | src/dazzle_page/runtime/template_renderer.py → _STATUS_TONE_MAP |
| Metric number formatter | src/dazzle_page/runtime/template_renderer.py → _metric_number_filter |
| Aggregate display inference | src/dazzle_page/runtime/workspace_renderer.py line ~260 |
| PersonaVariant IR | src/dazzle/core/ir/ux.py → PersonaVariant |
| PersonaSpec IR | src/dazzle/core/ir/personas.py → PersonaSpec |
| Linker validation | src/dazzle/core/validator.py → _validate_persona_backed_by |
| UX cycle backlog | dev_docs/ux-backlog.md |
| UX cycle log | dev_docs/ux-log.md |
| Component roadmap | dev_docs/framework-gaps/2026-04-15-component-menagerie-roadmap.md |