Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .claude/skills/react-hook-form/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,12 @@ schema, `onChange`, rendering, and the submit mapping.

- Gate Save on `isDirty`; show Cancel only when dirty. In the owner, destructure
from `form.formState`; anywhere else, `useFormState({ control })`.
- When the form lives in a Sheet or Dialog, also wire dirty dismissal:
`useConfirmOnClose` + `DiscardChangesConfirmationDialog`. Route Cancel,
Escape, and backdrop through the guard; call the raw `onClose` on successful
submit so you do not prompt after save. Details:
`apps/design-system/content/docs/ui-patterns/modality.mdx` (Dirty form
dismissal) and the studio-ui-patterns skill Sheets section.
- To show _which_ fields changed (review/summary dialogs), read `dirtyFields`
from the same subscription instead of hand-comparing
`defaultValues.x !== watchedX`. RHF already does that comparison correctly;
Expand Down
4 changes: 4 additions & 0 deletions .claude/skills/studio-ui-patterns/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ Forms in sheets:

- `layout="horizontal"` for wider sheets
- `layout="vertical"` for narrow sheets (`size="sm"` or below)
- When the sheet contains a form, wire dirty dismissal with `useConfirmOnClose` +
`DiscardChangesConfirmationDialog` (Cancel, Escape, and backdrop). Source of
truth: `apps/design-system/content/docs/ui-patterns/modality.mdx` (Dirty form
dismissal). Also see the react-hook-form skill for `isDirty` destructuring.

## Copy

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,12 +165,17 @@ the pair should use the dark set together.

**Where logos come from on `/authorize`**

- Curated partner logos resolve from allowlisted `redirect_uri` hosts only,
not from self-asserted `name` or `website`. Those pairs may use theme tiles
and dark assets when the partner has them.
- Curated partner logos resolve from allowlisted `redirect_uri` hosts, or from
a trusted partner name when `redirect_uri` is localhost / loopback (local MCP
clients). Do not resolve curated logos from self-asserted `name` or `website`
on a remote host. Those pairs may use theme tiles and dark assets when the
partner has them.
- Published organisation OAuth app icons uploaded in Studio remain trusted
remote images, paired with forced-light tiles on both sides.
- Everything else falls back to `SupabaseLogo` alone.
- If the requester name looks like a known partner but `redirect_uri` is a
remote host outside that partner's allowlist, show a caution admonition.
Localhost MCP redirects are excluded.

## Account row

Expand Down Expand Up @@ -217,20 +222,14 @@ Match feedback to its scope:
feedback for a failure the user needs to resolve on the current card.

```tsx
{
actionError && (
<div className="mt-3 border-t border-muted pt-5">
<p role="alert" className="text-center text-xs text-destructive text-balance">
{actionError}
</p>
</div>
)
}
<InterstitialActionError error={actionError} />
```

Clear stale action feedback when the user retries or changes a relevant
selection. Error copy should say what failed and, when it is not obvious, what
the user can do next.
the user can do next. When passive supporting copy occupies the same footer
region, replace it with the action error until the error is cleared instead of
stacking both messages.

<ComponentPreview
name="connect-interstitial-action-error"
Expand Down
2 changes: 1 addition & 1 deletion apps/design-system/content/docs/ui-patterns/forms.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Build a custom row when the cells are mixed controls, such as an input paired wi

4. **Use Cards for grouping**: Wrap form sections in `Card` components with `CardContent` and `CardFooter` for actions.

