File size picker

Multi-select picker for design output sizes with a filter chip bar for Recommended / My File Sizes / My Kits / All. Fetches from a Supabase-backed API route scoped to the given account. The All tab paginates upstream 60 rows at a time and forwards the search box to the API's `query` param so it scales to thousands of rows without loading the whole catalog. Optionally narrow the catalog to a `department` (`design` | `video` | `social` | `web` | `brand`) and/or a list of `projectType` IDs. Selections snapshot the full file-size metadata (dims, fold type, bleed) and folded print sizes annotate their dimensions as unfolded with a bleed badge.

onChange value0 selections
[]
Installation
$Terminal
npx shadcn add https://raw.githubusercontent.com/sis-thesqd/squad-sdk/main/public/r/file-size-picker-block.json
Setup
Configure environment variables and ensure the API route is in place.
TXTSetup
## Setup

1. Set the following environment variable in your `.env.local`:
   - `SQUAD_API_KEY` — your Squad API key (server-only). Used to call `https://api.thesqd.com/v1/prf/file-sizes`. Generate one at [sdk.thesqd.com/settings/api-keys](https://sdk.thesqd.com/settings/api-keys) (employees only).

2. The CLI install drops three Next.js route handlers under `app/api/file-sizes/` that proxy the Squad API with `Authorization: Bearer ${SQUAD_API_KEY}`:

   | File | Method | Squad API endpoint | Purpose |
   |------|--------|---------------------|---------|
   | `route.ts`                         | `GET`   | `GET /v1/prf/file-sizes`                       | List rows. Forwards `account`, `department`, repeating `project_type`, `query`, `recommended`, `list_defaults`, `limit`, and `offset` upstream; returns `{ rows, total }`. Rows include `foldType` (`tri-fold` / `bi-fold` / `no-fold` / `custom`) and `bleedSize` (`0.125` / `0.25`) when set upstream. The block uses `limit=60` + `offset` for the All tab, `recommended=true` for the Recommended tab, `list_defaults=false` for My File Sizes, and a single un-paginated catalog fetch for Kits. |
   | `route.ts`                         | `POST`  | `POST /v1/prf/file-sizes`                      | Create a new file size. The block sends `account: <currentAccount>` so the row is owned, not global. |
   | `[id]/route.ts`                    | `PATCH` | `PATCH /v1/prf/file-sizes/{id}`                | Edit an account-owned row. |
   | `[id]/visibility/route.ts`         | `PATCH` | `PATCH /v1/prf/file-sizes/{id}/visibility`     | Hide a row for the current account (the soft-delete — Squad API has no DELETE). |

3. **Editing a global row** (`account === null`) is special: the block POSTs a new account-owned copy with the user's edits applied instead of mutating the global. That keeps the global record intact for everyone else while still feeling like an in-place edit to the user.
Usage
Drop in the demo component or wire up the picker with your own state.
TSImport
import { FileSizePicker, type FileSize, type FileSizeSelection } from "@/components/blocks/file-size-picker-block";
TSExample
"use client";

import * as React from "react";
import {
  FileSizePicker,
  type FileSizeSelection,
} from "@/components/blocks/file-size-picker-block";

export default function Page() {
  // `account` scopes the picker to the customer's own file sizes.
  // Pass `readOnly` to skip mutating fetches (useful in docs/demos).
  // Pass `value` + `onChange` to control selection externally; omit both
  // to let the picker manage its own selection state.
  //
  // Use `department` ("design" | "video" | "social" | "web" | "brand")
  // and/or `projectType` (integer[]) to narrow the catalog to file sizes
  // linked to those project types upstream.
  //
  // Each selection snapshots the full file-size metadata at selection time
  // (`{ id, label, width, height, unit, foldType, bleedSize, ... }`) so
  // downstream consumers (review pages, submissions, cross-field logic)
  // can render and branch on it without re-fetching the catalog.
  //
  // In a dynamic form, gate the initial fetches on the form's prefetch
  // flag via `scopeReady` (defaults to true for standalone usage).
  const [selected, setSelected] = React.useState<FileSizeSelection[]>([]);
  return (
    <FileSizePicker
      account={3728}
      department="design"
      projectType={[1, 7]}
      value={selected}
      onChange={setSelected}
    />
  );
}
Default
Full grid of all account and global file sizes with Recommended / My File Sizes / My Kits / All filter chips and a removable-badge summary. Each tile shows a Printer icon for print sizes and a Monitor icon for digital ones.
onChange value0 selections
[]
Catalog-scoped Type chips (print-only)
The "New file size" modal scopes its Print / Digital chips to the types present in the currently shown options. This catalog only contains print sizes, so opening the + modal disables Digital and defaults the chip to Print — and vice versa for a digital-only catalog. Both stay enabled when the catalog mixes types. After creating, the new size is auto-selected and the picker jumps to My File Sizes.
Add file size in a popover
Pass `createIn="popover"` to anchor the "New file size" form to the + button as a popover instead of a centered modal — it closes on outside-click / Esc. The form, validation, and save behavior are identical; only the surface differs. Editing an existing size still opens the modal.

Nothing to show here yet

Add file size modal (animated open)
Click the + button in the header to open the "New file size" modal. It ships with the transitions-dev "Modal open / close" animation (06-modal.md): the dialog scales up from `--modal-scale` with a soft cross-fade on open and dips back down on close. The animation rides on the Dialog's `.t-modal` class in the shipped `squad-ui.css`, adapted to Base UI's `data-starting-style` / `data-ending-style` transition contract, so any account-scoped picker gets it for free. It honors `prefers-reduced-motion`.
onChange value0 selections
[]
Selection limits (min / max)
Pass `minSelections` (defaults to 1) and an optional `maxSelections`. The cap is a rolling window: picking a new card at the max swaps out the oldest selection instead of being blocked — with a cap of 1, clicking another card simply switches the selection. Here the cap is 3.

Nothing to show here yet