Skip to content

feat(date-picker): apply pressed scale to Date Picker and add pressed bg in month selector - #1931

Draft
iamchanii wants to merge 37 commits into
devfrom
apply-date-picker-pressed-scale
Draft

feat(date-picker): apply pressed scale to Date Picker and add pressed bg in month selector#1931
iamchanii wants to merge 37 commits into
devfrom
apply-date-picker-pressed-scale

Conversation

@iamchanii

Copy link
Copy Markdown
Collaborator
  • feat(rootage): describe the pressed scale with a scaleScope enum
  • feat(qvism-preset): derive a press scale from the element's rendered size
  • feat(react): publish an element's rendered size as CSS custom properties
  • feat: apply the press scale to self-scoped slots
  • feat(rootage): give callout and page banner close buttons their own specs
  • docs: render the close button specs on callout and page banner
  • refactor(qvism-preset): pin the resting press scale on the class
  • feat(css): publish the press scale class name
  • docs: warn that the duplicated breakpoint values must move together
  • feat(css): publish the press scale class name and values
  • docs: explain applying the pressed scale to custom components
  • docs: document opting a nested mark out of the pressed scale
  • docs: turn the pressed scale caveats into guidance
  • docs: describe turning the pressed scale off project-wide
  • feat(date-picker): apply pressed scale
  • feat(date-picker): add pressed header style
  • docs(date-picker): align custom cell example

te6-in and others added 14 commits August 7, 2026 15:35
Component specs pinned the pressed scale to fixed token values on
action-button and list-item, and said nothing at all for the other 27
components that shrink on press. Replace that with a scaleScope enum on every
slot that participates: `self` shrinks the whole slot, `content` keeps the
background in place and shrinks only the content layer.

- 35 slots across 33 specs declare `scaleScope: enum [self, content]`, assigned
  under base.pressed.
- action-button and list-item drop their fixed scale values. The qvism and
  lynx-qvism recipes now read $scale.s95/s97/s98 straight from the token vars
  instead of routing them through component spec vars.
- The press-scale prose on checkmark/radiomark/switchmark moves off the slot
  and splits: the opt-out caveat goes on the scaleScope property, and the hit
  area fact goes to checkbox/radio/switch, whose specs otherwise read as if
  they have no pressed feedback at all. The old wording named a single wrapper
  even though all three marks are reused elsewhere.

No slot that declares scaleScope publishes its own scaleDuration /
scaleTimingFunction. The press transition is one curve for the whole system, so
a per-component value was an override surface with nothing per-component to
resolve; the two recipes that read it now take $duration.pressed-scale and
$timing-function.pressed-scale from the token vars directly. Kept where the
properties describe a different effect: switchmark's thumb, whose `scale` is
the selected-state size and now says so, and slider-thumb, which grows on press
rather than shrinking.

tab's base.enabled.root held nothing but those two properties, so the slot goes
with them.

enum properties emit no CSS variables, so the generated vars only move where a
fixed scale or a transition was removed, or a description was added.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1ZqXpgCJJr6hNqPKFcWMw
…size

A pressed element shrinks by a fixed depth on its dominant dimension rather
than by a per-size `$scale.sXX` token, so every pressable loses the same amount
regardless of how large it is:

  basis = max(height, width / 4, 24)
  scale = (basis - 2) / basis

A first-order variant of react-spectrum S2's `p / (p + depth)`, computed as a
plain scale with no 3D transform involved.

The derivation ships once in base.css on `.seed-press-scale` rather than being
inlined into every recipe that scales; the declarations land on the same element
either way. Reduced motion pins the output on that element after the derivation,
so no value a consumer sets upstream brings the scale back.

The three parameters are constants, not tokens. They describe the curve for
every pressable in the system, so there is nothing per-theme or per-component to
resolve, and as `:root` custom properties they would have been an override
surface — including one that defeats the reduced-motion rule.

