Skip to content

FE-1228: Unify Panda CSS generation for HASH and Petrinaut - #9071

Merged
alex-e-leon merged 3 commits into
mainfrom
claude/fe-1228-unify-panda-css
Jul 22, 2026
Merged

FE-1228: Unify Panda CSS generation for HASH and Petrinaut#9071
alex-e-leon merged 3 commits into
mainfrom
claude/fe-1228-unify-panda-css

Conversation

@claude

@claude claude Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

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.css and Petrinaut's prebuilt dist/main.css. The polyfill replaces @layer with 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-helpers import map) then resolve by boost count instead of component intent: e.g. HASH's .grid-tr_0fr / .op_0 silently 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

🔍 What does this change?

How it works

  • @hashintel/petrinaut now builds two extra artifacts (mirroring how ds-components already ships build info):
    • dist/panda.buildinfo.json via panda ship with a dedicated panda.ship.config.ts (runtime src/** 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 of panda.config.shared.ts into src/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 (not definePreset) so it adds no runtime/type dependency on @pandacss/dev; its shape is Preset-checked in panda.config.shared.ts.
  • Keyframes are namespaced (fadeInpetrinautFadeIn, expandpetrinautExpand, 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's fadeIn/expand/collapse animations app-wide. A new unit test in panda.config.shared.test.ts fails if a Petrinaut keyframe ever collides with a ds-components preset keyframe again.
  • apps/hash-frontend/panda.config.ts adds require.resolve("@hashintel/petrinaut/panda.buildinfo.json") to include and the Petrinaut preset to presets (listed first so ds-components/panda definitions win the only remaining scalar conflict, fonts.mono, keeping existing HASH surfaces untouched).
  • The embed page keeps importing 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)

  • Shared native @layer contract (solution 2): still blocked by hash-frontend's unlayered global resets — the reason the polyfill exists.
  • Isolate Petrinaut / drop the host bundle in the embed (solution 3): the embed uses ds-components directly (Button, VersionPicker) and _app global CSS can't be conditionally dropped per page in the pages router.
  • Prefix atomic utilities (solution 4): insufficient against ID-level polyfill boosts without also solving the layer contract, as the ticket notes.
  • Given the design system is expected to move further into hash-frontend over time, solution 1 also scales best: any new Panda-styled surface in hash-frontend lands in the same single pipeline/layer graph, and the superset guarantee above is order-of-growth-proof — adding more host styles can only keep the unified bundle's boosts ≥ Petrinaut's. Solutions 3/4 would instead multiply isolation boundaries as ds usage grows.

Pre-Merge Checklist 🚀

🚢 Has this modified a publishable library?

This PR:

  • modifies an npm-publishable library and I have added a changeset file(s)

📜 Does this require a change to the docs?

The changes in this PR:

  • are internal and do not require a docs change

🕸️ Does this require a change to the Turbo Graph?

The changes in this PR:

  • do not affect the execution graph
    • (@hashintel/petrinaut's build was split into build:lib + build:buildinfo, but the build task and its dist/** outputs are unchanged; hash-frontend codegen already depends on ^build.)

⚠️ Known issues

  • Petrinaut's full runtime utility CSS is now part of hash-frontend's global 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 second cssgen was prototyped but Next.js's pages router only allows app-local global CSS in _app.
  • A subtle intentional alignment: ds-components recipes rendered inside standalone Petrinaut previously animated with Petrinaut's union-merged 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.
  • The environment used to prepare this PR cannot build the Rust-backed @blockprotocol/type-system, so a full clean yarn lint:tsc for hash-frontend could not be reproduced locally — the 1,377 errors observed all pre-exist in @local/hash-isomorphic-utils system-types and none reference files touched here (panda.config.ts is 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.
  • Ticket acceptance also asks for a browser-level regression test asserting computed styles (drawer top padding, optimization parameter expansion) — proposed as a follow-up; this PR adds build-time guarantees + the keyframe-collision unit test.
  • Optionally, split vendor CSS out of dist/main.css later 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.
  • Build-time verification (script-checked, before → after):
    • Before: HASH bundle utilities at 5 boosts, Petrinaut's at 4; Petrinaut's [data-expanded='true']]:grid-tr_[1fr] (4 boosts) lost to HASH's .grid-tr_0fr (5 boosts).
    • After: that conditional exists in the unified bundle at 5 boosts with higher natural specificity than .grid-tr_0fr — Petrinaut wins as intended.
    • All 1,102 Petrinaut runtime Panda rules are present in the unified stylesheet at ≥ boost level with identical declarations, except the 35 .petrinaut-root-scoped preflight/token rules intentionally still served by dist/main.css; 0 rules at lower boost, 0 declaration mismatches, 0 keyframes missing or differing.
    • All 2,381 pre-existing unified rules are byte-identical before/after (keyframes and --fonts-mono included) — zero impact on existing HASH surfaces.
    • Petrinaut's dist/main.css is byte-identical to main outside @keyframes definitions, modulo the intended keyframe renames.
    • After rebasing onto FE-1229: Temporarily fix Petrinaut Panda specificity #9069's merge and reverting the stopgap: Petrinaut's dist/main.css utilities 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 on main (FE-1233/FE-1236).
  • Checks run (re-run after the rebase + stopgap revert): petrinaut lint:tsc (clean), petrinaut lint:eslint (clean), petrinaut test:unit (25 files / 172 tests pass), petrinaut build (clean), yarn constraints (clean), oxfmt --check on touched files (clean).

❓ How to test this?

  1. Checkout the branch, turbo run build --filter @hashintel/petrinaut, then in apps/hash-frontend run yarn panda cssgen --outfile ./src/pages/ds-components-styles.gen.css.
  2. Open a process in HASH and expand an optimization parameter in the Petrinaut embed's Simulate panel — the min/max search-space inputs must be visible (grid-template-rows: 1fr, opacity: 1 in computed styles).
  3. Compare against the standalone Petrinaut site — drawer sticky-header padding and parameter expansion should render identically.

📹 Demo

No visual change outside the bug fix; before/after evidence is in the test section above.

@vercel

vercel Bot commented Jul 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hash Ready Ready Preview, Comment Jul 22, 2026 3:34pm
petrinaut Ready Ready Preview, Comment Jul 22, 2026 3:34pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
hashdotdesign-tokens Ignored Ignored Preview Jul 22, 2026 3:34pm

@github-actions github-actions Bot added area/apps > hash* Affects HASH (a `hash-*` app) area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team area/apps labels Jul 20, 2026
@claude
claude Bot marked this pull request as ready for review July 20, 2026 10:15
@claude
claude Bot requested a review from alex-e-leon July 20, 2026 10:15
@cursor

cursor Bot commented Jul 20, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches global hash-frontend CSS generation and publishable @hashintel/petrinaut build outputs; incorrect preset/keyframes merging could affect animations app-wide, though tests and preset ordering mitigate this.

Overview
Unifies Petrinaut and hash-frontend Panda output so the embed no longer fights two independently polyfilled bundles (different :not(#\#) boost counts). @hashintel/petrinaut now ships panda.buildinfo.json (panda ship) and @hashintel/petrinaut/panda-preset; hash-frontend includes both in its Panda include / presets so ds-components-styles.gen.css is the single source of truth for Petrinaut atomic utilities.

Theme extensions move from inline panda.config.shared.ts into src/panda-preset.ts (also built as a Vite entry). Colliding keyframes are renamed (petrinautFadeIn, petrinautExpand, etc.) with component references updated; a unit test blocks future ds-components keyframe name clashes.

The temporary PostCSS boost-panda-specificity workaround (#9069) is removed; PostCSS is plain Panda again. The embed still imports dist/main.css for vendor CSS and .petrinaut-root scoped tokens/preflight—documented as intentionally shadowed for Panda utilities.

Reviewed by Cursor Bugbot for commit ac66ab2. Bugbot is set up for automated code reviews on this repo. Configure here.

claude added 2 commits July 22, 2026 12:13
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.

@alex-e-leon alex-e-leon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@alex-e-leon
alex-e-leon enabled auto-merge July 22, 2026 15:13
@alex-e-leon
alex-e-leon added this pull request to the merge queue Jul 22, 2026
Merged via the queue into main with commit ff6b8f8 Jul 22, 2026
47 checks passed
@alex-e-leon
alex-e-leon deleted the claude/fe-1228-unify-panda-css branch July 22, 2026 15:52
This was referenced Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/apps > hash* Affects HASH (a `hash-*` app) area/apps area/deps Relates to third-party dependencies (area) area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team

Development

Successfully merging this pull request may close these issues.

2 participants