frontend/packages/ui is the shared component library. It is plain source, not a built package: main points at index.ts and apps/saas/next.config.ts lists @repo/ui in transpilePackages. Tailwind picks up its classes through @source "../../../packages/ui/components" in globals.css.
Importing
Both forms work:cn. Most component files start with "use client".
Components
Radix comes from the single
radix-ui package, for example import { Dialog as DialogPrimitive } from "radix-ui".
lib/index.ts exports cn(...inputs), which is twMerge(clsx(inputs)).
Button
Notes:
- The default variant is
secondary, notprimary. State the variant on the main action. secondaryandoutlinerender identically: card surface,border-input, dark text.- All sizes share one radius (
rounded-lg). - A leading SVG gets
me-1.5automatically. A second SVG is hidden. - Focus uses a visible ring (
focus-visible:ring-2).
Dialog
DialogContent is fixed and centered, with max-w-lg, a blurred overlay and a close button at top-4 end-4.
The content grid is grid-cols-[minmax(0,1fr)]. That is deliberate. A grid column defaults to auto minimum width, so one long unbroken string (a URL, a file name) used to push the dialog wider than the viewport. minmax(0, 1fr) lets the column shrink. apps/saas/modules/shared/components/dialog-layout.test.tsx asserts the class is present on both DialogContent and AlertDialogContent.
When you put a Select inside a dialog form, give the trigger w-full so it follows the column.
DialogContent and PopoverContent carry shadow-lg and shadow-md. Floating layers keep a shadow. In page containers do not.
Popover
PopoverContent defaults to align="center", sideOffset={4} and w-72 p-4. It renders in a portal.
Use the shared Popover for floating pickers instead of hand positioned absolute boxes. The folder colour picker in templates/FolderColorPicker.tsx is the reference: a Popover with a fixed track grid of swatches.
Toasts
Toaster is mounted once in the root layout at top-right with a 5 second duration. Toasts are rendered with sonnerToast.custom, so they use the app’s own card markup and tokens.
serverExplanation
lib/server-error.ts extracts a sentence worth showing from a failed core API call:
- Returns
undefinedwhen the error has adigest. That is a production server action error whose message Next.js has replaced. - Reads the status from the
core-api request failed: <status>prefix, or fromerror.status. - Only 4xx failures are quoted. A 5xx or a network failure says nothing useful to a coach and might say too much about the server.
- Strips the prefix and returns the rest when it is between 1 and 400 characters.
Error or an ApiFailure. toastAction calls it for you. Pass errorDetail: false to suppress the detail line.
Form primitives
form.tsx is the shadcn wrapper around react-hook-form. FormField provides the field name through context, FormItem generates ids, FormControl wires aria-describedby and aria-invalid, and FormMessage prints the field error. Usage is in Forms.
Adding a component
components.json configures the shadcn CLI for this package: style default, rsc: true, base colour slate, CSS variables on, Tailwind CSS file ../../tooling/tailwind/theme.css, aliases @/components and @/lib.
shadcn-ui is the package script for pnpm dlx shadcn@latest.
After generating:
- Change Radix imports to the
radix-uipackage form used by the other files. - Replace physical classes with logical ones (
ms-,pe-,start-,end-). - Replace raw colours with tokens (
bg-card,text-muted-foreground,border-border). - Export it from
index.ts. - Run
pnpm check:themefrom the repo root.packages/uiis one of the two themed roots the check scans.
apps/saas/modules/shared/components, not here. See Shared components.
Icons
lucide-react is the icon set, imported by about 250 files.globals.css sets svg.lucide { stroke-width: 1.5 } globally.
react-icons is also installed in saas and used in four files, for brand marks lucide does not have (Apple, Android, WhatsApp) and a few Phosphor glyphs in the phone country picker. Two inline SVGs exist: the Logo and the Google mark in modules/auth/constants/oauth-providers.tsx.