Skip to content

style(prompt-field): snap transparency values to nearest token ramp stops - #6627

Closed
rubencarvalho wants to merge 81 commits into
mainfrom
ruben/style-prompt-field-opacity-ramp-snap
Closed

style(prompt-field): snap transparency values to nearest token ramp stops#6627
rubencarvalho wants to merge 81 commits into
mainfrom
ruben/style-prompt-field-opacity-ramp-snap

Conversation

@rubencarvalho

Copy link
Copy Markdown
Contributor

Description

Experimental spike for the transparent-gray token discussion. Not intended to merge as-is — it exists to show, on top of the AI brand treatment in #6610, what the prompt field looks like if every off-ramp transparency value is snapped to the nearest stop on the proposed ramps.

Based on ruben/feat-prompt-field-ai-branding-v3 so the diff is only the value swap.

What changed

Replaces the off-ramp opacity/alpha/mix percentages with the nearest stop on the proposed transparent-neutral / static-white / static-black / opacity ramps:

Was Now Ramp stop
5% 6% *-125 (exact tie between 4% and 6%; 6% chosen)
12.5% 12% opacity-200
30% 32% opacity-500
50% 48% opacity-700
75% 80% opacity-900

Applied across all three roles those numbers play, so nothing is left half-snapped:

  • true white/black alpha (borders, shadows, inset highlights)
  • alpha on the brand color (ring hue-sweep, gloss)
  • color-mix ratios in the card wash

59 lines, one file (prompt-field.css).

Motivation and context

The proposed ramps only partially fit today: 2 / 3 / 4% land on the static-white/black stops exactly, but 5% and the 12.5 / 30 / 50 / 75 opacity scalars fall between stops, and most of the visible tint is a color-mix ratio rather than transparency. This branch quantifies the visual cost of forcing everything onto the ramp so the tokens discussion can decide between adding stops (5%, 12.5%) or rounding.

Open questions

  • 5% is a genuine tie (4% vs 6%). Prefer subtler (4%) or the choice here (6%)?
  • Should the card-wash color-mix ratios track the opacity ramp at all, given they are blend ratios, not alpha?
  • Brand-tinted alphas outside the five values (10 / 11 / 14 / 15 / 60%) are left untouched.

Screenshots

To add: before/after of subtle / balanced / prominent, light and dark.

rubencarvalho and others added 30 commits August 6, 2026 15:28
…ition

Adds an `expanded` boolean property to `swc-prompt-field`, matching the
Figma design's collapsed/expanded variant naming. Defaults to the
single-line collapsed pill (send button inline with the textarea);
`expanded` renders the taller layout with a separate action bar
(upload button + send/stop button on their own row).

Also fixes a rendering bug uncovered while implementing this: the
collapsed pill's border-radius used `corner-radius-full` (50%), which
renders an ellipse on a non-square box instead of a stadium shape.
Radius is now derived from half of the pill's own fixed block-size,
mirroring the existing fix in linear-progress-base.css.

The toggle between layouts is animated: box shape (border-radius,
padding, gap) transitions directly, and the action bar's height
animates via the calc-size() progressive-enhancement pattern already
used in accordion-item.css. The send/stop button crossfades between
its inline and action-bar positions; the inactive copy is marked
inert + aria-hidden, following the same pattern as AccordionItem's
collapsed content.
…duplicated+hidden

Replaces the duplicate-button crossfade (one inline copy, one action-bar
copy, inactive one marked inert/aria-hidden) with a single conditionally
rendered button per layout. Simpler, and removes the class of bugs the
duplication caused: ambiguous .swc-PromptField-send/-stop queries,
inert-vs-focus interactions, and the compensating negative-margin hack
needed to cancel the row's gap around the crossfading copy.

The box shape and action bar height still animate on expand/collapse;
only the button itself now appears/disappears instantly rather than
fading, since there's no second instance to fade into.
…om property

