Repository navigation
Add interactive playground for documented UI components - #168
Merged
Merged
Conversation
The scope authority is the component surface documented at fleetbase.io/docs/ui, not the addon's
full public export list: 275 components are exported, 63 are documented, and only those 63 get a
playground page. allowlist.js records that surface — resolution path, display name, category and
documentation URL — and registry.js is built from it.
Argument surfaces were read off the current addon templates and classes rather than the
documentation prose, so a control that would silently do nothing does not get written. Where the
two disagree the implementation wins; the discrepancies are recorded in PLAYGROUND.md.
Alongside it:
- controls.js control types, coercion and validation. An invalid value falls back to the
documented default rather than taking the preview down.
- state-codec.js one encoded query parameter carrying control state. Decoding validates against
the control schema; unknown keys, wrong types and malformed encoding all
degrade to defaults with a non-fatal warning. Functions, services, records,
Files and non-plain objects are never serialized.
- event-sanitizer.js turns callback arguments into something safe to display: DOM events reduced to
a few fields, records to modelName:id, Files to name/type/size, cycles and
depth bounded. It never throws.
- fixtures/ deterministic fixtures. Nothing reads the clock or the network.
- host-stubs.js host-application dependencies the console layout components need, registered
from the adapters so the existing integration tests are unaffected.
Uses the existing dummy app rather than a second workspace, so the playground consumes the real components through normal Ember resolution with the real addon styles. No copies, no Storybook. Routes: / redirects to the catalog, /components is a searchable categorized catalog, /components/:slug is the full interactive page and /embed/:slug is the minimal iframe view. Catalog and detail are siblings rather than parent/child so each owns its own model and query parameters. Anything else — including undocumented public components — lands on the playground's deliberate not-found page instead of being exposed automatically. Both routes render the same host component, so the embed showing the same state as the full page is true by construction rather than by duplication. Example adapters bind every argument to the real component explicitly: if an adapter stops forwarding a value, the test for that control fails. The event log appends after render rather than during it — layout/mobile-navbar invokes @onsetup from its constructor, and assigning tracked state mid-render is a backtracking re-render error. The embed reports its height to the parent through a one-way postMessage. It installs no incoming message handler; height is the only thing ever sent.
Uses the existing QUnit, Ember Test Helpers, Testem and headless Chrome setup. No second browser-testing framework: setupApplicationTest already drives real routes in real Chrome, and another runner would mean a second CI lane and a second set of flakes. These assert the playground's wiring, not the components' contracts — tests/integration/components/ remains the source of truth for component behaviour and is untouched. The Button suite does not re-test that a disabled button refuses clicks; it tests that the disabled control reaches Button. - allowlist/registry: scope completeness in both directions, slug uniqueness, resolvable components and adapters, valid control metadata, and the ScheduleCalendar mismatch. - state codec: defaults, round trips, unknown keys, wrong types, malformed encoding, and the values that must never be serialized. - controls and event sanitization, including cyclic and throwing values. - catalog, component page, embed behaviour and resize messaging. - route smoke coverage walking the registry: all 63 component routes and all 63 embed routes must settle, render their marker and title, resolve their adapter and raise nothing.
build:playground wraps ember build rather than replacing it, so the package build is untouched. Pages needs hash routing (it cannot rewrite deep links onto index.html) and assets under a sub-path; both are opt-in through the environment, so ember build, ember serve and the test suite are unaffected. The base path is a flag, not a constant, so a custom domain later is a CI argument rather than a code change. CI: the branch filters now include test/coverage-campaign. Without that, pull requests targeting the campaign branch ran no checks at all. The build job also builds the Pages artifact and asserts index.html, .nojekyll and assets/ exist, that assets resolve under the sub-path, and that no test output or coverage artefact reaches it. The Pages workflow never deploys a pull request head — a PR from a fork would otherwise be able to publish arbitrary content to the project's site. It triggers on pushes to test/coverage-campaign and on workflow_dispatch. Moving the trigger to main after the campaign merges, and setting the Pages source to GitHub Actions, are recorded in PLAYGROUND.md as post-merge steps.
PLAYGROUND.md covers the documentation-driven scope and how to update it, the architecture, the registry schema, adding an example, controls and presets, fixtures and services, the event log, state URLs, iframe integration and resize messaging, the Pages build and its post-merge settings, and the testing strategy. It also records the discrepancies found while reconciling the documentation against the implementation: /docs/ui/scheduling/event-calendar still names ScheduleCalendar, which was deleted from this addon as dead code in e6a3903 and is deliberately not restored, and InputGroup renders its label from @name — @labelText is not read by the component at all.
Navigating straight from one component page to another keeps the same route and the same position in the template, so Ember reuses the host component and its constructor does not run again. The new component was rendering with the previous one's control values: every key it does not share arrived as undefined, so Table's @selectable, @Sortable and @page were silently dropped after visiting Button. Found by driving the built GitHub Pages artifact, not by the suite — every application test boots a fresh application, so only a direct page-to-page navigation inside a single test reaches it. Three tests now cover that path; two of them fail without this fix, and the third was rewritten to navigate directly after it turned out that going via the catalog tears the host down and cannot reproduce the bug. Also: put scripts/build-playground.js under the repo's node ESLint config, drop its unused shebang, and add the four screenshots referenced from the pull request.
Reported while trying the playground: the components looked unstyled, and the page would not
scroll past the fold. Both were host-application gaps rather than anything wrong with a component.
Element normalisation. addon/styles/addon.css deliberately ships no `@tailwind base` — an addon
emitting preflight would clobber every application consuming it — and its `@apply`-generated rules
assume the elements underneath are already normalised. Without that layer a <button> keeps the user
agent's `buttontext` colour, pure black, instead of inheriting, and form controls keep UA fonts.
Emitting `@tailwind base` here fixes the previews and breaks the addon: preflight lands after the
addon stylesheet and wins on equal specificity, so `button { cursor: pointer }` silently overrides
the console's deliberate `* { cursor: default }` — which navigator-test.js asserts, and which duly
went red. A real console loads preflight before the addon, and that ordering cannot be reproduced
from inside the dummy app. So only the normalisation the previews need is applied, scoped to
.pg-host, with the ancestor inside `:where()` so the rules weigh exactly what preflight's own
element selectors weigh and .btn-sm keeps winning. Nothing outside the playground is affected.
Viewport lock. The shipped CSS contains `body, html { height: 100vh; overflow: hidden }`, which is
right for the console — it fills the viewport and scrolls its panes independently — but the
playground is an ordinary document. Inheriting it left the catalog measuring 3080px inside a 720px
non-scrolling viewport, with everything below unreachable. The dummy app loads after the addon
stylesheet, so restoring `height: auto` and `overflow-y: auto` at equal specificity is enough.
Also excludes playground-dist/ from the lint ignores, alongside dist/ and coverage/. Building the
Pages artifact and then linting put 4565 vendor-CSS errors through stylelint; CI never saw it
because lint and build run in separate jobs.
Covered by tests/acceptance/playground/styling-test.js, which asserts the computed styles of a
previewed Button and that nothing between the catalog and the document clips it. Screenshots
regenerated.
Implements 'Playground Redesign.dc.html' against the Fleetbase brand ramps in tailwind.config.js — sky for accent, night/nightsky for dark. No invented colours, no addon or app source touched. Catalog: the page title and lede are gone, as the design has none; the filter bar is a tinted band with its own rule; the category rail is divided from the content by a full-height border; labels are uppercase and muted; cards carry the component name, its slug in mono, and the description. The design's cmd-K affordance is wired rather than faked — the listener lives and dies with the catalog, and the addon's own cmd-K binding is never rendered on that route. Component page: title row with the component chip and Open embed, a segmented source/tests/docs meta strip, a preview hero on a dotted grid, and an inspector whose boolean rows put the label and control on one line. Three defects surfaced while building it, each now covered by a test that fails without the fix: - Dark mode never actually themed the previews. 1094 of the addon's rules are scoped to `body[data-theme='dark']` specifically, and the attribute was only being set on the playground's own container — so dark mode gave near-black component text on a dark surface. A modifier now mirrors the theme onto <body>, which is what a real console does. - The offered embed URL was built by concatenating location.pathname with '#/embed/…', which is only valid in the hash-routed Pages build. In development it produced '/components/table#/embed/table'. It is built through the router now, so it is right under both location types. - Playground typography was set on <body>, so it reached #ember-testing and every component integration test with it. Dropping the base to 13px flipped a drag-reorder decision in query-builder/group-by-test.js, which derives clientY from element geometry. Type now lives on the playground's own containers and the addon keeps its 16px baseline. Table previews additionally opt into @useTfootPagination: the default pagination bar is fixed to the viewport and landed outside the preview panel, which is also what the design depicts.
roncodes
force-pushed
the
feature/component-playground
branch
from
August 28, 2026 05:46
5207c7e to
acdd5ad
Compare
Closed
This was referenced Sep 16, 2026
Closed
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds an interactive playground for the component surface documented at
fleetbase.io/docs/ui, built from this addon's existing Ember dummy
application.
Why
tests/dummyThe dummy app is already a real Ember application that consumes this addon through normal
resolution, with the real addon styles, the real services and the real build. Adding a second
workspace (or Storybook) would mean a second dependency graph, a second build to keep in step, and
previews that drift from what consumers actually render. Everything here is playground-only code
under
tests/dummy; nothing was added toaddon/, because none of it is useful to consumers.addon/andapp/are byte-identical to the base of this PR. No production source changed.Why scope follows the documentation, not the export list
The addon exports 275 public components.
/docs/uidocuments 63. Only those 63 get a page.tests/dummy/app/playground/allowlist.jsis the scope authority — resolution path, display name,category and documentation URL per entry. An existing-but-undocumented public component
(
chat-container,metadata-editor,aside-item-scroller, …) resolves to the playground'sdeliberate not-found page rather than being exposed automatically. Two tests enforce this in both
directions, and neither compares the registry against
app/components; that comparison is exactlywhat the playground exists to avoid.
63 of 63 documented components are represented. The live documentation navigation was audited
against the reviewed list and matches it — including the grouped pages (the six
Layout::*scaffolding components share
/layout/overview, the four resource layouts share/layout/resource-tabular, the eight modal layouts share/modals/modal-layouts).ScheduleCalendar: the documentation is stale
/docs/ui/scheduling/event-calendaris titled "EventCalendar / ScheduleCalendar", butScheduleCalendarandScheduleItemCardwere deleted from this addon as confirmed dead code ine6a3903during PR #143.EventCalendaris included.ScheduleCalendarandScheduleItemCardare not restored, and no replacement was invented.The mismatch is recorded in the allowlist as
REMOVED_FROM_ADDON_STILL_IN_DOCS, asserted by a testso it cannot be quietly forgotten, and written up in PLAYGROUND.md. Recommendation: correct the
documentation separately — that page should describe
EventCalendaronly. Its source is not inthis repository, so it was not touched here.
Other documentation/runtime discrepancies
Registry controls were read off the current templates and classes, not the prose. Where they
disagree the implementation wins, so a control that would silently do nothing never gets written:
InputGrouprenders its label from@name, not@labelText.@labelTextis not read atall (
input-group.hbs:3), and@placeholderfalls back to@name. The existing integrationtests use
@namethroughout. The control is bound to@nameand labelled accordingly.InputGroupexposes no change callback, so the adapter observes the native input eventthrough splattributes.
Layout::Resource::Panelhas no save button unless@saveTaskis passed — deliberate, perDEFECTS.md. The example passes a local no-op task so the button is demonstrable.
Routes and iframe behaviour
//componentsq,categoryin the URL)/components/:slug/embed/:slugProduction uses hash routing, because Pages cannot rewrite deep links:
Both routes render the same host component with the same state and the same adapters, so "the
embed shows the same thing" is true by construction rather than by duplication. The embed reports
its height to the parent through a one-way
postMessage(
fleetbase:ember-ui-playground:resize); it installs no incoming message handler, and height isthe only thing ever sent. PLAYGROUND.md documents the iframe markup, accessible title, sandbox
guidance, the resize listener, and the
frame-src https://fleetbase.github.ioCSP the docs sitewill need.
Controls, fixtures, state and events
input shows an accessible message and falls back to the documented default — it never takes the
preview down.
Loading, …), fixture scenarios pick data (Table's Five orders / Empty state).
statequery parameter. Only non-default values are encoded. Decoding validatesagainst the control schema; unknown keys, wrong types and malformed encoding all degrade to
defaults with a non-fatal warning. Functions, services, records,
Files,Dates,Errors andnon-plain objects are never serialized.
records to
modelName:id,Files to name/type/size (never contents), cycles and depth bounded.It never throws.
API or persists anything. Modal layouts are demonstrated through the real
modals-managerservice, which is how the documentation tells consumers to use them.
A bug this found
Navigating straight from one component page to another keeps the same route and template position,
so Ember reuses the host component and its constructor does not run again — the new component
rendered with the previous one's control values, silently dropping
@selectable,@sortableand@pageon Table after visiting Button.The suite could not catch it: every application test boots a fresh application, so only a direct
page-to-page navigation inside one test reaches it. It was found by driving the built Pages
artifact in a real browser. Three tests now cover that path; two fail without the fix, and the
third was rewritten to navigate directly after it turned out that going via the catalog tears the
host down and cannot reproduce it.
What the host application had to supply
Two gaps surfaced while trying the playground for real. Neither was a component defect; both were
things a consuming application normally provides and the dummy app did not.
Element normalisation.
addon/styles/addon.cssdeliberately ships no@tailwind base— anaddon emitting preflight would clobber every application consuming it — and its
@apply-generatedrules assume the elements underneath are already normalised. Without that layer a
<button>keptthe user agent's
buttontextcolour (pure black) instead of inheriting, and form controls kept UAfonts. The components looked unstyled when only the layer beneath them was missing.
Emitting
@tailwind basefrom the dummy app fixes the previews and breaks the addon: preflightlands after the addon stylesheet and wins on equal specificity, so
button { cursor: pointer }silently overrides the console's deliberate
* { cursor: default }— whichlayout/sidebar/navigator-test.jsasserts, and which duly went red when I tried it. A real consoleloads preflight before the addon; that ordering cannot be reproduced from inside the dummy app.
The existing test was not touched. Instead only the normalisation the previews need is applied,
scoped to
.pg-host, with the ancestor inside:where()so the rules weigh exactly whatpreflight's own element selectors weigh (0,0,1) and
.btn-smkeeps winning.The console's viewport lock. The shipped CSS contains
body, html { height: 100vh; overflow: hidden }— right for the console, which fills the viewportand scrolls its panes independently, wrong for an ordinary document. The catalog measured 3080px
inside a 720px non-scrolling viewport, so everything below the fold was unreachable.
tests/dummyloads after the addon stylesheet, so restoring
height: auto/overflow-y: autoat equalspecificity is enough — no
!important, no selector hacks.Both are covered by
tests/acceptance/playground/styling-test.js, which asserts the computedstyles of a previewed Button (border, background, radius, and that it no longer falls back to the
UA button colour) and that nothing between the catalog and the document clips it.
Also excluded
playground-dist/from the lint ignores alongsidedist/andcoverage/: buildingthe Pages artifact and then linting pushed 4565 vendor-CSS errors through stylelint. CI never saw
it because lint and build run in separate jobs.
Testing
Uses the existing QUnit + Ember Test Helpers + Testem + headless Chrome setup.
No Playwright, Puppeteer, Selenium, Cypress or Storybook was added —
setupApplicationTestalready drives real routes in real Chrome, and a second runner would mean a second CI lane, a
second set of flakes, and another thing to keep in step with the coverage lifecycle.
323 playground tests, layered so they assert wiring rather than re-testing component contracts.
tests/integration/components/remains the source of truth for behaviour and is untouched — theButton suite does not re-test that a disabled button refuses clicks; it tests that the
disabledcontrol reaches Button.
adapters, valid control metadata, the ScheduleCalendar mismatch
two store-backed components
render their marker and title, resolve their adapter, produce content and raise nothing
Commands and results
All on Node 22.22.2. A non-default
--test-portwas used throughout because another suite wasrunning on this machine.
pnpm install --frozen-lockfilepnpm run lintpnpm exec ember test --filter=playgroundpnpm exec ember test(full)pnpm run coverage:selftestnode scripts/stamp-coverage-run.js && COVERAGE=true ember testpnpm run coverage:checkpnpm run builddist/, 9.0Mpnpm run build:playgroundplayground-dist/, 9.0M--filter="the view-all link reports the press"Pages artifact inspected:
index.htmland.nojekyllat the root,404.htmlcopied fromindex.html, every asset URL under/ember-ui/,rootURL: /ember-ui/,locationType: hash, andno
tests/,tests.html,testem.js,coverage/,lcov.infoor log files.Inherited failures — this PR introduces none
coverage:checkfails with 267 sites across 96 files inaddon/. This is pre-existing:addon/andapp/are byte-identical to this PR's base —git diff <base> --name-only -- addon/ app/is empty. This PR adds only tests and dummy-app code, which can only increase coverage, so
the base has at least as many failing sites.
addon/),so none of it is gated.
DEFECTS.mdalready catalogues under "Why the remainingcoverage gaps are where they are" — framework-invoked defaults, resolver-provided services,
@trackedinitializers a constructor pre-empts, statements after athrow, guards behindalready-disabled buttons, comparator early-return halves.
The 100% gate is not green, and this PR does not claim it is. The campaign is actively closing
these gaps — it advanced through four commits while this work was in progress, and this branch was
rebased onto each.
The notification-tray timeout from PR #143 reproduces in CI on this PR — and equally on the
campaign branch without any of this code. The
Test with coveragejob fails with:The run aborts at that test, so coverage enforcement is never reached — the same shape of failure
the campaign has been seeing.
Evidence that it is inherited, not introduced here:
(32866406523,
test/coverage-campaign)fails with the identical message on the identical test:
# tests 2177, pass 2176, fail 1.The last six runs on that branch are all failures.
addon/source and does not touchnotification-trayor its test.three times, and two full-suite runs (5513 and 5520 tests, 0 failures) all passed.
fix/testem-disconnect-timeout(#166) already landed on the campaign branch and raisedbrowser_disconnect_timeoutfrom 10s to 120s — the message above shows the higher ceiling nowbeing hit, so that change moved the ceiling without removing the underlying stall. It is a CI-only,
environment-sensitive hang.
It was deliberately not papered over: no global timeout increase, no skip, no weakened
assertion, and no unrelated production change. Diagnosing it needs a CI-reproducible investigation
of what stays pending in that test, which is campaign work rather than playground work.
DEFECTS.md #18 (±1 branch-count nondeterminism) was not investigated and is not claimed fixed —
no fresh differing artifacts were captured to name the responsible file.
GitHub Pages
.github/workflows/playground-pages.ymlusesactions/configure-pages,actions/upload-pages-artifactandactions/deploy-pageswithcontents: read,pages: write,id-token: write, thegithub-pagesenvironment and apagesconcurrency group.Pull request heads are never deployed — a PR from a fork could otherwise publish arbitrary
content to the project's site. It triggers only on pushes to
test/coverage-campaignand onworkflow_dispatch.CI branch filters were widened to include
test/coverage-campaign; without that, PRs targeting thecampaign branch ran no checks at all. The build job also builds the Pages artifact and asserts
its contents.
Required repository settings (not done by this PR)
uploads but has nothing to publish. External repository settings were deliberately not mutated.
mainwhen the campaign merges, and droptest/coverage-campaign.Leaving both would let two branches overwrite the same site unpredictably.
Screenshots
Catalog — 63 of 63 documented components, categorized and searchable:
Button component page — metadata, real component, controls, presets, event log:
Button embed view — preview first, controls below, no catalog chrome:
Table — a complex fixture-backed example, deterministic five-row fixture:
All four were captured from the built
playground-dist/artifact served under/ember-ui/, notfrom a dev server.