Documentation
- CSS
@starting-styleguide section (v0.6.0) — documents that browser-native@starting-styleworks unmodified with djust's VDOM insert path. No new djust attributes or JS — the feature is pure CSS. Guide section indocs/website/guides/declarative-ux-attrs.mdincludes a quick-start example, a side-by-side comparison vsdj-transition(browser support, runtime cost, per-element customization), interop notes withdj-removefor enter+exit coverage, and caveats around@supportsgating for older browsers. ROADMAP parity-tracker row updated to ✅ Documented v0.6.0.
Changed
- Package consolidation sunset — ADR-007 Phase 4 closure (v0.6.0) — the three-phase consolidation that started in v0.5.0 is now complete. The five sibling repos (
djust-auth,djust-tenants,djust-theming,djust-components,djust-admin) are sunset atv99.0.0— each retains a shim-only__init__.pythat re-exports fromdjust.<name>and emits aDeprecationWarning. Path A was chosen over PyPI publish: existing releases remain installable indefinitely for legacy projects; no new PyPI versions will ship. djust core now exposes the consolidation via[project.optional-dependencies]—djust[auth],djust[tenants](withdjust[tenants-redis]anddjust[tenants-postgres]backend-specific sub-extras),djust[theming],djust[components],djust[admin]. Two new extras (auth,tenants) added in this release; the others shipped in v0.5.0. ADR-007 status updated from "Proposed" → "Accepted + Phase 4 complete". New migration guide:docs/website/guides/migration-from-standalone-packages.md(mechanical sed script + FAQ + edge cases). Cosmetic tech-debt: sibling repos retain dead pre-consolidation source files next to the shim — cleanup tracked separately; no user impact.
Added
Request-path profiling harness (v0.6.0, investigative, ROADMAP Group 5 P2) — reproducible profile of the mount → event → VDOM diff → patch path. New
scripts/profile-request-path.py(cProfile wrapper, optional py-spy hint, writesartifacts/profile-<timestamp>.{txt,pstats}; exits non-zero on target-miss for CI). Newtests/benchmarks/test_request_path.pywith eight pytest-benchmark cases across four groups (HTTP render, WebSocket mount, event dispatch, VDOM diff+patch) with hard assertions against the 2 ms per-event / 5 ms list-update budgets. Newdocs/performance/v0.6.0-profile.mdreporting all measured timings (mount 0.07 ms, event 4 µs, VDOM diff 4 µs, list reorder 0.38 ms — all within targets by at least 5x). Newmake profiletarget wired to the harness (the priormake profileruntime-stats target is nowmake profile-stats). No optimizations were required; the profile confirms the existing Rust-side architecture is well under target.Service Worker advanced features (v0.6.0) — three SW-backed optimizations landed in one PR:
- VDOM patch cache: per-URL HTML snapshots served instantly on popstate, then reconciled against the live WebSocket mount reply. Configurable via
DJUST_VDOM_CACHE_ENABLED/DJUST_VDOM_CACHE_TTL_SECONDS/DJUST_VDOM_CACHE_MAX_ENTRIES. New system checksdjust.C301/C302/C303guard config ranges. - LiveView state snapshots: opt-in per view via
enable_state_snapshot = Trueon aLiveViewsubclass. Client captures JSON-serializable public state ondjust:before-navigate; server restores via_restore_snapshot(state)in lieu ofmount()when the user hits back. Views override_should_restore_snapshot(request)to reject stale snapshots. System checkdjust.C304warns when a snapshot-opt-in view declares attributes matching PII naming patterns. - Mount batching: when multiple
dj-lazyLiveViews hydrate together, the client sends onemount_batchWebSocket frame instead of N separatemountframes. Server responds with onemount_batchcarrying all rendered views; per-view failures are isolated in afailed[]array (atomicity relaxed so one bad view doesn't kill the batch). Opt out viawindow.DJUST_USE_MOUNT_BATCH = false. - New client module
46-state-snapshot.js(~120 LOC); new senders ondjust._sw.cacheVdom/lookupVdom/captureState/lookupState. registerServiceWorker({vdomCache: true, stateSnapshot: true})gates the new behaviors alongside existinginstantShell/reconnectionBridgeoptions.
See
docs/website/guides/service-worker.md.- VDOM patch cache: per-URL HTML snapshots served instantly on popstate, then reconciled against the live WebSocket mount reply. Configurable via
Changed
LiveViewConsumer.handle_mount()accepts newstate_snapshotkwarg; dispatches to the snapshot-restore path when the view opts in and the payload'sview_slugmatches. New methodhandle_mount_batch()+_mount_one()collector seam enable the mount-batch path without regressing the single-viewmountflow.
Security
State snapshots are JSON-only (no pickle).
safe_setattrblocks dunder keys and private (_-prefixed) attributes during restoration. SW enforces a 256 KB upper bound onstate_jsonpayloads; client clamps at 64 KB. System checkdjust.C304warns when snapshot-opt-in views declare attribute names matchingpassword|token|secret|api_key|pii.Sticky LiveViews (v0.6.0) — Phoenix
live_render sticky: trueparity. Shipped across three PRs: #966 (Phase A — embedding primitive), #967 (Phase B — preservation acrosslive_redirect), #969 (Phase C — ADR-011, user guide, demo app). Mark a LiveView class withsticky = True+sticky_idand embed it via{% live_render "myapp.views.AudioPlayerView" sticky=True %}. Destination layouts declare<div dj-sticky-slot="<id>"></div>at the re-attachment point; the same Python instance, DOM subtree, form values, scroll/focus, and background tasks all survivelive_redirectnavigation. Use case: app-shell widgets (audio players, sidebars, notification centers), wizard preview panes.User-facing API
LiveView.sticky: bool = False+sticky_id: Optional[str] = Noneclass attrs.{% live_render "dotted.path" sticky=True %}template tag (validates class opt-in at render time;TemplateSyntaxErroron mismatch).[dj-sticky-slot="<id>"]slot markers in destination layouts.djust:sticky-preserved/djust:sticky-unmountedCustomEvents for lifecycle hooks (reasons:server-unmount,no-slot,auth)._on_sticky_unmount()per-instance hook (default: cancels pendingstart_asynctasks).
Wire protocol
child_update(Phase A) — scoped VDOM patches for embedded non-sticky children.sticky_hold(server→client, sent BEFOREmountonlive_redirect) — enumerates surviving sticky_ids so the client reconciles its stash against the authoritative list. Ordering is load-bearing: the mount handler eagerly reattaches, so a latesticky_holdwould reattach auth-revoked views.sticky_update(server→client) — per-child VDOM patches scoped to[dj-sticky-view="<id>"]via a newapplyPatches(patches, rootEl)variant in12-vdom-patch.js(whenrootElis non-null, node lookups / focus save-restore / autofocus queries all scope to that subtree).- Per-view VDOM version tracking via
clientVdomVersions: Map<view_id, number>with"__root"sentinel for top-level patches.
Client-side
static/djust/src/45-child-view.js—stickyStashMap;stashStickySubtrees()(detach on outbound nav),reconcileStickyHold(views)(drop non-authoritative),reattachStickyAfterMount()(replace[dj-sticky-slot]with stashed subtree viareplaceWith()— DOM identity preserved),handleStickyUpdate(msg)(scoped patch apply),clearStash()(abnormal-close cleanup).18-navigation.jscallsstashStickySubtrees()BEFORE outboundlive_redirect_mount(and beforepopstate-triggered redirects).03-websocket.jsonclose callsclearStash()on abnormal disconnect.[dj-root]audit across40-dj-layout.js,24-page-loading.js,12-vdom-patch.jsautofocus sites adds:not([dj-sticky-root])so sticky children don't masquerade as layout / page roots.
Security
- Per-sticky auth re-check via new
djust.auth.check_view_auth_lightweight(view, request) -> bool; a sticky view whose permissions are revoked mid-session is unmounted on the next navigation. DJUST_LIVE_RENDER_ALLOWED_MODULESprefix-allowlist gates dotted-path resolution (unset = permit-all, backward compatible).sticky_idHTML-escaped via server-sideescape()+CSS.escapeon client-side selectors.- Client stash bounded by developer-authored content; idempotent
stashStickySubtreescoalesces duplicates; cleared on abnormal WS close. - Inbound
sticky_update/sticky_holdframes rejected by the consumer's allowlist (server-to-client only).
Testing (32 Python + 20 JSDOM + 6 integration)
- 11 Phase A tests in
tests/unit/test_live_render_tag.py(HTML-parsed) + 21 Phase B/C tests intests/unit/test_sticky_preserve.py. - 7 Phase A tests in
tests/js/child_view.test.js+ 15 Phase B/C tests intests/js/sticky_preserve.test.js. - 3 end-to-end tests in
tests/integration/test_sticky_redirect_flow.py(Dashboard→Settings preservation, rapid A→B→A instance identity, no-slot reconcile path) + 3 demo-app smoke tests covering the full navigation cycle. - Phase C regression tests:
skipMountHtmlmount branch reattaches sticky subtrees (Fix F1);disconnect()drains_sticky_preservedso background tasks don't leak (Fix F2).
Documentation
- ADR-011 — wire protocol, DOM attributes, client/server flow diagrams, full security model + threat matrix, failure modes, relationship to v0.7.0
dj-activity. - User guide — quick start, common patterns, limitations, debugging, FAQ.
- Runnable demo app in
examples/demo_project/sticky_demo/— Dashboard, Settings, Reports pages with sticky AudioPlayer + NotificationCenter widgets showing preservation +no-slotunmount.
FLIP list-reorder animations (v0.6.0 animations milestone finale) — Opt-in per container via
dj-flip. Declarative attribute on a list parent animates direct-child reorders using First-Last-Invert-Play. Tunables:dj-flip-duration(default 300ms, parsed viaNumber+isFinite+ clamp[0, 30000]— trailing garbage rejects to fallback),dj-flip-easing(defaultcubic-bezier(.2,.8,.2,1), strings containing;"'<>rejected to defeat CSS-property-breakout). Respectsprefers-reduced-motion. Nested[dj-flip]isolated viasubtree: false. Author-specified inlinetransformon children is preserved across the animation. Overlapping reorders are guarded against cache corruption via an in-flight-transition check. Works with keyed lists where items carry stableid=(Rust VDOM emits MoveChild). Lands instatic/djust/src/44-dj-flip.js(~260 LOC). 12 JSDOM tests intests/js/dj_flip.test.js.{% djust_skeleton %}shimmer placeholder (v0.6.0 animations milestone finale) — Template tag for placeholder blocks. Props:shape(line|circle|rect, whitelist-validated),width/height(regex-whitelisted against^[\d.]+(px|em|rem|%|vh|vw|ch)?$, invalid falls back to shape default),count(clamped to[1, 100]),class_. All values HTML-escaped viabuild_tag(). Shimmer@keyframesemitted once per render viacontext.render_context. Integrates with existingdj-loadingshorthand and with{% if async_pending %}server blocks. 21 Python tests intests/unit/test_djust_skeleton_tag.py.