`--seed-element-width` / `--seed-element-height` are published by
`@seed-design/react`. Without JS they are unset, which makes the derivation
guaranteed-invalid, and every consumer's `var(--seed-press-scale, 1)` falls back
to 1 — no scale, no breakage.

Ported from feat/dynamic-scale, minus its counter-scale half: the slots that
keep a fixed background while only their content shrinks (`scaleScope: content`)
need two boxes at different scales and are held for the next major.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PYxdfTtfB8oKBcWKu5kKc4
`useElementSizeVars` keeps unitless `--seed-element-width` /
`--seed-element-height` in sync on the element and stops there. The size is a
plain fact about the element rather than a feature, so whatever needs it derives
its own values in CSS — today that is the press scale in
`@seed-design/qvism-preset`, which owns the formula and the parameters.

Press detection stays in CSS `:active` gates, so nothing here tracks
interaction. One shared `ResizeObserver` serves every measured element, created
lazily so the module stays importable during SSR.

`usePressScale` hands back the size-var ref and the class that derives
`--seed-press-scale` together, so the two halves of the input cannot drift
apart; if one goes missing the derivation chain stays invalid and consumers fall
back to `scale: 1`, costing the effect rather than producing a wrong scale.

Two details are load-bearing and easy to lose:

- Compose the ref with `useComposedRefs`, never a bare `composeRefs`. The latter
  returns a new function every render, which makes React detach and re-attach
  the ref — re-registering the observation and republishing the vars on every
  render instead of on every resize, restarting the press transition each frame
  for a component that re-renders while held.
- Observe with `{ box: "border-box" }`. The content box would miss a border-box
  change that leaves it untouched, e.g. a variant that only alters padding.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PYxdfTtfB8oKBcWKu5kKc4
Every slot whose spec declares `scaleScope: self` now scales as a whole on
press, driven by the runtime value instead of a fixed `$scale.sXX` token: 24
recipes and 26 components across action-button, the attachment-input family,
chip, chip-tabs, tabs, toggle-button, the marks, the floating buttons, the
reaction buttons, quantity-picker's decrement/increment buttons, and the
close/action buttons of bottom-sheet, menu-sheet, side-panel, snackbar,
help-bubble, callout, page-banner, input-button and content-dialog.

The scale always gates on `active` — `pseudo(not(disabled), active)` where the
slot has a disabled state, plain `pseudo(active)` where it doesn't, and
`[aria-grabbed=true]` for attachment-input so a dnd-kit dragged item shrinks by
the same depth, composing with the drag translate rather than clobbering it.
quantity-picker's buttons and content-dialog's close button keep `engaged` for
their pressed background, but take the scale on `active` alongside it: `engaged`
resolves to `:hover` on a hover-capable device, which is right for a background
tint and wrong for press feedback — it would shrink the button on hover alone,
before any press.

Marks read the value through `--seed-{checkmark,radiomark,switchmark}-press-scale`
so an ancestor that already gives its own pressed feedback can opt a nested mark
out; list-item does that on `prefix` and `suffix`.

Slots declaring `scaleScope: content` are deliberately untouched. Their
implementation needs a fixed background layer at a different scale from the
content, which is held for the next major — accordion-item, input-button root,
list-item, menu-item, menu-sheet-item, page-banner root, segmented-control-item,
select-box, select-trigger and select-item keep their current behaviour.

callout root is `self` rather than `content`: the whole card shrinks, background
and focus ring included, so the ring can stay on the root instead of moving to a
counter-scaled layer.

content-dialog's close button switches from `composeRefs` to `useComposedRefs`,
which also stops its render-tracking ref from detaching and re-attaching on
every render.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK7NT69oFSVhhKgaKgZsdk
…pecs