Adds --swc-prompt-field-max-block-size (default 40vh) to the textarea's
max-block-size, combined with the existing max-rows cap via min() so
whichever is smaller wins. Guards against max-rows worth of text
overflowing the viewport on small/embedded surfaces; textareas already
scroll internally once content exceeds this, so no extra overflow rule
is needed.
Flips the boolean property from expanded (opt-in) to collapsed
(opt-in): the taller multiline card with the action bar is now the
default, unmarked layout, and the single-line row is the opt-in
collapsed layout via the new collapsed attribute.

All CSS state selectors, template conditionals, and animations are
inverted accordingly, along with tests, stories, and docs. Verified
live: the default (no attribute) now renders the expanded tree/layout,
collapsed opts into the single-line row, and the animated transition
still works in both directions.

BREAKING CHANGE: the expanded attribute is removed; consumers relying
on the previous expanded-opt-in, collapsed-by-default behavior must
add the collapsed attribute to keep the single-line layout.
…t verbosity

Extracts the action-bar block into _renderActionBar(), matching the
existing _renderStatusIcon() pattern, with a one-line comment
explaining why it stays always-mounted (calc-size height animation)
instead of conditionally rendered like the send/stop button.

Also trims several multi-line JSDoc/CSS comments down to one line
each; the removed detail was better suited to prose than inline code
comments.
Matches the Figma reference, which uses the same gap in both the
collapsed and expanded states; the input row previously widened to
16px by default, a leftover from an earlier iteration.
…ing API

Restructure controls into a shared icon+text-group wrapper used by both
collapsed and expanded layouts for animation consistency, fix collapsed
mode to scroll horizontally like a single-line input instead of clipping
wrapped lines, bump textarea type scale to font-size-200/line-height-200,
and drop the min-rows/max-rows API in favor of the 40vh max-height cap,
matching the reference implementation's sizing model.
…d-expanded' into ruben/feat-prompt-field-collapsed-expanded
…radius

The box-height and upload-button-width transitions animated independently
of the discrete single-line/two-row layout switch, which is not
interpolatable without flicker. The result was a visible desync: content
snapped to its final position instantly while the box/button sizes
lagged behind, producing an empty gap or a missing button mid-transition.
Drop the transitions so the layout switch is instant and consistent, and
use a fixed 24px radius (no matching token exists) for both layouts.
…tion

Explore a CSS-only alternative to the FLIP branch. calc-size(auto, size)
does not smoothly animate a flex-wrap-driven auto height in this engine
(verified in isolation with a minimal repro outside the component), so
switch .swc-PromptField-controls from flex-wrap to a 2-row CSS grid and
animate grid-template-rows between 0fr and 1fr instead, which does
interpolate smoothly. The box itself needs no height transition: it's a
plain flex column sized to the grid's content, so it naturally follows
every frame.

This also lets the send/stop button span both grid rows and center
itself on their combined height, so it re-centers automatically as row 2
grows/shrinks instead of needing a discrete row reassignment or a
JS-driven position correction. Removes the now-unnecessary line-break
marker element and the max-inline-size-based hiding on the upload
button, since the row's own height animation handles both.

Gotcha found along the way: padding-block-start on the row-2 grid item
(an overflow: hidden flex/grid item) still counts toward its own minimum
size and prevents the track from ever reaching a true 0fr; moving that
spacing to a margin on the child fixed it.
…transition

Row 1's min-block-size (24px) was shorter than the send/stop button (32px)
spanning into it, forcing the grid to reserve a few invisible px in row 2
to satisfy that minimum even when collapsed, pushing row 1's content off
its own vertical center. Match row 1's min-block-size to the button's own
height instead, and move the row-1/row-2 gap from a margin on the upload
button to .controls's own row-gap so it collapses to 0 in sync with the
grid-template-rows animation rather than leaving a fixed leftover gap.
Also switch the send/stop button from centering across the full span to
aligning with its bottom edge, matching the upload button's own position
when expanded instead of sitting visibly higher. Bumps the shared spike
transition duration from 160ms to 240ms; 160ms read as too quick.
Row 1 was auto-sized; an auto grid track adjacent to one animating via
grid-template-rows fr briefly overshoots its resolved size mid-transition
in this engine, confirmed via an isolated repro completely independent of
the send/stop button's grid placement (ruled that out first, since it was
the earlier suspect). The overshoot stretched row 1's content taller than
its final 32px and back, which read as the icon/text bobbing down and
back up. A fixed 32px track has nothing to recompute mid-transition.
…ments

Replace the custom --swc-prompt-field-spike-duration property with
token("animation-duration-500") (250ms, closest match to the 240ms this
was tuned to), and tie row 1's fixed height and the send/stop button's
size together via the existing --swc-prompt-field-send-block-size custom
property instead of two separate hardcoded 32px literals. No matching
token exists for the cubic-bezier(0.8, 0, 0.2, 1) easing (a deliberate,
steeper curve than animation-ease-in-out), so it stays a literal. Also
condense several multi-line comments down to one line each.
…edent

animation-duration-500 (250ms) doesn't match any existing block-size/
height reveal transition in the codebase: accordion-item.css uses
animation-duration-100 (130ms) and message-sources.css/response-status.css
use animation-duration-200 (160ms) for the same kind of animation. Switch
to animation-duration-200 to match the closer, pattern-level precedent.
- Re-add min-rows/max-rows as optional properties (no default): unset,
  only --swc-prompt-field-max-block-size caps textarea growth.
- max-block-size now resolves against a sized ancestor's container query
  block size (40cqb) when one exists (e.g. a chat panel with a fixed
  height), falling back to 40vh otherwise. Fixes growth capping against
  an unrelated viewport (verified against the Storybook preview iframe).
- Fix a regression from the collapse/expand animation work: text-group
  was locked to a fixed height to stop icon/text jitter during the
  transition, which also silently clipped multi-line textarea growth
  instead of growing the card. Restructured so text-group stays in
  plain flex flow (grows freely) while only leading-actions' own reveal
  animates, in an isolated single-row grid; the send/stop button is
  anchored via absolute positioning instead of spanning grid rows.
- Fix leading-actions' 0fr grid row not collapsing to a true 0 height:
  overflow/min-block-size need to live on the grid item (a new
  leading-actions-row wrapper), not the grid container itself.
- Pin the status icon to the top of text-group so it doesn't drift
  toward the vertical center as the textarea grows to multiple lines.
- Swap the placeholder three-dots icon for swc-ui-icon's asterisk,
  which reads more like an actual AI indicator; remove the now-unused
  ThreeDotsIcon export and its orphaned three-dots.svg reference asset.
Clarify that 40cqb resolves against a consumer-provided ancestor with
container-type: size, not the prompt field's own box (which would be a
circular reference), and show the wrapper needed to opt in.
Pair leading-actions' existing height reveal with an opacity transition
on the same timing, so the upload button fades out/in alongside the
height collapse instead of just being clipped away.
Give opacity a shorter duration than grid-template-rows, so it reaches
0 before the height finishes collapsing instead of exactly matching it.
Collapsed no longer clamps to a single line with horizontal scroll;
it now wraps and grows like the expanded layout, just starting more
compact. Also clarifies why the container-query max-height fallback
is safe (plain size query, not a style() query).
…ument 40cqb via custom property

The @container block only flipped the max-block-size default to 40cqb
when a consumer wrapped the field in a container-type: size ancestor.
Nothing in the repo triggered it, and it duplicated the
--swc-prompt-field-max-block-size escape hatch that already lets a
consumer set 40cqb themselves. Remove the block and document the
preferred container-relative cap via the custom property.
…-variant colors

Adds a variant property plus the outer ring, hue-sweep, gloss, and inset-shadow
layers behind the card, with real brand-derived OKLCH colors wired for
variant="subtle" across idle/hover/focus/generating states. Balanced/prominent
remain placeholder colors pending their own real-color pass.
rubencarvalho and others added 24 commits August 13, 2026 22:01
These are empty, role-less spans; aria-hidden is a no-op since
nothing in them would ever reach the a11y tree.
Matches the reference implementation: a text cursor over the card
hints that clicking blank space (padding, gaps between rows) focuses
the textarea, same as clicking it directly. A pointerdown handler
walks up from the actual target and only falls through to focus when
nothing focusable (or a slot boundary, for slotted artifact tiles)
is found along the way, so real controls and artifact interactions
are unaffected.
@Property's initial-value doesn't reach elements declared in the same
shadow root that consumes it (confirmed via an isolated Chromium
repro, and documented at shadow-dom-css.adobe.com); a light-DOM
registration doesn't have this problem, and swc.css is already
imported at the document level. Removes the :host workaround
re-declaration this previously required.
…lements

Retargets .swc-PromptField-hue-sweep and .swc-PromptField-inset-shadow
to .swc-PromptField-box::before/::after, removing both spans from the
render template. Pseudo-elements are always excluded from the a11y
tree (stronger guarantee than the aria-hidden they carried before),
and preserve the same stacking order as the real elements did:
::before is always the box's back-most child, ::after always its
front-most, matching hue-sweep's back position and inset-shadow's
front position in the original DOM order. Verified paint order and
the generating-state animation targeting in Chromium before applying.
gloss stays a real element in the middle, since only two pseudo-
element slots are available for three layers.
…ndex

.swc-PromptField-box gets an explicit z-index: 0 to anchor its own
stacking context, and the three decorative layers (::before, .gloss,
::after) get z-index: -1, guaranteeing they stay behind all of the
box's real content regardless of DOM/generation order. Defensive fix;
no confirmed regression, but removes the theoretical stacking risk
pseudo-elements introduced.
…bled

Replaces the flat gray-75/gray-100 backgrounds with a relative-color
expression that picks black or white (whichever contrasts) from the
card's own current bg-stop-1, then applies it as a low-alpha tint.
calc((0.5 - l) * infinity) overflows to +/-infinity depending on the
source lightness, which oklch()'s L channel clamps to 0 or 1, same
technique as react's reference implementation. Meshes with the
gradient across variants/themes instead of a static gray regardless
of the card's current color.
Renders the variant x generating permutations in light/ltr and dark/rtl
within the single Variants story (one snapshot, both axes), matching
the badge VRT convention.
…nimate

con-hue-opacity, bg-stop-1..4, and outer-stop-1..3 were @property-
registered inside prompt-field.css (the shadow root), where the
registration doesn't take effect for the root's own elements. That
left them effectively unregistered and non-interpolable, so every
hover/variant/state change flashed instead of animating over the
700ms transition. Moving all eight to swc.css (light DOM, alongside
brand-color) makes them genuinely registered and animatable, matching
the reference implementation's smooth transitions.
No element carries that class: the component dispatches
swc-prompt-field-upload-click and leaves the file picker to consumers,
so there is no internal file input for the rule to hide. Leftover from
an earlier internal-input iteration.
The .swc-PromptField-legal-disclaimer shadow rules never applied (the
disclaimer is slotted light DOM), so the footer rendered at UA
defaults: 14px, black, start-aligned, accent-blue link. Moves the
typography onto slot[name="legal"] so it inherits into slotted content
(illustrated-message's pattern): 12px, neutral, centered, matching the
reference implementation's ui-sm disclaimer (uniform gray-800 text and
link in both themes). Sets the neutral link-color vars on the slot too,
so inline links in a swc-Typography--links paragraph inherit the footer
font size and render in the neutral color instead of accent blue,
matching react's secondary inline link. Stories, VRT, and docs updated
to author the disclaimer with swc-Typography--links.
…e works in Firefox

