FE-1228: Unify Panda CSS generation for HASH and Petrinaut - #9071
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
|
PR SummaryMedium Risk Overview Theme extensions move from inline The temporary PostCSS boost-panda-specificity workaround (#9069) is removed; PostCSS is plain Panda again. The embed still imports Reviewed by Cursor Bugbot for commit ac66ab2. Bugbot is set up for automated code reviews on this repo. Configure here. |
Petrinaut now ships Panda static-analysis build info (@hashintel/petrinaut/panda.buildinfo.json) and a shared theme preset (@hashintel/petrinaut/panda-preset). The HASH frontend compiles that build info through its own Panda pipeline, so every Panda rule loaded in the Petrinaut embed lives in one cascade-layer graph with one polyfilled specificity contract, instead of two independently generated bundles whose synthetic :not(#\#) boost counts could differ and let host utilities override Petrinaut's conditional styles. Petrinaut's keyframes are namespaced (petrinautFadeIn, petrinautExpand, ...) because Panda deep-merges same-name keyframes across presets, which would otherwise mutate the host's animations.
The unified Panda pipeline supersedes the FE-1229 stopgap (#9069): the host now compiles Petrinaut's styles through its own Panda config, so every rule lives in one cascade-layer graph and no synthetic specificity padding is needed. Restore the plain Panda PostCSS config and drop the boost plugin, its tests, its dependencies, and its now-superseded changeset.
2a6c9b6 to
cd765cd
Compare
Requested by Alex Leon · Slack thread
🌟 What is the purpose of this PR?
Before: the Petrinaut embed page loads two independently generated, layer-polyfilled Panda CSS bundles — hash-frontend's
ds-components-styles.gen.cssand Petrinaut's prebuiltdist/main.css. The polyfill replaces@layerwith synthetic:not(#\#)specificity boosts, and because the bundles have different sets of non-empty layers, HASH utilities carry 5 boosts while Petrinaut's carry 4. Shared atomic class names (both pipelines use the same ds-components build info and@hashintel/ds-helpersimport map) then resolve by boost count instead of component intent: e.g. HASH's.grid-tr_0fr/.op_0silently overrode Petrinaut's[data-expanded='true']conditional styles, leaving the optimization min/max search-space inputs collapsed and invisible in the HASH embed, and breaking the drawer's zero-padding override. Stylesheet order can't fix it, and the stopgap in #9069 hard-codes boost counts (raise 4→6), which breaks silently the next time either bundle's layer structure changes.After: Petrinaut ships its Panda static-analysis results (
@hashintel/petrinaut/panda.buildinfo.json) and its theme extension as a preset (@hashintel/petrinaut/panda-preset), and hash-frontend compiles them through its own Panda pipeline, alongside ds-components. Every Panda rule that matters in the embed now exists in one stylesheet, in one cascade-layer graph, with one specificity contract — Petrinaut's conditional styles win over base utilities by natural specificity, exactly as on the standalone Petrinaut site. No hard-coded boost counts anywhere.This is solution 1 from FE-1228 ("Generate one consumer-owned Panda stylesheet"), which the ticket marks as the preferred direction.
🔗 Related links
postcss.config.mjs/scripts/boost-panda-specificity.mjs(+ its test, deps, and changeset), restoring the plain Pandapostcss.config.cjs(the acceptance criterion "the temporary Petrinaut PostCSS boost can be removed" is thereby fulfilled).🔍 What does this change?
How it works
@hashintel/petrinautnow builds two extra artifacts (mirroring how ds-components already ships build info):dist/panda.buildinfo.jsonviapanda shipwith a dedicatedpanda.ship.config.ts(runtimesrc/**only — no stories/tests/Storybook, no re-export of ds-components' build info, which hosts already include directly).dist/panda-preset.js— the theme extension (keyframes +fonts.mono) moved out ofpanda.config.shared.tsintosrc/panda-preset.ts, consumed by both Petrinaut's own config and the host, so the two pipelines compile against an identical theme contract. It is a plain object (notdefinePreset) so it adds no runtime/type dependency on@pandacss/dev; its shape isPreset-checked inpanda.config.shared.ts.fadeIn→petrinautFadeIn,expand→petrinautExpand, etc., referenced via Panda's"[name]"escape hatch, matching the existing"[optimizationGlow]"usage). This is load-bearing: Panda deep-merges same-name keyframes across presets (preset order does not help), so un-namespaced keyframes would have mutated hash-frontend'sfadeIn/expand/collapseanimations app-wide. A new unit test inpanda.config.shared.test.tsfails if a Petrinaut keyframe ever collides with a ds-components preset keyframe again.apps/hash-frontend/panda.config.tsaddsrequire.resolve("@hashintel/petrinaut/panda.buildinfo.json")toincludeand the Petrinaut preset topresets(listed first so ds-components/panda definitions win the only remaining scalar conflict,fonts.mono, keeping existing HASH surfaces untouched).dist/main.css— it still supplies what the host pipeline intentionally does not: vendor CSS (xyflow, uplot, Monaco, fonts) and the.petrinaut-root-scoped preflight/token variables/global styles. Its Panda utilities are now harmlessly shadowed dead weight.Why this stays correct (no hidden count coupling)
The unified stylesheet's content is a superset of Petrinaut's runtime Panda output, so any layer that is non-empty in Petrinaut's bundle is non-empty in the unified one — the polyfill therefore structurally guarantees the unified bundle's boost count is ≥ Petrinaut's, whatever Panda emits in future. Petrinaut's rules all exist at the top boost level with Panda's canonical intra-layer ordering, which is precisely the single-bundle semantics of the standalone Petrinaut site.
Alternatives considered (from the ticket)
@layercontract (solution 2): still blocked by hash-frontend's unlayered global resets — the reason the polyfill exists.Button,VersionPicker) and_appglobal CSS can't be conditionally dropped per page in the pages router.Pre-Merge Checklist 🚀
🚢 Has this modified a publishable library?
This PR:
📜 Does this require a change to the docs?
The changes in this PR:
🕸️ Does this require a change to the Turbo Graph?
The changes in this PR:
@hashintel/petrinaut'sbuildwas split intobuild:lib+build:buildinfo, but thebuildtask and itsdist/**outputs are unchanged; hash-frontendcodegenalready depends on^build.)ds-components-styles.gen.css(+~98 KB raw, before compression) on every page, not just the embed. This is inherent to the ticket's preferred single-pipeline direction; a page-scoped secondcssgenwas prototyped but Next.js's pages router only allows app-local global CSS in_app.fadeIn/expand(extra translate/fade). They now use the genuine ds-components definitions in both the standalone site and the embed — consistent everywhere, and closer to ds intent.@blockprotocol/type-system, so a full cleanyarn lint:tscfor hash-frontend could not be reproduced locally — the 1,377 errors observed all pre-exist in@local/hash-isomorphic-utilssystem-types and none reference files touched here (panda.config.tsis also excluded from the app tsconfig). CI should confirm.🐾 Next steps
Revert FE-1229: Temporarily fix Petrinaut Panda specificity #9069's PostCSS boost if it merges before/alongside this (see Related links).Done — FE-1229: Temporarily fix Petrinaut Panda specificity #9069 merged first and is reverted by this PR's second commit.dist/main.csslater so embed consumers stop double-loading Petrinaut's (now shadowed) Panda utilities.🛡 What tests cover this?
libs/@hashintel/petrinaut/panda.config.shared.test.ts— new: asserts the shared preset is wired into the generated config, and that no Petrinaut keyframe name collides with the ds-components preset (the deep-merge hazard). Full petrinaut suite: 25 files / 172 tests pass.[data-expanded='true']]:grid-tr_[1fr](4 boosts) lost to HASH's.grid-tr_0fr(5 boosts)..grid-tr_0fr— Petrinaut wins as intended..petrinaut-root-scoped preflight/token rules intentionally still served bydist/main.css; 0 rules at lower boost, 0 declaration mismatches, 0 keyframes missing or differing.--fonts-monoincluded) — zero impact on existing HASH surfaces.dist/main.cssis byte-identical tomainoutside@keyframesdefinitions, modulo the intended keyframe renames.dist/main.cssutilities are back at 4 boosts (2,813 rules; zero 6-boost padding — the one remaining 6-boost run is Panda's own.petrinaut-root-scoped preflight[hidden]rule), and the regenerated unified stylesheet has all utilities at 5 boosts (3,124 rules) including both[data-expanded='true']conditionals; the only diff vs. the pre-rebase generation is utilities added/removed by Petrinaut UI work that landed onmain(FE-1233/FE-1236).lint:tsc(clean), petrinautlint:eslint(clean), petrinauttest:unit(25 files / 172 tests pass), petrinautbuild(clean),yarn constraints(clean),oxfmt --checkon touched files (clean).❓ How to test this?
turbo run build --filter @hashintel/petrinaut, then inapps/hash-frontendrunyarn panda cssgen --outfile ./src/pages/ds-components-styles.gen.css.grid-template-rows: 1fr,opacity: 1in computed styles).📹 Demo
No visual change outside the bug fix; before/after evidence is in the test section above.