ZYEL LAB / ENGINEERING FIELD NOTES
Build a workspace that remembers how you work.
By Chason Arthur · UX Workspace Studio · Implementation tutorial
Configuration becomes useful when it reaches the surfaces people actually use. This example connects controlled appearance tokens to navigation, forms, data tables and overlays, then saves the user's working arrangement in the browser. The code deliberately keeps a small boundary: preferences are not application records, authentication or account synchronization.
Open the working demo ↗ · Setup and measured evidence · Preference contract source
Record this tutorial: original script and numbered shot plan. Rehearse with the demo’s confirmed temporary walkthrough so saved valid preferences remain unchanged after the checkpoint.
Start with a small, reproducible workspace.
Objective and prerequisites
Run the existing repository without pointing at its production database. Use Node.js 24, a current browser and a local HTTP server. The Studio itself needs no package installation or backend. The shared DataTables files are vendored; opening the page through file:// will break module loading.
Recommended implementation and code
From the integrated repository root, run the restricted static preview server, then open the route. It refuses backend/config scripts and does not run the site's production front controller.
node lab/tools/serve.mjs .
# Open http://127.0.0.1:4173/lab/workspace-studio/
node --test lab/workspace-studio/tests/model.test.mjsThe page imports ../shared/runtime.mjs, data.mjs and table.mjs. Only lab/workspace-studio/ belongs to this project. The common shell, storage adapter, dataset and vendored dependencies are shared foundation, not another demo. The 72 dealer rows are synthetic. City geography is illustrative; names and revenue do not describe customers.
Controls → allowlisted model → shared appearance store → CSS attributes
Table UI → DataTables + ColReorder → route/table-scoped store
JSON file → size + schema validation → table API → appearance store
↳ rollback on adapter failurePractical alternatives, pros and cons
A Vite or Vue application offers typed components and a familiar build pipeline, but adds tooling to a small native module demo. A server preference endpoint enables account sync, but creates identity, privacy and conflict-resolution work. Static modules were chosen because these controls need neither an authenticated account nor business data.
Common pitfalls
Do not copy private environment files into a public preview, invoke the legacy database-backed root page for a static test, or infer that a static UI demonstrates backend authorization. Serve only the intended worktree locally.
Verification and result before proceeding
Load the demo route, confirm the shared Lab navigation, 72 synthetic rows and a ready table status. Open the tutorial in a second tab. The next checkpoint assumes the modules and vendored table files load successfully; a visible loading/error message must replace a silent blank page when they do not.
Accept choices, not arbitrary CSS.
Objective and prerequisites
Define the complete preference shape before wiring a control. Read model.mjs, studio.css and the shared runtime. This implementation owns theme, typography, scale, spacing, density, navigation, corners, motion and focus. It cannot reach into every third-party or browser control.
Recommended implementation and explained code
export const OPTIONS = {
theme: ['paper', 'dark', 'blue'],
font: ['system', 'serif', 'mono'],
// ...the seven other enumerated settings in model.mjs
};
// validateAppearance requires exactly the known keys and values.
applyAppearance(appearance);The shared runtime sets named data-* attributes on the root element. CSS maps those attributes to authored values. A font choice is an enum, never a user-provided font-family, URL or style rule. The Studio's --st-* variables pair background, text, border and focus colors. Form controls inherit those pairs. Compact density changes cell padding; it does not shrink every touch target.
html[data-theme=dark] {
--st-bg:#151914; --st-surface:#20261d;
--st-ink:#f8f7ee; --st-muted:#c3c9bb;
}
html[data-density=compact] { --st-cell:.4rem .7rem; }Practical alternatives, pros and cons
CSS custom property values directly from a JSON file are flexible, but require a much broader value grammar, contrast validation and URL checks. Per-component theme objects give strong encapsulation, but can drift across routes. The enum-to-authored-CSS mapping keeps the import boundary small while letting the shared runtime apply the same contract elsewhere.
Common pitfalls
A large settings panel does not establish coverage. Inventory visible states as well: hover, focus, validation, loading, empty data and overlays. Avoid fonts loaded from arbitrary imports and color controls that permit unreadable combinations. A preset is a starting point, not a special unvalidated path.
Verification and result before proceeding
Apply each preset, then change each of the nine controls. Confirm the preview, form, table and modal all follow the selected tokens. Run the model tests: every shipped preset must pass the exact appearance validator; unknown keys and CSS-looking values must fail. Proceed with one schema used by every control.
Save preferences with a lifetime and a fallback.
Objective and prerequisites
Make reloads useful while keeping the browser usable when storage is denied. Read the shared storage.mjs and MDN's storage availability guidance. Having a localStorage property does not establish that writes will succeed.
Recommended implementation and explained code
const appearanceStore = getAppearanceStore();
appearanceStore.set(nextAppearance);
// The shared key is zyel.lab:v2:global:appearance.
// It holds an envelope with schemaVersion, savedAt, expiresAt and data.The singleton matters: two independent stores with the same key can hold different in-memory values. The shell owns the appearance store, and the Studio uses that same handle. The adapter bounds depth, key count and size; validates the data; migrates its known v1 key; expires saved preferences after 30 days; probes writes; and uses memory when access or quota fails. Route/table identity keeps table settings separate. A storage event updates compatible tabs on the same origin. This is local to a browser profile and device.
Undo keeps at most 30 appearance snapshots in page memory. Reset appearance intentionally affects the shared new-demo appearance. Reset this table is separate and changes only this Studio's table arrangement. Neither stores form content or sends anything to a service.
Practical alternatives, pros and cons
sessionStorage limits the persistence window but does not survive closing the session. IndexedDB handles larger structured datasets, but is unnecessary for a tiny preference contract. Account synchronization can follow later with explicit authorization, retention and conflict handling; it is not implied by local persistence.
Common pitfalls
Do not save raw API responses, tickets, articles, credentials, tokens or AI conversations under a “preferences” label. Even table search terms are stored configuration: use synthetic terms here. Catch reads and writes, and never show “saved” when the adapter reports memory mode. A private browsing context may discard saved state when closed.
Verification and result before proceeding
Choose a dark preset, reload and confirm it returns. Deny storage in a browser test and confirm controls still work with a memory-only message. Populate a known v1 envelope and confirm migration, then test an expired and malformed envelope. The shared foundation tests cover these storage cases; the Studio's browser test covers controls while storage throws. Continue only when persistence failure is visible and nonfatal.
Give the table one DOM owner.
Objective and prerequisites
Preserve ascending, descending and multi-column sort, search, per-column filters, page length, page, visibility and column order. The shared adapter uses pinned DataTables and ColReorder files. Consult the shared dependency record for the exact vendored versions; this Studio was integrated with DataTables 3.1.4 and ColReorder 3.1.2.
Recommended implementation and explained code
table = await createTable({
element: document.querySelector('#workspace-dealers'),
tableId: 'workspace-studio-dealers',
columns: COLUMNS.map(key => ({ key, title: columnLabels[key] })),
data: dealers
});
table.api.column('region:name').search('South', false, true).draw();DataTables owns the body and stable column names. The Studio uses the API rather than replacing its cells. A framework can wrap the component, but it must not also render those same rows with v-for; see the official Vue integration guide. Values are displayed as text. A separate custom controller exports sort rules by stable names, so a reordered numeric position cannot silently change the meaning of a saved sort.
DataTables normally uses a 7,200-second validity window. The shared adapter intentionally selects 30 days and custom save/load callbacks to match the Lab preference envelope, rather than relying on that default. Read stateSave, stateDuration and stateSaveCallback. ColReorder supplies supported header dragging and order restoration; Move left/right buttons expose the same action to keyboard users.
Practical alternatives, pros and cons
A native table is small and accessible, but requires a separate tested controller for sorting, paging and state. A server-side grid supports datasets larger than this fixture, but needs validated query parameters, authorization and pagination. The shared client adapter suits 72 synthetic rows. This prototype does not establish production service limits or query performance.
Common pitfalls
Do not allow all columns to disappear, persist unbounded regex search inputs, save numeric column positions without identity, or let a framework and DataTables fight over the same DOM. A custom import must restore filters before clamping the requested page to the available filtered pages.
Verification and result before proceeding
Sort region descending then name ascending, hide revenue, move region to the first position, choose 25 rows and change page. Reload and inspect the state evidence. Export and restore the same arrangement. Filter to zero records and recover. Continue with a readable empty state, preserved named-column sort semantics and a keyboard route to reorder.
Validate the entire document before changing anything.
Objective and prerequisites
Let a user inspect and transfer settings without making a JSON upload an execution channel. The import boundary is 16 KiB of UTF-8, one known format and scope, one supported schema, nine appearance enums and a bounded table contract.
Recommended implementation and explained code
const incoming = parseImport(raw); // no mutation yet
const previous = { appearance: clone(appearance), table: readTable() };
applyTable(incoming.table); // fallible adapter first
setAppearance(incoming.appearance);
// On failure, restore the previous table and appearance.makeExport() returns format, schemaVersion: 2, scope, appearance and table. parseImport() rejects extra envelope keys, arbitrary CSS, prototype-looking keys, unknown columns, duplicate sort columns, unsupported directions, control characters, overlong searches, invalid lengths and an all-hidden table. It migrates only the known v1 contract that lacked the focus field. Unknown future versions fail closed.
The UI disables transfer while the table is unavailable. It performs all validation before mutation, runs table API changes before appearance, and attempts rollback on an adapter exception. The browser has no multi-resource transaction: rollback is best effort, and a visible recovery instruction replaces a false success message if restoration fails. File reading follows an explicit user selection; nothing reads the clipboard silently.
Practical alternatives, pros and cons
Importing the full DataTables state is convenient, but also imports extension-specific properties and search behavior that this UI does not intend to support. A JSON Schema library can be valuable for many contracts, but introduces a dependency here. The small hand-written validator is testable and makes the accepted values visible in one file.
Common pitfalls
JSON.parse() checks syntax, not trust. Do not spread imported objects into options, use imported HTML in a template, accept external CSS URLs, or overwrite another route's table key. Check file size before reading and byte size before parsing; characters and UTF-8 bytes differ.
Verification and result before proceeding
Export, inspect, change a known value, import and verify it. Attempt invalid JSON, an extra html field, a CSS URL, an all-hidden state and a file above 16 KiB. Every rejected import must leave the previous appearance and table state intact and explain the problem without rendering untrusted markup. Run the version migration test before continuing.
Make every setting usable where it lands.
Objective and prerequisites
Exercise the appearance contract across real controls and states. Use a keyboard, small viewport, reduced-motion preference and forced-colors mode. Automated checks cannot replace this inspection.
Recommended implementation and explained code
document.querySelector('#open-dialog').addEventListener('click', () => {
document.querySelector('#review-dialog').showModal();
});
@media (prefers-reduced-motion: reduce) {
.studio-main * { animation:none!important; transition:none!important; }
}Native labels and selects provide a dependable settings path. The modal uses a named native dialog, an autofocus return action, browser focus containment and Escape handling. Status changes use role="status". Table reordering has buttons in addition to dragging. The OS reduced-motion preference always applies; the user can reduce motion further, but cannot use this setting to override the OS preference with animation. See MDN's reduced-motion reference.
The synthetic form uses native required validation only after submit; values never enter preference storage. Empty-state preview temporarily swaps the rows without deleting data. Failed table loading leaves appearance controls available and disables incomplete transfer. Responsive CSS collapses the controls and preview before the layout becomes cramped. At narrow widths, the side navigation becomes a wrapping top row.
Practical alternatives, pros and cons
A custom overlay offers total styling control, but requires focus trapping, inert background handling and keyboard behavior. Native dialog reduces that code. A free-form color picker offers more expression, but cannot guarantee the authored contrast pairings. Token choices were selected for predictable accessible states.
Common pitfalls
Do not remove outlines, hide the focused column control during a redraw, use color as the only state signal or make compact density reduce touch targets below a usable size. Native browser controls and third-party internals can vary; the inventory must state that boundary rather than promise complete control.
Verification and result before proceeding
Tab from the skip link through each control. Move a column using buttons and confirm focus remains on that action. Open and close the modal using only the keyboard. Check all three themes, 390-pixel width and large text for readable content and no page-level overflow. Confirm the empty, validation and loading/error states tell the user how to recover.
Collect evidence that matches the claim.
Objective and prerequisites
Verify the boundary separately from the rendered behavior. Use Node.js 24.19.0 for the pure model tests and Playwright/Chromium for the local browser checks. Browser tooling is a development dependency; it is not delivered to the public route.
Recommended implementation and explained code
node --test lab/workspace-studio/tests/model.test.mjs
node lab/workspace-studio/tests/benchmark.mjs
node lab/workspace-studio/tests/browser.mjsThe nine model tests cover all shipped presets, strict keys, CSS rejection, prototype-looking input, round trips, malformed/oversized files, known v1 migration, scope/version rejection, all table fields and bounded undo history. The browser test covers persistence, multi-sort, named-column export, invalid-import nonmutation, modal behavior, narrow layout and denied storage. Read README.md for exact run prerequisites and current execution results; an unavailable browser dependency is a test blocker, not a passing result.
The local CPU benchmark warms up 1,000 parses, runs seven batches of 10,000 parses and reports the median batch mean. On this Windows x64 machine with Node 24.19.0 and an AMD Ryzen 9 9900X3D, the 565-byte synthetic document measured 0.00542 ms per parse (batch means 0.00525–0.00677 ms). This is a local validation-cost observation. It excludes browser rendering, storage, I/O and DataTables, and does not support any production-load or latency claim.
Practical alternatives, pros and cons
A single screenshot is fast to inspect but cannot establish persistence or failed-import behavior. A full accessibility audit provides broader assurance but takes more scope and specialist review. The chosen small model and interaction tests target the demonstrated failure boundaries; an independent rendered review adds the human check.
Common pitfalls
Do not report “all tests pass” when only syntax was checked, compare a CPU-only measurement to browser load time, or treat a synthetic 72-row dataset as a production traffic benchmark. Repeat measurements after substantive model changes and identify the environment.
Verification and result before proceeding
Retain command output, browser screenshots and the independent review findings in the release evidence. Resolve actionable defects and rerun affected checks. A prepared implementation and a reviewed implementation are different statuses; neither means it is live. The release checkpoint requires the actual evidence record.
Release the smallest reviewed surface.
Objective and prerequisites
Prepare an additive release to the existing Zyel host only after integration review and the required publication approval. The owner checkout contains concurrent work, so use the isolated integration branch and a draft PR. This tutorial grants no deployment authority.
Recommended implementation and explained code
git diff --stat origin/main...HEAD
# Scope: new Lab routes + shared foundation + additive Lab links.
# Preserve lab/celebration.js, existing effects and admin/CRM work.
# After an authorized deployment: read back each actual live URL.Ship the versioned shared assets and the Studio route together. First stage to an isolated preview, exercise saved-state migration and all demo links, and verify lazy loading keeps heavy map/AI dependencies off unrelated routes. Keep the prior reviewed release available. Rollback restores that previous file set and Lab links; do not run broad deletes or reset the owner working tree. The preference namespace and schema version must remain compatible with the rolled-back reader, or the reader must safely ignore newer envelopes.
On a future schema change, add a bounded migration and tests before increasing the version. Recheck official DataTables compatibility when changing ColReorder, preserve vendor license notices and retain exact dependency versions and source hashes in the shared record. Check current browser behavior for modal, storage and native controls. Retire stale preference fields deliberately instead of accepting them forever.
Practical alternatives, pros and cons
A separate static host is convenient for previews, but can lose the existing brand, route and hosting context. A feature flag helps incremental release, but needs a tested flag lifecycle. This change remains additive in the existing Lab and is prepared for review without any new hosting or authorization infrastructure.
Common pitfalls
Do not publish by copying the owner's uncommitted checkout, treat a successful upload as a successful live release, or include a WordPress compliance claim without a bounded adapter tested in an approved isolated WordPress environment. This Studio is not fulfillment of an entire procurement scope.
Verification and result before proceeding
Before release, verify the draft PR scope, at least five substantive critique → fix → re-verification cycles with a distinct independent critic, exact test evidence and rollback path. Builder checks alone do not satisfy that policy. After an authorized publication, read back /lab/workspace-studio/ and /lab/workspace-studio/tutorial.html, inspect browser errors and verify Celebration still works. Until those live checks happen, describe this work as prepared or reviewed, not deployed.
Rehearse the result without replacing the workspace.
Objective and prerequisites
Prepare an original computer tutorial over actual browser behavior. Complete the preceding model, table, import and recovery checks first. Use a clean synthetic browser profile, local preview, readable text and the numbered SAY / SHOW / EXPECTED script. Chason Arthur is Project Lead; this attribution does not imply sole authorship of every line.
Recommended implementation and explained code
Let ordinary initialization finish, then require Prepare and Confirm. The checkpoint starts after normal migration, expiry/corruption handling and the storage write probe. Do not snapshot every storage key or silently reset from a scene URL. The checkpoint keeps bounded in-page form/JSON/history values; the saved preference store remains the source of truth.
// Excerpts from studio.mjs; these run inside the route.
if (!temporary) appearanceStore.set(appearance);
renderAppearance(); // applyAppearance changes authored data-* tokens in the DOM
await createTable({
element, tableId: 'workspace-studio-dealers', columns, data: dealers,
persistState: false, initialState: null
});Destroy the prior DataTables instance before building the temporary one. All table controls then save to its page-memory handle, including sorts, search, filters, page, visibility/order and resets. Presets/import/undo use the same temporary appearance path. Repeated resets retain the first form checkpoint. Undo is explicitly appearance-only.
// On restore, read the current shared handles. Never write an old envelope back.
await mountTable({persistState: true});
appearance = appearanceStore.get();
// An explicit file selection may finish after a reset/restore or newer selection.
const revision = sessionRevision, selection = ++fileRevision;
const raw = await file.text();
if (revision !== sessionRevision || selection !== fileRevision) return;
await importDocument(raw);The actual handler also checks the file size before reading and shows a stale-selection notice. Reset, restore, empty-mode transitions and navigation invalidate pending file reads. An interrupted reload resumes ordinary saved settings. Storage-denied/quota mode preserves the current page's fallback values but cannot promise persistence across reload.
Empty-state rehearsal has its own temporary table: copy the current validated DataTables state into initialState, then clear only the temporary rows. Restoring dealer data recreates the persistent table from its latest handle. This avoids saving a clamped first page and lets a newer change from another tab win. The rehearsal deletes no records and performs no service call.
Optional recording tools load through dynamic import only after an explicit Show recording tools action. A failed optional plan cannot stop core appearance/table initialization. Loading has a live status, an eight-second bound, at most three user-triggered attempts and static guide/script recovery. See MDN dynamic import and W3C status messages. The recording guide intentionally loads its own tools; ordinary demo use does not.
Practical alternatives, pros and cons
A disposable browser profile is useful for clean captures, but alone does not make the application reset safe. Copying all localStorage entries and writing them back is easy to implement but can overwrite newer cross-tab work or unrelated routes. A compare-and-swap protocol can support a deliberate persistent transaction, but needs conflict/version handling this rehearsal does not require. A pause-persistence feature on one grid avoids reconstruction but expands the shared adapter API and must cover every save event. This implementation chooses a separate nonpersisting instance and current-store restoration because the boundary is visible and testable.
Persisting the recording session in sessionStorage could recover an interrupted rehearsal, but creates another schema/lifetime and risks surprising a returning viewer. Page memory was chosen: reload exits the rehearsal. The normal preferences remain useful on the same browser/device; no account sync is implied.
Common pitfalls
Do not call getAppearanceStore().set() for temporary presets, reset the original checkpoint on every scene, accept arbitrary CSS/HTML, clear storage for a video, or allow an old file read to apply after restoration. Scene/chapter links navigate only. Presentation mode changes chrome and exits with Escape; it is not a reset. The future-video field validates a link locally without loading an iframe or thumbnail. A draft script is not the transcript of an existing video.
Verification and result before proceeding
# With the restricted preview already running and Playwright available:
node lab/workspace-studio/tests/recording.mjs
node lab/workspace-studio/tests/story.mjs
node lab/workspace-studio/tests/empty-state.mjs
node lab/workspace-studio/tests/loading.mjsAfter normal boot, retain exact valid saved envelopes, confirm a walkthrough and exercise every control. Assert zero post-checkpoint writes and equality after repeated reset, rejected import and restore. Change settings in a second synthetic tab; newer values must win. Test interrupted reload, denied/quota fallback and a delayed File.text finishing after restore. At 320px with large monospace text and airy spacing, inspect control rectangles and keyboard focus, not merely document overflow.
Before recording, follow each of the nine quick cuts and verify its expected result. Retain separate actual-browser 1920×1080 before/after, rejection and recovery frames with shot ID, exact source revision and SHA256. Keep diagnostic/intermediate files out of the final frame list. Proceed with a reproducible, reversible rehearsal; publication, a real video attachment and production-provider operation remain separate actions.
Sources and tested boundaries
Official documentation checked during implementation on October 25, 2026. Exact shared dependency versions and hashes belong to the shared dependency evidence, not a guessed “latest” label.
Appearance preferences apply to the four demos in this release. The separately authored Answer-first Gateway retains its standalone local presentation.