Both close buttons borrowed the parent's `suffixIcon` slot for their target
size, so the parent carried a `targetSize` nothing else read. Each close button
now owns a spec of its own — `callout-close-button` and
`page-banner-close-button` — alongside the existing dialog, side panel and
bottom sheet ones, and gains a pressed background the transparent version never
had.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KXwDCV9syRzMUfzfr1wzgj
Side panel already lists its close button spec next to its own; these two
pages showed only the parent, leaving the new specs invisible.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KXwDCV9syRzMUfzfr1wzgj
The resting `scale: 1` was declared per slot by `createPressScaleRestStyles`,
so a consumer building their own pressable had to remember it too. Forgetting
it was the one failure that did not fail closed: the element became a stacking
context only while pressed, which moves `position: fixed` descendants
mid-gesture, with no error to trace.

`.seed-press-scale` already had to be on the element for the derivation, so it
declares the identity value as well and every element that opts in gets it.
Callout is the one behaviour change: its resting scale was nested under
`:is(button, a)`, so a non-actionable callout is now a stacking context too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SZDvriSH2q9jDqgwdrr7Qi
`seed-press-scale` was hardcoded in two packages that cannot import each other:
qvism-preset emits the rule into base.css, and react has to put the class on
every element it measures. A mismatch produced no error — the derivation would
simply never apply.

`@seed-design/css/class-names` makes it a declared value react imports, the same
topology `@seed-design/css/breakpoints` already uses. qvism-preset still keeps a
copy, since css is generated from it, and now says so.

Consumers building a custom pressable get the string too, instead of copying a
literal out of the docs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SZDvriSH2q9jDqgwdrr7Qi
The copy in qvism-preset named its origin, but the origin never named the copy —
and packages/css is the side someone editing a breakpoint value would reach for,
since it is the published one. Drift there silently splits the generated media
queries from the runtime store that reads the same names.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SZDvriSH2q9jDqgwdrr7Qi
`--seed-press-scale` and `--seed-press-scale-transition` are meant to be read
by styles this package does not author, unlike the component vars next to them.
Export them as strings so consumers can splice them into their own rules.

`pressScale` carries the resting fallback because the custom property is not
registered with `@property`: consumed without it, `scale` computes to `none`
before the element is measured and drops the stacking context the resting
`scale: 1` holds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SZDvriSH2q9jDqgwdrr7Qi
The scale is derived at runtime from the element's own size, so a consumer
building a pressable of their own needs two halves: `usePressScale` publishes
the size, and their own rule consumes the ratio. Cover both, with the
Tailwind CSS, vanilla-extract and plain CSS forms of the consuming rule.

The live example is a card and a row of tiles rather than a button — SEED
ships no card, so that is the surface consumers actually hand-build, and the
5x size gap between the two makes the fixed 2px depth visible.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SZDvriSH2q9jDqgwdrr7Qi
A custom pressable wrapping a mark scales twice — once on the wrapper, once
on the mark. Each mark's doc now names the custom property that turns its own
scale off, framed around the mark rather than the composed component since
all three are also documented for standalone use.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SZDvriSH2q9jDqgwdrr7Qi
The `position: fixed` note explained why the resting `scale` is pinned, which
describes a symptom the pinning already prevents. Say instead that the
containing block stays put across the press, so one check of the descendant's
placement covers both states.

The `scale` note offered `transform` as an equal alternative. Recommend the
individual property outright: `pressScaleClassName` already sets `scale`, so
`transform` splits the element's scaling across two sources on top of having
to carry any other transform in the same value.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SZDvriSH2q9jDqgwdrr7Qi
Every consumer reads one variable, so pinning it to 1 on the class that
declares it turns the effect off everywhere at once. The rule has to land
after SEED's own CSS to win the cascade, so the note leads with that.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SZDvriSH2q9jDqgwdrr7Qi
@changeset-bot

changeset-bot Bot commented Aug 7, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4284002

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@seed-design/react Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9566ec4a-6cb8-4529-a1c1-aa9f6cccbfaa

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@iamchanii iamchanii changed the title apply date picker pressed scale feat(react): apply pressed scale to Date Picker and add pressed bg in month selector Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Alpha Preview (Stackflow SPA)

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Alpha Preview (Storybook)

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Alpha Preview (Docs)

te6-in and others added 10 commits August 10, 2026 16:24
…clared

