Files
supabase/apps/docs/features/ui/IconLink.tsx
Danny White 3b06c6c7cc fix(docs): unify docs card hover and retire IconPanel (#48379)
## What kind of change does this PR introduce?

Bug fix / docs UI polish.

## What is the current behavior?

- Many docs `GlassPanel`s use `background={false}`, so hover only tweaks
the border and reads as having no hover state
- Compact icon+label grids still use `IconPanel`, which has a broken
`-z-10` hover fill and overlaps with the newer `IconLink` pattern
- Description card grids jump to 3-up too early on medium widths

## What is the new behavior?

**GlassPanel**
- Removes the `background` prop; cards always use the filled surface
with stronger border hover
- Tightens icon→description gap (`gap-6` → `gap-3`)
- Decorative icons/logos use empty `alt` so screen readers don’t hear
the title twice

**Icon tiles**
- Retires `IconPanel` from docs and deletes it from `ui-patterns`
- Uses `IconLink` / `IconLinkList` for compact navigation tiles (auth
providers, social login, etc.)
- Adds `IconLinkButton` for SMS provider pickers (same chrome, opens a
dialog)
- Adds focus styles, list labelling, and dialog-trigger ARIA where
needed

**Layout / content**
- Migrate-to-Supabase description cards on resources use `GlassPanel`
(not slim icon tiles)
- Grid spans use `md:… xl:…` so cards stay 2-up until ~1280px
- Fixes migrate links to `/guides/platform/migrating-to-supabase/…` and
SSR quickstarts to `creating-a-client` with framework query params
- Moves the Extensions list `key` onto the outer `Link`

| Before | After |
| --- | --- |
| <img width="1185" height="1323" alt="Resources Supabase Docs"
src="https://github.com/user-attachments/assets/1677bf65-d3a3-4202-8c70-e758f7c3bcce"
/> | <img width="1185" height="1323" alt="Resources Supabase Docs"
src="https://github.com/user-attachments/assets/51760f0f-62b6-4010-9841-de26039f37b4"
/> |

## Additional context

Homepage compact sections already use `IconLinkList` from #48317; this
PR finishes that pattern for remaining docs `IconPanel` callsites and
cleans up GlassPanel hover.

`www/customers` only drops the removed `background` prop; those cards
already use the filled surface via `logo`.

## Test plan

- [ ] `/guides/getting-started`: GlassPanels show filled surface and
clearer border hover
- [ ] `/guides/resources`: migrate cards are GlassPanels with working
`/platform/…` links; 2-up until xl
- [ ] `/guides/auth/social-login` and auth providers partial: IconLink
tiles hover/focus correctly
- [ ] `/guides/auth/phone-login`: SMS provider buttons open dialogs;
keyboard focus works
- [ ] Docs homepage: migrate / self-host IconLinkLists unchanged in
behaviour
- [ ] `/guides/auth/server-side`: Next.js / SvelteKit cards resolve on
docs preview

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

## Summary by CodeRabbit

* **Improved Layouts**
* Made “GlassPanel” card grids more responsive and consistent; refined
card and success badge spacing for a cleaner presentation.
* **Updated Documentation**
* Refreshed multiple guide and resource pages (including quickstarts and
migration content) with standardized card layouts and updated link
destinations.
* **Component Updates**
* Standardized “GlassPanel” styling (background toggle removed) and
simplified icon-based panels; added an `IconLinkButton` for action
tiles; updated authentication provider grids to use the shared tile UI.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-31 06:34:05 +10:00

160 lines
3.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import MenuIconPicker from '~/components/Navigation/NavigationMenu/MenuIconPicker'
import Link from 'next/link'
import { type ButtonHTMLAttributes, type ReactNode } from 'react'
import { cn } from 'ui'
const sizeStyles = {
sm: {
tile: 'h-10 w-10',
image: 'w-5',
},
lg: {
tile: 'h-10 w-10 sm:h-16 sm:w-16',
image: 'w-5 sm:w-8',
},
} as const
export type IconLinkSize = keyof typeof sizeStyles
export type IconLinkItem = {
title: string
href: string
icon: ReactNode
className?: string
}
const iconLinkClassName =
'group relative -m-3 flex items-center gap-3 rounded-xl p-3 no-underline transition-colors hover:bg-accent focus-ring focus-visible:bg-accent'
function IconLinkContent({
title,
icon,
size = 'sm',
}: {
title: string
icon: ReactNode
size?: IconLinkSize
}) {
return (
<>
<div
className={cn(
'flex shrink-0 items-center justify-center overflow-hidden rounded-lg border bg-surface-100 transition-colors group-hover:border-strong group-focus-visible:border-strong',
sizeStyles[size].tile
)}
>
{icon}
</div>
<span className="text-base text-foreground">{title}</span>
</>
)
}
export function IconLink({
href,
title,
icon,
size = 'sm',
className,
}: {
href: string
title: string
icon: ReactNode
size?: IconLinkSize
className?: string
}) {
return (
<Link href={href} className={cn(iconLinkClassName, className)}>
<IconLinkContent title={title} icon={icon} size={size} />
</Link>
)
}
export function IconLinkButton({
title,
icon,
size = 'sm',
className,
disabled,
tabIndex,
...props
}: {
title: string
icon: ReactNode
size?: IconLinkSize
className?: string
} & ButtonHTMLAttributes<HTMLButtonElement>) {
return (
<button
type="button"
disabled={disabled}
tabIndex={tabIndex ?? (disabled ? -1 : 0)}
className={cn(iconLinkClassName, 'w-full text-left', className)}
{...props}
>
<IconLinkContent title={title} icon={icon} size={size} />
</button>
)
}
export function IconLinkList({
items,
labelledBy,
label,
className,
itemClassName = 'col-span-6 md:col-span-4',
size = 'sm',
}: {
items: IconLinkItem[]
/** Prefer when a visible heading is the lists name. */
labelledBy?: string
/** Prefer when the nearby heading is section/setup copy, not a list title. */
label?: string
className?: string
itemClassName?: string
size?: IconLinkSize
}) {
return (
<ul
className={cn('grid grid-cols-12 gap-6', className)}
aria-labelledby={labelledBy}
aria-label={label}
>
{items.map((item) => (
<li key={item.href} className={cn(itemClassName, item.className)}>
<IconLink href={item.href} title={item.title} icon={item.icon} size={size} />
</li>
))}
</ul>
)
}
export function IconLinkImage({
path,
hasLightIcon,
size = 'sm',
}: {
path: string
hasLightIcon?: boolean
size?: IconLinkSize
}) {
const imgClassName = sizeStyles[size].image
return (
<>
{hasLightIcon ? (
<img src={`${path}-light.svg`} alt="" className={cn(imgClassName, 'dark:hidden')} />
) : null}
<img
src={`${path}.svg`}
alt=""
className={cn(imgClassName, hasLightIcon && 'hidden dark:block')}
/>
</>
)
}
export function IconLinkMenuIcon({ icon }: { icon: string }) {
return <MenuIconPicker icon={icon} width={18} height={18} />
}