mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
## 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 -->
268 lines
7.9 KiB
CSS
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);
|
|
}
|
|
}
|