5. **Handle dirty state**: Show cancel buttons and disable save buttons based on `form.formState.isDirty`. Make sure you destructure `isDirty` from `form.formState` (see https://react-hook-form.com/docs/useform/formstate)
5. **Handle dirty state**: Show cancel buttons and disable save buttons based on `form.formState.isDirty`. Make sure you destructure `isDirty` from `form.formState` (see https://react-hook-form.com/docs/useform/formstate). When the form is in a dialog or sheet, also follow [Dirty form dismissal](./modality#dirty-form-dismissal) so Cancel, Escape, and backdrop ask before discarding unsaved changes.

6. **Error handling**: Match feedback to its scope. Use `FormMessage` or `FieldError` for field validation. Show submission failures inline near the form actions when the user needs to retry or change something. Reserve toasts for non-blocking feedback or completed operations whose originating surface is no longer visible.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Button } from 'ui'

import {
AccountRow,
InterstitialActionError,
InterstitialShell,
LogoPair,
StripeLogo,
Expand All @@ -24,11 +25,7 @@ export default function ConnectInterstitialActionError() {
<Button variant="text" block>
Cancel
</Button>
<div className="mt-3 border-t border-muted pt-5">
<p role="alert" className="text-center text-xs text-destructive text-balance">
Failed to authorize Stripe Projects. Please try again.
</p>
</div>
<InterstitialActionError error="Failed to authorize Stripe Projects. Please try again." />
</div>
</div>
</InterstitialShell>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,18 @@ export function InterstitialShell({
)
}

export function InterstitialActionError({ error }: { error?: React.ReactNode }) {
if (!error) return null

return (
<div className="mt-3 border-t border-muted pt-5">
<p role="alert" className="text-center text-xs text-destructive text-balance">
{error}
</p>
</div>
)
}

export function SignOutButton() {
return <Button variant="default" icon={<LogOut />} className="px-2" aria-label="Sign out" />
}
2 changes: 1 addition & 1 deletion apps/docs/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ Run `pnpm test:local lib/content-listings.test.ts` from apps/docs.
**Manually add content listings:**

1. Add or update a `ContentListingGroup` export in [`data/content-listings/[topic].data.ts`](data/content-listings/). The `id` field must be globally unique across all listing groups. For example, use `storage-get-started` rather than `get-started`. The ID is both the lookup key and the telemetry `listingId`.
2. Place the component inline in guide MDX, for example `<ContentListings id="storage-get-started" />`. Use a partial only when the block is reused or gated with `$Show` at the partial level.
2. Place the component inline in guide MDX, for example `<ContentListings id="storage-get-started" />`. Use a partial only when the block is reused or gated with `$Show` at the partial level. For individual items that depend on a feature flag (for example `sdk:dart`), set `feature` on the item instead of wrapping the whole listing.
3. Run `pnpm test:local lib/content-listings.test.ts` from `apps/docs`.

Code snippets for manually adding content listings are available in [`.vscode/content-listing.code-snippets`](../../.vscode/content-listing.code-snippets). Use `cl-data` for a data export with a namespaced ID. Use `cl-inline` for an MDX component.
Expand Down
12 changes: 9 additions & 3 deletions apps/docs/components/ContentListings/ContentListings.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

import type { ContentListingGroup, ContentListingItem } from '~/lib/content-listings.schema'
import {
filterContentListingItems,
getContentListingById,
getContentListingGroupLabel,
isExternalContentListingHref,
} from '~/lib/content-listings.utils'
import { useSendTelemetryEvent } from '~/lib/telemetry'
import Link from 'next/link'
import { useCallback } from 'react'
import { useCallback, useMemo } from 'react'
import { Badge } from 'ui'
import { GlassPanel } from 'ui-patterns/GlassPanel'
import { Heading } from 'ui/src/components/CustomHTMLElements'
Expand Down Expand Up @@ -52,10 +53,13 @@ function ContentListingGroupHeading({ group }: { group: ContentListingGroup }) {

function ContentListingsGroup({ group }: { group: ContentListingGroup }) {
const { trackClick } = useContentListingClickHandler(group)
const items = useMemo(() => filterContentListingItems(group.items), [group.items])
const isGrid = group.type === 'grid'
const listClassName = isGrid ? 'grid md:grid-cols-12 gap-4' : 'list-disc pl-6 space-y-2'
const gridItemClassName = isGrid ? GRID_ITEM_CLASS[group.columns ?? 3] : undefined

if (!items.length) return null

// Heading stays outside `not-prose` so it inherits the surrounding MDX prose
// typography. The list itself opts out so its explicit Tailwind layout wins.
return (
Expand All @@ -64,7 +68,7 @@ function ContentListingsGroup({ group }: { group: ContentListingGroup }) {
<div className="not-prose space-y-4">
{group.description && <p className="text-foreground-light">{group.description}</p>}
<ul className={listClassName}>
{group.items.map((item) => {
{items.map((item) => {
const external = isExternalContentListingHref(item.href)
const key = `${group.id}-${item.href}`

Expand All @@ -77,6 +81,7 @@ function ContentListingsGroup({ group }: { group: ContentListingGroup }) {
className="block h-full"
onClick={() => trackClick(item)}
target={external ? '_blank' : undefined}
rel={external ? 'noopener noreferrer' : undefined}
>
<GlassPanel
title={item.title}
Expand Down Expand Up @@ -106,6 +111,7 @@ function ContentListingsGroup({ group }: { group: ContentListingGroup }) {
href={item.href}
onClick={() => trackClick(item)}
target={external ? '_blank' : undefined}
rel={external ? 'noopener noreferrer' : undefined}
>
<strong>{item.title}</strong>: {item.description}
</Link>
Expand All @@ -120,7 +126,7 @@ function ContentListingsGroup({ group }: { group: ContentListingGroup }) {

export function ContentListings({ id }: { id: string }) {
const group = getContentListingById(id)
if (!group || !group.items.length) return null
if (!group || !filterContentListingItems(group.items).length) return null

return (
<div className="my-10 space-y-10">
Expand Down
Loading
Loading