Every rule that consumed the pressed scale had to spell the resting fallback
itself, and omitting it was silent: `--seed-press-scale` was guaranteed-invalid
until the element was measured, so a bare `scale: var(--seed-press-scale)`
computed to `none` and dropped the stacking context the resting `scale: 1`
exists to hold.

Split the name in two. `--seed-press-scale-measured` holds the derivation and
stays guaranteed-invalid before measurement; `--seed-press-scale` is what
everything reads and absorbs that invalidity into 1 where it is declared. A
matching `:root` default keeps it defined outside any pressable, so the ratio
resolves document-wide and no consuming rule needs a fallback of its own.

`@property` with `initial-value: 1` would give the same guarantee without the
second name, but SEED supports Safari 15, which shipped individual `scale` in
14.1 and `@property` only in 16.4 — a registration would hold on newer engines
alone, turning a missing fallback into a bug that reproduces on the old browsers
exclusively.

`pressScale` from `@seed-design/css/press-scale` drops its baked-in fallback
accordingly. Rules already written as `var(--seed-press-scale, 1)` keep working
unchanged; the fallback is simply never taken.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NhMn2efohuBnctJ2V53xJg
The ratio now resolves to 1 wherever no measurement exists, so a consumer's own
rule reads `var(--seed-press-scale)` bare. Removes the warning callout that
existed only to stop readers from forgetting the fallback.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NhMn2efohuBnctJ2V53xJg
Puts every element the pressed scale touches on one screen so the behaviour can
be pressed rather than reasoned about: SEED components, the three ways a custom
component opts in, and the cases where it deliberately does nothing — a missing
ref, a missing class, and a mark whose ancestor pinned its scale.

Each panel carries a readout of the size the element published, the ratio
derived from it, and the distance it actually loses. Reading it as a table
rather than prose is the point: a column of identical shrink values is how "a
32px button and a 343px button both lose the same 2px" becomes visible without
doing arithmetic.

The readout resolves a mark's override the way the recipe does, so a Checkmark
inside a ListItem reports the 1 it actually applies instead of the ratio it
derived and never uses.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NhMn2efohuBnctJ2V53xJg
The hook required callers to hand it a stable ref. A fresh function every render
makes React detach and re-attach, which unobserved the element and republished
the vars on every render instead of on every resize — so the rule landed on every
caller, and getting it wrong was silent.

The hook now tells a genuine detach from an identity change itself. It tracks
what React last handed it against what the observer is watching, and defers the
`null` teardown to a microtask: a commit runs to completion synchronously, so the
re-attach an identity change performs has already landed by the time the
microtask runs, and the two disagree only when the element really left.

Teardown still runs on a real detach — the element is unobserved and its vars
removed — so an element that keeps the class but loses the ref falls back to a
ratio of 1 rather than holding a stale one.

Also records why the `offsetWidth` / `offsetHeight` fallback and the dropped
`box` option are live paths rather than formalities: `borderBoxSize` and
`observe({ box })` both landed in Safari 15.4, and SEED supports Safari 15.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A7ciSNTQ7FfFZa6WuJ3ZfU
`usePressScale` hands back two halves that have to land on the same element, and
dropping either one is silent — the ratio stays 1 and nothing else about the
element changes. `PressScale` pairs them up: wrap the element and the wiring is
done.

It slots onto its child rather than rendering an element of its own. A wrapper
would be the element measured, and a block wrapper around an inline-block button
reports the container's width, so the ratio would come out wrong rather than
absent; the resting `scale: 1` would settle on the wrapper too, leaving the child
to become a stacking context mid-press. That is also why there is no `asChild`.

Applying the ratio is still the consumer's to write. In development the component
warns when the child never forwarded `ref` to a DOM element or dropped the
`className` it was handed — the two ways slotting fails without leaving a trace.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A7ciSNTQ7FfFZa6WuJ3ZfU
`@seed-design/css/class-names` held one export, and that export was already
re-exported from `@seed-design/css/press-scale` beside the two values its rules
consume. The split published one constant under two names and left the library
importing from a different entrypoint than the docs teach.

