Files
supabase/apps/docs/components/Extensions/Extensions.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

135 lines
4.1 KiB
TypeScript

import { X } from 'lucide-react'
import Link from 'next/link'
import React, { useState } from 'react'
import { extensions } from 'shared-data'
import { Badge, Input } from 'ui'
import { GlassPanel } from 'ui-patterns/GlassPanel'
type Extension = {
name: string
comment: string
tags: string[]
link: string
}
type LinkTarget = React.ComponentProps<'a'>['target']
function getLinkTarget(link: string): LinkTarget {
// Link is relative, open in the same tab
if (link.startsWith('/')) {
return '_self'
}
// Link is external, open in a new tab
return '_blank'
}
function getUniqueTags(json: Extension[]): string[] {
const tags: string[] = []
for (const item of json) {
if (item.tags) {
tags.push(...item.tags)
}
}
return [...new Set(tags)]
}
export default function Extensions() {
const [searchTerm, setSearchTerm] = useState<string>('')
const [filters, setFilters] = useState<string[]>([])
const tags = getUniqueTags(extensions)
function handleChecked(tag: string) {
if (filters.includes(tag)) {
setFilters(filters.filter((x) => x !== tag))
} else {
setFilters([...filters, tag])
}
}
return (
<>
<div className="mb-8 grid">
<label className="mb-2 text-xs text-foreground-light">Search extensions</label>
<Input
type="text"
placeholder="Extension name"
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<div className="lg:grid lg:grid-cols-12">
<div className="col-span-3 not-prose">
<div className="lg:sticky top-32">
<h3 className="text-sm text-foreground-light">Filter</h3>
<ul className="mt-3 flex flex-wrap lg:grid gap-2 grow">
{tags.sort().map((tag) => (
<li key={tag}>
<label
htmlFor={tag}
className={`text-sm text-foreground-lighter py-0.5 px-2 capitalize inline-block rounded-lg hover:bg-surface-100 cursor-pointer border ${
filters.includes(tag) ? 'bg-surface-100 ' : ''
}`}
>
<span className="flex items-center gap-1">
<input
type="checkbox"
className="sr-only"
id={tag}
name={tag}
value={tag}
onChange={() => handleChecked(tag)}
checked={filters.includes(tag)}
/>
{tag}
<span>{filters.includes(tag) && <X size={12} />}</span>
</span>
</label>
</li>
))}
</ul>
<p className="mt-2">
<button
tabIndex={0}
type="reset"
className="text-xs hover:underline"
onClick={() => setFilters([])}
>
Reset
</button>
</p>
</div>
</div>
<div className="col-span-9 mt-4 lg:mt-0">
<div className="grid gap-4">
{extensions
.filter((x) => x.name.indexOf(searchTerm) >= 0)
.filter((x) =>
filters.length === 0 ? x : x.tags.some((item) => filters.includes(item))
)
.map((extension) => (
<Link
key={extension.name}
href={extension.link}
target={getLinkTarget(extension.link)}
className="no-underline"
>
<GlassPanel title={extension.name}>
<p className="mt-4">
{extension.comment.charAt(0).toUpperCase() + extension.comment.slice(1)}
</p>
{extension.deprecated && (
<Badge variant="destructive">
Deprecated in {extension.deprecated.join(', ')}
</Badge>
)}
</GlassPanel>
</Link>
))}
</div>
</div>
</div>
</>
)
}