Repository navigation
fix: classes that compiled to nothing, SSR column mismatches, and a CI guard - #6850
tsahimatsliah wants to merge 3 commits into
Conversation
…I guard The phone shell (#6767) broke desktop feed settings by splitting `w-74` into `tablet:w-74 w-full`: `w-74` had never existed, so the no-op turned into a desktop regression (#6840 fixed it). This sweeps the rest of the classes that compile to nothing and guards against new ones. Dead classes, now real: - Spotlight: `animate-spotlight-scrim-in`, `-panel-in` and `-list-fade` were never defined; keyframes and animations added. - `bg-<token>/NN` alpha modifiers cannot apply to CSS-variable colours (hot takes sparks and blobs, composer spam warning): `color-mix`. - `opacity-70/80` -> `opacity-64`, `z-10` -> `z-1`, `z-20` -> `z-[20]`, `hover:opacity-90` on the brand button -> `hover:brightness-90`, `hover:bg-action-blueCheese-hober` -> `hover:bg-accent-blueCheese-flat`. - `shadow-1`, `line-clamp-10`, `scale-60`, `scale-80` added to the config so the three places that already use them render. Hydration: - `MainFeedLayout`'s `--num-cards` and the tag page's follow grid rendered the client's column count on the hydration pass while the server rendered one column. In production React keeps a mismatched attribute, so the layout stayed one column until a later re-render. `useIsHydrated` renders the server value on that pass and the real one after mount; later mounts start hydrated, so client navigation does not flash. CI: - `scripts/check-dead-classes.mjs` compiles every class added by a branch through the real Tailwind config and fails on tokens with no selector. Runs as `dead_classes_changed` next to the strict typecheck. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
- Alpha modifiers work on the accent, text and status tokens (`withAlpha` wraps each variable with Tailwind's `<alpha-value>`), so the hot takes and composer classes are written as `bg-x/40` or, for an alpha outside the opacity scale, `bg-x/[0.35]`, not color-mix literals. - The three classes that never applied are deleted rather than replaced: the analytics button's hover (its Secondary pressed fill was the shipped look), the feedback caption's opacity (contrast), and the brand button's hover. - `useIsHydrated` reads React's own hydration state through `useSyncExternalStore`, so a boundary hydrated after the first effects still renders the server value; `MainLayoutHeader` uses it too. - The spotlight animations fade opacity only, so `both` leaves no transform on the scrolling list or the panel. - The checker skips tokens that touch an interpolation, scans whole files and keeps the literals that reach an added line, and follows the repo's Prettier config. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
tsahimatsliah
left a comment
There was a problem hiding this comment.
Reviewed the diff, compiled the Tailwind config on main and on this branch, and read the failing CI logs.
Blocking
- CI is red.
lint_sharedand the lint step intest_webappfail withtailwindcss/classnames-orderin files this PR doesn't touch:PostFocusCard.tsx:794,SidebarDesktopV2.tsx:2544,Spotlight.tsx:1016and:1103, andDevCardStep2.tsx:249. The cause is that registeringshadow-1,scale-60/80andanimate-spotlight-*as real utilities changes where the sorter puts them. Runningeslint --fixon those five files should fix it. caret-text-linkbreaks on every text field (inline comment onwithAlpha.ts).
Non-blocking
- The SSR column fix covers two consumers, but
FeedContainerwrites the same--num-cardswithout the gate (inline). - 6 existing
/10,/20and/30usages still compile to nothing, and the diff-only guard won't flag them (inline). eslint-plugin-tailwindcssis already installed, and itsno-custom-classnamerule covers what the new script does (inline).- Classes that never applied before now render, so several surfaces change visually. Before/after screenshots are needed (inline).
- Scope: this PR combines a change to every
text-*,accent-*andstatus-*utility (each now compiles tocolor-mix), a new hydration hook, a new CI tool and several visual changes. Splitting the colour-token change and the CI guard into their own PRs would make each one cheaper to review and to revert.
Reviewed by AI.
| if (!variable) { | ||
| return value; | ||
| } | ||
| return `color-mix(in srgb, var(${variable}) calc(<alpha-value> * 100%), transparent)`; |
There was a problem hiding this comment.
Blocking: this breaks caret-text-link, the caret colour on the shared text inputs (fields/common.tsx, fields/Textarea.tsx).
The custom caret plugin (packages/shared/tailwind/caret.ts) reads theme('colors') and writes each value straight into caret-color, so it never replaces <alpha-value>. Compiling caret-text-link on this branch gives:
.caret-text-link {
caret-color: color-mix(in srgb, var(--theme-text-link) calc(1 * 100%), transparent);
caret-color: color-mix(in srgb, var(--theme-text-link) calc(<alpha-value> * 100%), transparent)
}On main it compiles to caret-color: var(--theme-text-link). The plugin's declaration comes last, so it wins. Because it contains var(), the browser accepts it when parsing and only rejects it at computed-value time. The caret then falls back to auto (the text colour), and nothing reports an error.
Suggested fix: drop the custom caret plugin, since core Tailwind's caretColor already generates the same caret-* classes from theme.colors. Alternatively, have the plugin replace <alpha-value> with 1. Also check for other plugins or JS that read these colour objects directly and would pass the placeholder through.
Reviewed by AI.
| const isHydrated = useIsHydrated(); | ||
| const feedWidthStyle = { | ||
| '--num-cards': feedSpacinessCards.eco, | ||
| '--num-cards': isHydrated ? feedSpacinessCards.eco : ssrNumCards.eco, |
There was a problem hiding this comment.
Non-blocking: the same mismatch is still there in FeedContainer.
packages/shared/src/components/feeds/FeedContainer.tsx:175 writes '--num-cards' from currentSettings.numCards.eco with no hydration gate. useFeedHeroAd, Feed and useFeed also read numCards during the first render. All of them get the client's useMedia value on the hydration pass. If the main feed container is server-rendered, its grid has the problem this PR describes.
Rather than patching each consumer, could the gate go in FeedLayoutProvider? It owns numCards, so it could serve ssrNumCards while !useIsHydrated() and fix every consumer at once. ssrNumCards and the per-call-site gates would no longer be needed.
Reviewed by AI.
| }; | ||
|
|
||
| export default statusColors; | ||
| export default withAlpha(statusColors); |
There was a problem hiding this comment.
Non-blocking: 6 more alpha modifiers still compile to nothing.
The opacity scale has no 10, 20 or 30, so these existing classes are still dead after this change (checked by compiling on this branch):
bg-status-success/10:profile/SocialLinksInput.tsx,organizations/components/LinksInput.tsxbg-status-warning/10:webapp/pages/recruiter/[opportunityId]/plans.tsxbg-accent-cabbage-default/10:shortcuts/components/AddShortcutTile.tsxring-accent-cabbage-default/30:shortcuts/components/ShortcutEditForm.tsxbg-accent-onion-default/20:brand/HighlightedWord.tsx
The new guard only checks lines a branch adds, so it won't catch these. Either convert them to /[0.1]-style values here, as the hot takes classes were, or list them as a follow-up. The description currently says alpha modifiers on tokens work.
Reviewed by AI.
| @@ -0,0 +1,216 @@ | |||
| #!/usr/bin/env node | |||
| // Fails on Tailwind classes that compile to nothing. | |||
There was a problem hiding this comment.
Non-blocking question: why a custom script instead of the lint plugin we already have?
eslint-plugin-tailwindcss is already a dependency and configured in packages/eslint-config (callees: classnames/clsx/ctl/classNames). Its tailwindcss/no-custom-classname rule flags classes the Tailwind config doesn't generate. It runs in the editor and in the existing lint jobs, and accepts a whitelist for the hand-written stylesheet classes (shell-*, no-scrollbar, …).
This script adds 216 lines of regex heuristics (UTILITY, AMBIGUOUS, VARIANT prefix lists that must be kept in sync by hand), a separate CircleCI job, and a diff-only mode that can't see existing dead classes. It also counts every class in every *.module.css as globally defined.
Could we enable no-custom-classname (warn, or error with a whitelist) instead? If the plugin can't handle something here, it would help to note the reason in the PR description.
Reviewed by AI.
| ); | ||
|
|
||
| const baseShadows = { | ||
| 1: (color) => `0 0.125rem 0.25rem -0.0625rem ${color}66`, |
There was a problem hiding this comment.
Non-blocking: please add before/after screenshots (desktop and mobile). These classes never applied before, so making them real changes what users see. Checks so far confirm the classes compile, not how they look:
shadow-1(a new value, not taken from a design token) now appears onPostFocusCardandSidebarDesktopV2.scale-60andscale-80now apply inDevCardStep2atmobileLandmobileXXL.- In the hot takes onboarding card, the aura blobs and sparks were invisible because their background classes were dead. They now render in colour, and the wrapper drops to
opacity-64. - The spotlight scrim, panel and list now animate (gated on
motion-safe). - The comment thread toggle and the GIF star button now get a real z-index.
Some of these probably need design sign-off rather than "render as written", since the design that shipped is the one without them.
Reviewed by AI.
Why
The phone shell (#6767) broke desktop feed settings by splitting
w-74intotablet:w-74 w-full.w-74had never existed in our Tailwind config, so a no-op became a desktop regression (#6840 fixed it). A sweep of the files the shell PRs touched found more classes that compile to nothing, two SSR mismatches, and no guard. This PR fixes what the sweep found and adds the guard.Changes
Alpha modifiers work on tokens
bg-accent-avocado-default/40and friends compiled to nothing because the token colours were barevar(--theme-…)strings.withAlphawraps the accent, text and status tokens with Tailwind's<alpha-value>placeholder, so/NNworks (for an alpha on the opacity scale) and/[0.35]works for the rest. The hot takes sparks and blobs and the composer's spam warning keep their authored alphas and render for the first time. The plain utilities compile to the same colour as before.Dead classes, now real or removed
animate-spotlight-scrim-in,animate-spotlight-panel-inandanimate-spotlight-list-fadewere never defined (since feat: spotlight feature #6011). All three fade opacity only, so thebothfill leaves no transform on the scrolling list or the panel.opacity-70→opacity-64(the scale has no 70),z-10→z-1,z-20→z-[20](keeps its place under thez-[21]overlay).hover:bg-action-blueCheese-hober(its pressed Secondary fill is the design), the feedback caption'sopacity-80(contrast), and the squad brand button'shover:opacity-90.shadow-1,line-clamp-10,scale-60andscale-80are added to the config, so the three places that already use them render as written.Hydration
MainFeedLayout's--num-cardsand the tag page's follow grid rendered the client's column count on the hydration pass while the server rendered one column (dev warned on/searchat tablet width and on every tag page). In production React keeps a mismatched attribute, so the layout stayed one column until something re-rendered the component.useIsHydratedreads React's own hydration state throughuseSyncExternalStore: the server value on every hydration render, including a boundary hydrated after the first effects, and the real one on every later mount, so client-side navigation does not flash.MainLayoutHeaderdrops its localhasHydratedstate for it.CI
scripts/check-dead-classes.mjsextracts the class tokens from the string literals a branch touches (whole files, kept to literals that reach an added line, with interpolated classes skipped), compiles them throughpackages/webapp/tailwind.config.ts(utilities and components, plus every hand-written stylesheet), and fails on tokens with no selector.npm run check:dead-classes;--all <files>audits whole files. It runs asdead_classes_changednext to the strict typecheck.Verified
check:dead-classes --allon every touched file plus the spotlight and feed settings files: all 307 classes compile. In branch mode: all classes added by this branch compile.src/lib/transaction.spec.tsxthat this branch does not touch.Events
No new event names.
Experiment
None.
🤖 Generated with Claude Code
Preview domain
https://dead-classes-hydration-dailydotd.preview.app.daily.dev