`class-names` also had no organising principle to say what else belonged in it.
Every other entrypoint is named for a concept — `theming`, `breakpoints`,
`press-scale` — so a second behaviour class can get its own drawer once one
exists. Neither entrypoint has shipped, so there is nothing to deprecate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A7ciSNTQ7FfFZa6WuJ3ZfU
Wrapping is the shorter path and the one that cannot be half-applied, so it takes
the main flow and `usePressScale` moves to a section for elements you can't wrap.
The advice to compose refs with `useComposedRefs` goes with it — the hook now
survives an unstable ref on its own.

Adds registering the ratio as a Tailwind utility, since a project applying it in
more than one place would otherwise repeat the variable at every call site. The
note has to say `@utility` rather than `@theme`: a `@theme` entry resolves at
`:root`, so the ratio an element computed from its own size never reaches it and
the class is generated while nothing shrinks. The transition can't be folded in
the same way — `transition-*` applies Tailwind's own duration and timing function
over the ones the variable carries.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A7ciSNTQ7FfFZa6WuJ3ZfU
Each panel carried a table reporting the size the element published, the ratio
derived from it, and the distance it lost, and a further panel compared the
current size observation against a reimplementation of the Safari 15.0-15.3 path.
Both had to duplicate the derivation in JS to report on it — a second
implementation to keep true to the first, for values devtools already shows.

The specimens are the point, and pressing them is what makes the behaviour
visible. Removes the readouts along with the `data-probe` labels that only fed
them, leaving the panels and the section descriptions that say what to look for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A7ciSNTQ7FfFZa6WuJ3ZfU
# Conflicts:
#	packages/css/all.layered.min.css
#	packages/css/all.min.css
#	packages/css/base.layered.min.css
#	packages/css/base.min.css
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A7ciSNTQ7FfFZa6WuJ3ZfU
te6-in and others added 10 commits August 10, 2026 18:17
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A7ciSNTQ7FfFZa6WuJ3ZfU
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0124vgqeUQ5E9RyBmE8c8FFv
The feature is called scale feedback everywhere a consumer can see it, and
dynamic scale names the runtime derivation behind it, so the implementation
can change without the public name moving. Names that hold a number end in
-scale:

  --seed-dynamic-scale-basis   max(height, width / 4, 24)
  --seed-dynamic-scale         the derivation, invalid until measured
  --seed-feedback-scale        var(--seed-dynamic-scale, 1) — what rules read

`--seed-element-width` / `--seed-element-height` keep their names: they report
size and know nothing about the feature consuming it.

None of this has shipped, so no alias is left behind.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyadPw4ZLB6nGDSkoCLNAf
$duration.pressed-scale and $timing-function.pressed-scale keep their names
while everything around them moves to scale feedback. Both already ship as
CSS custom properties, so renaming them is a breaking change that belongs on
its own schedule.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyadPw4ZLB6nGDSkoCLNAf
Both Tailwind tabs opened by promising `active:feedback-scale` as an
alternative, before saying the class has to be registered to exist. Naming
the condition and the action first puts the payoff last, where it reads as
a result rather than a claim.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyadPw4ZLB6nGDSkoCLNAf
The list stages sat flush against the panel, so a pressed row's background
cleared the panel by 6px at the sides and nothing at the top — no radius can
look concentric against that. Pad the stage vertically by the same 6px the
background is already held off the row's sides, and take that 6px off the
panel's radius.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyadPw4ZLB6nGDSkoCLNAf
@iamchanii iamchanii self-assigned this Aug 11, 2026
@iamchanii
iamchanii force-pushed the apply-date-picker-pressed-scale branch from 008877a to 4284002 Compare August 11, 2026 07:48
@iamchanii iamchanii changed the title feat(react): apply pressed scale to Date Picker and add pressed bg in month selector feat(date-picker): apply pressed scale to Date Picker and add pressed bg in month selector Aug 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants