Files
Danny White b10511052d fix(ui): share raised styling across dropdown triggers (#50830)
## Problem

Select, combobox and empty multi-select triggers use a flat border
treatment, while the default Button has a raised surface and shadow.
This makes dropdown controls look inconsistent when closed.

## Solution

Share the existing Button shadow variables and raised surface classes
with the select trigger CVA. SelectTrigger and ComboboxTrigger keep
their own layout, focus, and disabled behaviour.

The multi-select shares the raised surface when empty and keeps a sunk
field surface for selected chips. Static size classes keep its empty and
single-row selected states at the same height; wrapped badges can still
grow. The caret stays aligned as values are selected. Select, Combobox
and Multi-select share Button's size-specific corner radii. Button's
existing styling is preserved.

| Before | After |
| --- | --- |
| <img width="518" height="180" alt="CleanShot 2026-09-24 at 13 36
32@2x"
src="https://github.com/user-attachments/assets/3822e51f-b1c5-46a2-b268-0bf08a03ae06"
/> | <img width="534" height="224" alt="CleanShot 2026-09-24 at 13 36
41@2x"
src="https://github.com/user-attachments/assets/e553c973-3d21-4228-ae6f-d3c098d051d3"
/> |
| <img width="638" height="148" alt="CleanShot 2026-09-24 at 15 08
01@2x"
src="https://github.com/user-attachments/assets/6d0c3dfe-3392-4b76-ae61-b6aa7a09583d"
/> | <img width="658" height="148" alt="CleanShot 2026-09-24 at 15 08
17@2x"
src="https://github.com/user-attachments/assets/184c50c3-b951-410e-89d5-3b1c2ee8f3b8"
/> |
| <img width="482" height="162" alt="CleanShot 2026-09-24 at 15 07
22@2x"
src="https://github.com/user-attachments/assets/a088d832-d0a9-45c5-a272-9c84cc601d1d"
/> | <img width="490" height="168" alt="CleanShot 2026-09-24 at 14 59
52@2x"
src="https://github.com/user-attachments/assets/4df0ca29-53d8-4926-80db-1cafb705faad"
/> |

## Review instructions

1. Open the design system
[Select](https://design-system-git-dnywh-dropdown-trigger-surface-supabase.vercel.app/design-system/docs/components/select),
[Combobox](https://design-system-git-dnywh-dropdown-trigger-surface-supabase.vercel.app/design-system/docs/components/combobox)
and
[Multi-select](https://design-system-git-dnywh-dropdown-trigger-surface-supabase.vercel.app/design-system/docs/fragments/multi-select)
examples.
2. Compare the closed surfaces and corner radii with a default Button,
then check hover, focus and open states.
3. Select one or two multi-select items. Check that a single row remains
the same height as the empty control, the caret stays aligned and the
chip field remains distinct. Add enough items to check wrapping.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Summary

* **Style**
* Default buttons display a raised surface with a subtle shadow and
size-appropriate rounded corners.
* Select and combobox triggers share a raised, card-like appearance,
with consistent shadows and rounded corners at small sizes. Invalid
select triggers display a border.
* Empty multi-select controls use a raised surface, while controls with
selected values use a field-style surface.
* Small and tiny multi-select controls have updated spacing and sizing
in empty and selected states.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-28 14:26:47 +10:00

268 lines
7.9 KiB
CSS

/*
* Custom Tailwind v4 utilities migrated from packages/config/ui.config.js
* (addUtilities + matchUtilities) and packages/config/tailwind.config.js
* (the .bg legacy shim).
*/
/*
* The `bg` class worked in Tailwind v3 but stopped working in v4. Restored as
* an alias for `bg-default` to avoid a wide rename across the codebase.
* Should be deprecated in favor of `bg-default`.
*/
@utility bg {
background-color: var(--background);
}
/* Contrast 0.5 produces (0.05 + 0.95 * 0.5)² = 0.275625. */
@utility control-surface-shadows {
--button-shadow-opacity: 0.04;
--button-edge-strength: calc(var(--contrast-border, 0.275625) / 0.275625 * 0.6);
--button-edge-color: var(--colors-black);
--button-shadow-drop: 0 1px 3px 0 hsl(var(--colors-black) / var(--button-shadow-opacity));
--button-shadow-raised:
var(--button-shadow-drop),
inset 0 1px 0 0 hsl(var(--button-edge-color) / calc(0.04 * var(--button-edge-strength))),
inset 0 0 0 1px hsl(var(--button-edge-color) / calc(0.06 * var(--button-edge-strength))),
inset 0 0 0 1px hsl(var(--button-edge-color) / calc(0.1 * var(--button-edge-strength)));
--button-shadow-default:
var(--button-shadow-drop),
inset 0 1px 0 0 hsl(var(--button-edge-color) / calc(0.04 * var(--button-edge-strength))),
inset 0 0 0 1px hsl(var(--colors-black) / calc(0.06 * var(--button-edge-strength))),
inset 0 -1px 0 0 hsl(var(--colors-black) / calc(0.06 * var(--button-edge-strength))),
inset 0 0 0 1px hsl(var(--button-edge-color) / calc(0.1 * var(--button-edge-strength)));
@variant dark {
--button-shadow-opacity: 0.2;
--button-edge-strength: calc(var(--contrast-border, 0.275625) / 0.275625);
--button-edge-color: var(--colors-white);
}
}
@utility raised-control-surface {
@apply text-foreground bg-card shadow-[var(--button-shadow-default)];
@variant hover {
@apply bg-muted;
}
&[data-state='open'] {
@apply bg-muted;
}
@variant dark {
@apply bg-muted;
background-image: linear-gradient(
to bottom,
hsl(var(--colors-white) / 0.015),
hsl(var(--colors-black) / 0.01)
);
@variant hover {
@apply bg-accent;
}
&[data-state='open'] {
@apply bg-accent;
}
}
}
/*
* Soft shimmer gradient used by LoadingLine.
*/
@utility line-loading-bg {
background: rgb(0, 0, 0);
background: linear-gradient(
90deg,
rgba(0, 0, 0, 0) 0%,
rgba(255, 255, 255, 0.65) 50%,
rgba(0, 0, 0, 0) 100%
);
}
@utility line-loading-bg-light {
background: rgb(0, 0, 0);
background: linear-gradient(
90deg,
rgba(0, 0, 0, 0) 0%,
rgba(33, 33, 33, 0.65) 50%,
rgba(0, 0, 0, 0) 100%
);
}
/* may be unused — verify before pruning */
@utility mask-fadeout-right {
-webkit-mask-image: linear-gradient(to right, white 98%, transparent 100%);
mask-image: linear-gradient(to right, white 98%, transparent 100%);
}
/* may be unused — verify before pruning */
@utility mask-fadeout-left {
-webkit-mask-image: linear-gradient(to left, white 98%, transparent 100%);
mask-image: linear-gradient(to left, white 98%, transparent 100%);
}
/*
* Functional color utilities. The `--value(--color-*)` token accepts any
* registered --color-* design token (e.g. highlight-foreground-light).
*/
/* may be unused — verify before pruning */
@utility highlight-* {
box-shadow: inset 0 1px 0 0 --value(--color-*);
}
/* may be unused — verify before pruning */
@utility subhighlight-* {
box-shadow: inset 0 -1px 0 0 --value(--color-*);
}
/*
* Inline-SVG grid background. Used by ProjectLinker as bg-grid-black/5 and
* bg-grid-white/5 for the integrations onboarding visuals.
*/
@utility bg-grid-* {
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' width='32' height='32' fill='none' stroke='--value(--color-*)'><path d='M0 .5H31.5V32'/></svg>");
}
@utility no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
&::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera */
}
}
/*
* Text shimmer (shadcn shimmer utility, inlined so we don't pull in the full
* shadcn CSS package). Apply `shimmer` to text to draw attention.
*/
@property --shimmer-angle {
syntax: '<angle>';
inherits: true;
initial-value: 20deg;
}
@property --shimmer-image {
syntax: '*';
inherits: false;
}
@property --shimmer-text-fill {
syntax: '*';
inherits: false;
}
@utility shimmer {
--_spread: var(--shimmer-spread, calc(3ch + 40px));
--_base: currentColor;
--_highlight: var(--shimmer-color, oklch(from currentColor l c h / calc(alpha* 0.2)));
background-image: var(
--shimmer-image,
linear-gradient(
calc(90deg + var(--shimmer-angle)),
var(--_base) calc(50% - var(--_spread)),
color-mix(in oklch, var(--_highlight), var(--_base) 50%) calc(50% - var(--_spread) * 0.5),
var(--_highlight) 50%,
color-mix(in oklch, var(--_highlight), var(--_base) 50%) calc(50% + var(--_spread) * 0.5),
var(--_base) calc(50% + var(--_spread))
)
);
background-repeat: no-repeat;
background-size: calc(200% + var(--_spread) * 2) 100%;
background-position: 0 0;
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: var(--shimmer-text-fill, transparent);
animation: tw-shimmer var(--shimmer-duration, 2s) linear infinite;
transition: -webkit-text-fill-color 0.5s ease;
/* Fade to solid text on dismiss — keep the gradient until fill is opaque */
&[data-shimmer-fading] {
-webkit-text-fill-color: currentColor;
animation-play-state: paused;
}
@variant dark {
--_highlight: var(
--shimmer-color,
oklch(from currentColor max(0.8, calc(l + 0.4)) c h / calc(alpha + 0.4))
);
}
&:where([dir='rtl'], [dir='rtl'] *) {
animation-direction: reverse;
}
}
@utility shimmer-once {
animation-iteration-count: 1;
}
@utility shimmer-reverse {
animation-direction: reverse;
}
@utility shimmer-none {
--shimmer-image: none;
--shimmer-text-fill: currentColor;
background-image: none;
-webkit-text-fill-color: currentColor;
}
@utility shimmer-color-* {
--shimmer-color: --value(--color-*, [color]);
--shimmer-color: color-mix(
in oklch,
--value(--color-*, [color]) calc(--modifier(integer) * 1%),
transparent
);
}
@utility shimmer-duration-* {
--shimmer-duration: calc(--value(integer) * 1ms);
}
@utility shimmer-spread-* {
--shimmer-spread: calc(var(--spacing) * --value(integer));
--shimmer-spread: --value([length], [percentage]);
}
@utility shimmer-angle-* {
--shimmer-angle: calc(--value(integer) * 1deg);
}
@media (prefers-reduced-motion: reduce) {
.shimmer {
animation: none;
background-image: none;
-webkit-text-fill-color: currentColor;
}
}
/*
* Canonical keyboard focus rings. Prefer these over inventing local ring/outline
* stacks.
*
* - focus-ring: default offset ring (buttons, inputs, most controls).
* `outline-hidden` stays always-on so mouse click does not show the browser
* default; the ring is `:focus-visible` only.
* - focus-inset: dense/flush surfaces such as interactive <tr>s. Must use
* outline (not ring/box-shadow) — box-shadow on table-row is unreliable,
* especially in Safari. Do NOT pair with outline-hidden: that sets
* outline-style: none and suppresses the focus outline.
*/
@utility focus-ring {
@apply outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background;
}
@utility focus-inset {
/* Reserve outline geometry up front so focus never flashes the browser default. */
outline: 2px solid transparent;
outline-offset: -2px;
/* Call sites often add `transition`/`transition-all`; keep outline instant. */
transition-property:
color, background-color, border-color, text-decoration-color, fill, stroke, opacity, transform,
filter, backdrop-filter, box-shadow;
&:focus-visible {
outline-color: var(--ring);
border-radius: var(--radius-md, 0.375rem);
}
}