The focus-recede selectors used :host(...) X:has(.swc-PromptField-input-area:focus-within),
which Firefox did not match, so the treatment never receded on focus there.
The legal footer renders outside .swc-PromptField-box / .swc-PromptField-outer-border,
so plain :focus-within on those elements scopes to composer focus (excluding legal-link
focus) and works in every engine. Verified recede-on-composer-focus and no-recede-on-
legal-focus in both Chromium and Firefox.
Extends the prefers-contrast focus-ring block to (forced-colors: active)
and adds an always-visible border on the outer wrapper, which
forced-colors renders as an opaque system-colored boundary so the
control edge isn't lost. Per Steph's review suggestion.
…private cp

The 700ms cubic-bezier(0.32, 0.72, 0, 1) timing was repeated across
every transition declaration (box, outer border, inset/hue-sweep
pseudo-elements). Defines --_swc-prompt-field-transition once on
.swc-PromptField and references it everywhere. Per Steph's review nit.
…heir rules

Groups the box's border/drop-shadow/inset-shadow-color definitions (and
the wrapper's transition-timing cp) at the top of each rule instead of
after the property declarations. Per Steph's review nit.
Adds flexLayout: 'column-center' to the Playground so the field renders
centered and capped at 80ch instead of full-width at the top, which
reads closer to real-world usage. Per Steph's review nit.
…ceded surface

The subtle-idle and focus-recede states set the card wash to
light-dark(white, gray-75), which is exactly token("background-layer-2-color").
Holds it in --_swc-prompt-field-recede-surface and references the token
instead of the raw light-dark (8 occurrences across two rules). Per
Steph's review nit.
…he remaining rules

Applies the cp-defs-first ordering file-wide (outer border, hue-sweep
pseudo-element, artifact single/multiple/viewport/scroll-button rules,
and the legal slot), not just the box rule flagged in review, so the
convention is consistent across the stylesheet.
…n a private cp

light-dark(rgb(255 255 255), rgb(255 255 255 / 2%)) was repeated for all
three outer-border stops in both the subtle-idle and focus-recede rules.
Holds it in --_swc-prompt-field-outer-recede-stop and references that in
the six spots. Per Steph's review nit.
…layground render

The stories use a manual renderPromptField rather than the helper's
template, so the --swc-prompt-field-brand-color control had nowhere to
apply. Threads the arg onto the element's inline style, so the color
control now retheme the treatment live. Per Steph's review.
…the reference

Replaces flexLayout: 'column-center' (80ch cap) with a demo wrapper of
inline-size 800px, max-inline-size 90vw, centered -- matching react's
demo. Story wrapper only, not the component.
…ants

Mirrors the other components' forced-colors coverage: captures the
opaque card boundary the treatment falls back to when forced-colors
drops the gradients.
…tops

Experimental, for the transparent-gray token discussion; not intended to
merge as-is.

Replace the off-ramp opacity/alpha/mix percentages (5, 12.5, 30, 50, 75)
with the nearest stop on the proposed transparent-neutral / static-white /
static-black / opacity ramps (6 / 12 / 32 / 48 / 80). Applied across all
three roles those numbers play so nothing is left half-snapped:

- true white/black alpha (borders, shadows, inset highlights)
- alpha on the brand color (ring hue-sweep, gloss)
- color-mix ratios in the card wash

5% is an exact tie between the 4% and 6% stops; 6% chosen. Brand-tinted
alphas that are not among the five values (10/11/14/15/60%) are left as-is.
@changeset-bot

changeset-bot Bot commented Aug 17, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: a54e325

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@rubencarvalho rubencarvalho added Status:WIP PR is a work in progress or draft do-not-merge NO MERGE-Y! labels Aug 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📚 Branch Preview Links

🔍 Gen1 Visual Regression Test Results

When a visual regression test fails (or has previously failed while working on this branch), its results can be found in the following URLs:

Deployed to Azure Blob Storage: pr-6627

If the changes are expected, update the current_golden_images_cache hash in the circleci config to accept the new images. Instructions are included in that file.
If the changes are unexpected, you can investigate the cause of the differences and update the code accordingly.

Base automatically changed from ruben/feat-prompt-field-ai-branding-v3 to main August 19, 2026 09:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do-not-merge NO MERGE-Y! Status:WIP PR is a work in progress or draft

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant