Repository navigation
fix(studio): ship component styles as their own stylesheet for embedders - #4393
Merged
Merged
Conversation
Studio's component rules (timeline clips, loaders, range inputs, toasts and the rest) lived only in the app sheet, so a page that embeds Studio's components with theme.css got unstyled clips and dark-on-dark labels. They move unchanged into components.css, exported as @hyperframes/studio/components.css; studio.css keeps the app shell and imports it, so Studio's compiled CSS has the same rules.
jrusso1020
approved these changes
Sep 24, 2026
jrusso1020
left a comment
Collaborator
There was a problem hiding this comment.
Approving at e519a60d. I checked the move and the guard directly.
Verified
- The move is byte for byte. I sorted the lines of
main'sstudio.cssand compared them with the lines of this branch'sstudio.css(minus the new import) pluscomponents.css. The only difference is the two lines of the new header comment incomponents.css. - The order change can't change which rule wins.
- The moved rules now sit before
body,:rootand#rootinstead of around them. - Nothing in
components.cssselects an element,:rootor*. index.htmlputs no class onhtml,bodyor#root, and no code insrcadds one. So no moved class rule and no app-shell rule can ever match the same element.- The
@layer basecursor rule loses to unlayered rules wherever it sits.
- The moved rules now sit before
components.cssworks on its own for an embedder.- It has no
@apply,theme(),@utilityor@variant. - Every
var()it reads is either defined intheme.cssorcomponents.css, set inline by the component (--clip-*inTimelineClip.tsx,--timeline-transition-zone-widthinTimelineTransitionBadge.tsx), or has a fallback (--hf-heading,--hf-text-*).
- It has no
- The export works.
filesincludessrc, so./components.cssis served fromsrcin bothexportsandpublishConfig.exports, the same as./theme.css.
Tests
src/stylesplus the two retargeted tests: 62/62 pass.- Mutation 1: I appended
.hf-toast {}tostudio.cssnested inside@mediaand then@layer base.componentStyles.test.tsfails, as it should. - Mutation 2: I dropped the
@import "./components.css"fromstudio.css.tokenGate.test.tsfails twice, so Studio can't silently lose its component styles. - The full studio suite with mutation 2 applied had 5460 passing and only those 2 failing.
Non-blocking
- An embedder now gets Studio's class names and
@keyframesnames (hf-toast-in,hf-topology-*, …) in its global scope. With thehf-andtimeline-prefixes, a clash is unlikely. It is worth one line in the embedder note that these names are global and unscoped.
— Rames
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.
What changes
A page that embeds Studio's components can now style them. Studio's component rules (timeline clips and their labels, loaders, range inputs, toasts, preset shelf, CodeMirror overrides and the rest) lived only in Studio's app stylesheet. An embedder that imports
@hyperframes/studio/theme.cssgot none of them: timeline clips drew as bare boxes, most disappeared against the row, and clip labels rendered black on a dark background.Those rules now live in
packages/studio/src/styles/components.css, exported as@hyperframes/studio/components.css.studio.csskeeps the app shell (Tailwind, palette reset, theme,@source, the button cursor, color scheme,body,#root) and importscomponents.css, so each rule has one home and Studio itself is unchanged.An embedder on Tailwind v4 imports:
The class names and
@keyframesnames incomponents.cssare global, as they are in Studio:timeline-*andhf-*prefixed, so a clash with the host page is unlikely but not impossible.How
exports,publishConfig.exportsandpackage-subpaths.json, shaped like the existing./theme.css(served fromsrc).hex-baseline.json: the 48 colour literals are now counted under the new file. The total is unchanged.scripts/contrast-pairs.json: the 33 source pointers intostudio.cssnow point at the same lines incomponents.css. Each one was checked by line content, not by arithmetic.studio.cssas text for rules that moved (VolumeControl.styles.test.ts,timelineMotionStyles.test.ts) now readcomponents.css.componentStyles.test.ts:studio.cssmay define no class rules at any depth, including inside@layerand@media, so a component rule can't drift back where an embedder never sees it.theme.cssandcomponents.cssalone must yield the timeline clip label rule.studio.cssat top level and inside@layer base, the label rule added back, and the label rule removed fromcomponents.csseach fail the matching test.Evidence
dist/styles.css, built before and after: the same 2 280 rules (identical as a multiset; only the order changed, because the imported rules now come beforebodyand#root, which target different elements).theme.cssand Studio's real timeline markup. The clip label's computed colour isrgb(0, 0, 0)before andrgb(255, 255, 255)after.Before
Timeline markup embedded with
theme.cssonly (today's embedder):After
The same markup with
theme.cssandcomponents.css:Studio is unchanged
The same 24-scene project in Studio, scene thumbnails hidden. main's screenshot and this branch's screenshot are byte-identical (md5
412b74d27e7c2f8716ae46fa1b9e2245for both), so it appears once:Review
An independent adversarial review over three passes:
studio.cssas text for rules that moved. It also flagged a guard test that missed classes applied outsideclassNameliterals, and a stale comment. All three are fixed here.@layeror@media. That is fixed too.The review also confirmed:
components.css;