一键导入
jem-ui-patterns
Guide for composing @jem-open/jem-ui components into app-level UI patterns — forms, data views, modals, navigation, and feedback.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Guide for composing @jem-open/jem-ui components into app-level UI patterns — forms, data views, modals, navigation, and feedback.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Release workflow for when you are ready to ship. Bumps the version file, creates a release branch, publishes a GitHub release with auto-generated notes, and opens PRs into the base branch and main. Both PRs are auto-merged by default once all branch protection requirements are satisfied; pass --no-auto-merge to leave them open.
Use when a code analysis tool reports findings and you want an automated fix-verify loop — runs the analysis command, dispatches parallel subagents to fix all findings, verifies with project lint and tests, and loops until zero findings remain.
Jem brand guidelines — the single source of truth for colours, typography, tone of voice, logo usage, iconography, and visual identity. Use this skill whenever creating any content, collateral, code, or communication that needs to be on-brand for Jem. Trigger when someone asks to "check brand guidelines", "use Jem colours", "write in Jem's voice", "what font does Jem use", "stay on brand", or any request involving Jem's visual identity, tone, or brand standards. Also use when reviewing content for brand consistency, building branded templates, choosing colours or fonts for Jem assets, or when another skill needs brand context. This is a reference skill — it provides the rules, not the execution. For creating visual collateral, use the jem-collateral skill. For reviewing content voice, use the brand-voice skill.
Enforces safe secret handling using 1Password CLI and secret references (op:// URIs) during local development. Use this skill whenever a coding session involves secrets, API keys, tokens, credentials, database connection strings, environment variables with sensitive values, .env files, or any authentication/authorization configuration. Also trigger when the user asks you to hit an API endpoint, test a webhook, run a curl command with auth, or access any external service that requires credentials. If you are about to run any command that involves a secret value — stop and consult this skill first. The core rule: never resolve, print, log, store, or expose a secret value in the terminal, shell history, files, or your own context. Use op:// references and op run exclusively.
Copy-paste-ready code blocks for common pages and features built with @jem-open/jem-ui — search tables, CRUD forms, settings pages, and more.
Complete reference for using @jem-open/jem-ui components — props, variants, design tokens, setup, and common mistakes to avoid.
| name | jem-ui-patterns |
| description | Guide for composing @jem-open/jem-ui components into app-level UI patterns — forms, data views, modals, navigation, and feedback. |
Guide for AI agents composing @jem-open/jem-ui components into app-level UI patterns. Covers how to combine components for forms, data views, modals, drawers, navigation, and feedback — with correct wiring, accessibility, and state management.
Verify the consuming project is set up to use jem-ui.
Check 1: Package installed
Look for @jem-open/jem-ui in package.json dependencies or devDependencies.
If not found, install it:
npm install @jem-open/jem-ui
Peer dependencies required: react@^18 | ^19, react-dom@^18 | ^19, tailwindcss@^3.4.
Check 2: Styles imported
The app entry point (e.g. layout.tsx, _app.tsx, or main.tsx) must import jem-ui styles:
import "@jem-open/jem-ui/styles.css"
If missing, add it.
Check 3: Tailwind preset configured
tailwind.config.js (or .ts) must include the jem-ui preset and content path:
const jemPreset = require("@jem-open/jem-ui/tailwind-preset");
module.exports = {
presets: [jemPreset],
content: [
"./src/**/*.{ts,tsx}",
"./node_modules/@jem-open/jem-ui/dist/**/*.{js,mjs}",
],
};
If missing, add both the preset and the content path.
Halt if any check fails and cannot be auto-fixed.
Use the table below to match the user's goal to a pattern, then follow the corresponding subsection in Step 3.
| Pattern | When to use | Key components |
|---|---|---|
| Form layout | Collecting user input with validation | InputField, Select, Checkbox, RadioGroup, Button, Label |
| Data view | Displaying searchable/filterable tabular data | DataTable, DataTableColumnHeader, SearchInput, Tag, Pagination, EmptyState |
| Modal workflow | Confirming actions, editing records, multi-step flows | Dialog, DialogContent, DialogHeader, DialogFooter, Button, form components |
| Drawer panel | Side panels for detail views, editing, or creation | Drawer, DrawerContent, DrawerHeader, DrawerBody, DrawerFooter, Button |
| Navigation structure | Page sections, hierarchical nav, collapsible content | Tabs, TabsList, TabsTrigger, TabsContent, Breadcrumb, Accordion |
| Feedback & notifications | Alerts, toasts, empty states, tooltips | Alert, Toaster, EmptyState, Tooltip, TooltipProvider |
Composition:
<form> elementInputField (not raw Input) for automatic label associationSelectField (not raw Select) for labeled selectsgap-md (24px)gap-xs between themerror prop on fields to validation stateAccessibility: Each input must have a label. InputField/SelectField/CheckboxWithLabel handle this automatically. For custom layouts, use Label with htmlFor.
import { InputField, SelectField, Select, SelectTrigger, SelectContent, SelectItem, SelectValue, CheckboxWithLabel, Button } from "@jem-open/jem-ui"
function UserForm({ onSubmit }: { onSubmit: (data: FormData) => void }) {
const [errors, setErrors] = useState<Record<string, string>>({})
return (
<form onSubmit={handleSubmit} className="flex flex-col gap-md">
<InputField
label="Full name"
name="name"
placeholder="Enter your name"
error={!!errors.name}
helperText={errors.name}
/>
<InputField
label="Email"
name="email"
type="email"
placeholder="name@example.com"
error={!!errors.email}
helperText={errors.email}
/>
<SelectField label="Role" description="Determines permissions">
<Select name="role">
<SelectTrigger>
<SelectValue placeholder="Select a role" />
</SelectTrigger>
<SelectContent>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="editor">Editor</SelectItem>
<SelectItem value="viewer">Viewer</SelectItem>
</SelectContent>
</Select>
</SelectField>
<CheckboxWithLabel
label="Send welcome email"
description="User will receive an onboarding email"
/>
<div className="flex gap-xs justify-end">
<Button variant="outline" type="button">Cancel</Button>
<Button variant="primary" type="submit">Create user</Button>
</div>
</form>
)
}
Composition:
DataTable with typed ColumnDef arrayDataTableColumnHeader in sortable column headersTag for status columnsSearchInput for filtering (connect to DataTable's filterColumn)showPagination={true} (default)EmptyState when data array is empty (check before rendering DataTable)Accessibility: DataTable renders semantic <table> elements with proper headers. Column headers announce sort state.
import { DataTable, DataTableColumnHeader, Tag, EmptyState } from "@jem-open/jem-ui"
import { ColumnDef } from "@tanstack/react-table"
type Order = {
id: string
customer: string
status: "completed" | "pending" | "failed"
total: number
}
const columns: ColumnDef<Order>[] = [
{
accessorKey: "id",
header: ({ column }) => <DataTableColumnHeader column={column} title="Order ID" />,
},
{
accessorKey: "customer",
header: ({ column }) => <DataTableColumnHeader column={column} title="Customer" />,
},
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => {
const status = row.getValue("status") as string
const variant = status === "completed" ? "success" : status === "pending" ? "pending" : "failed"
return <Tag variant={variant}>{status}</Tag>
},
},
{
accessorKey: "total",
header: ({ column }) => <DataTableColumnHeader column={column} title="Total" />,
cell: ({ row }) => `$${(row.getValue("total") as number).toFixed(2)}`,
},
]
function OrdersPage({ orders }: { orders: Order[] }) {
if (orders.length === 0) {
return (
<EmptyState
icon="inbox"
title="No orders yet"
description="Orders will appear here once customers place them"
variant="card"
/>
)
}
return (
<DataTable
columns={columns}
data={orders}
filterColumn="customer"
filterPlaceholder="Search customers..."
/>
)
}
Composition:
Dialog as the rootDialogTrigger wraps the button that opens the modal (use asChild)DialogContent contains the modal bodyDialogHeader with DialogTitle (required for accessibility) and DialogDescriptionDialogFooter for action buttonsDialogClose wraps the cancel button (use asChild)DialogTitle variant="error"Accessibility: Dialog traps focus, DialogTitle is announced by screen readers, Escape key closes the dialog.
import {
Dialog, DialogTrigger, DialogContent, DialogHeader,
DialogTitle, DialogDescription, DialogFooter, DialogClose
} from "@jem-open/jem-ui"
import { Button, InputField } from "@jem-open/jem-ui"
function EditProfileDialog() {
const [name, setName] = useState("")
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Edit profile</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit profile</DialogTitle>
<DialogDescription>Update your display name and preferences.</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-md py-md">
<InputField
label="Display name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button variant="primary" onClick={handleSave}>Save changes</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
Composition:
Drawer with direction prop (default: "right")DrawerTrigger opens the drawer (use asChild)DrawerContent is the panel containerDrawerHeader has pink background, title, and close buttonDrawerBody is scrollable content area (padding included)DrawerFooter sticks to the bottom with a top borderDrawerSection to group content blocks within DrawerBodyAccessibility: Drawer traps focus when open, close button in header, Escape key closes.
import {
Drawer, DrawerTrigger, DrawerContent, DrawerHeader,
DrawerBody, DrawerFooter, DrawerTitle, DrawerClose
} from "@jem-open/jem-ui"
import { Button, Avatar, AvatarFallback, Tag, Divider } from "@jem-open/jem-ui"
function UserDetailDrawer({ user }: { user: User }) {
return (
<Drawer direction="right">
<DrawerTrigger asChild>
<Button variant="ghost">View details</Button>
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>{user.name}</DrawerTitle>
</DrawerHeader>
<DrawerBody>
<div className="flex items-center gap-md mb-md">
<Avatar size="lg">
<AvatarFallback size="lg">{user.initials}</AvatarFallback>
</Avatar>
<div>
<p className="text-greyscale-text-title font-semibold">{user.name}</p>
<p className="text-greyscale-text-caption">{user.email}</p>
</div>
</div>
<Divider spacing="md" />
<div className="flex flex-col gap-sm">
<div className="flex justify-between">
<span className="text-greyscale-text-subtitle">Status</span>
<Tag variant="success">{user.status}</Tag>
</div>
<div className="flex justify-between">
<span className="text-greyscale-text-subtitle">Role</span>
<span className="text-greyscale-text-body">{user.role}</span>
</div>
</div>
</DrawerBody>
<DrawerFooter>
<Button variant="primary">Edit user</Button>
<DrawerClose asChild>
<Button variant="outline">Close</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
)
}
Composition:
Tabs for page sections (default variant for card-style, line variant for minimal)Breadcrumb for hierarchical page navigationAccordion for collapsible sections within a pageAccessibility: Tabs use arrow keys for navigation, Accordion uses Enter/Space to toggle.
import {
Tabs, TabsList, TabsTrigger, TabsContent,
Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink,
BreadcrumbPage, BreadcrumbSeparator
} from "@jem-open/jem-ui"
function SettingsPage() {
return (
<div className="flex flex-col gap-md">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/">Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Settings</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<Tabs defaultValue="general">
<TabsList variant="line">
<TabsTrigger variant="line" value="general">General</TabsTrigger>
<TabsTrigger variant="line" value="security">Security</TabsTrigger>
<TabsTrigger variant="line" value="notifications">Notifications</TabsTrigger>
</TabsList>
<TabsContent value="general">
{/* General settings form */}
</TabsContent>
<TabsContent value="security">
{/* Security settings form */}
</TabsContent>
<TabsContent value="notifications">
{/* Notification preferences */}
</TabsContent>
</Tabs>
</div>
)
}
Composition:
Alert for inline, persistent messages (validation summaries, warnings)Toaster + toast() for transient notifications (success confirmations, errors)EmptyState for no-data states (empty lists, no search results)Tooltip for supplementary info on icons/buttonsKey rules:
<Toaster /> once in root layout<TooltipProvider> oncesuccess, warning, destructive, notetoast.success(), toast.error(), toast.info(), toast.warning()import { Alert, AlertTitle, AlertDescription, EmptyState, Tooltip, TooltipTrigger, TooltipContent } from "@jem-open/jem-ui"
import { Button, IconButton } from "@jem-open/jem-ui"
import { toast } from "sonner"
import { Info } from "lucide-react"
// Inline alert for form validation
<Alert variant="destructive">
<AlertTitle>Validation failed</AlertTitle>
<AlertDescription>Please fix the errors below before submitting.</AlertDescription>
</Alert>
// Toast for async operation result
async function handleSave() {
try {
await saveData()
toast.success("Changes saved successfully")
} catch {
toast.error("Failed to save changes")
}
}
// Empty state when no data
<EmptyState
icon="inbox"
title="No messages"
description="You're all caught up"
primaryAction={{ label: "Compose", onClick: openCompose }}
/>
// Tooltip for icon button
<Tooltip>
<TooltipTrigger asChild>
<IconButton icon={<Info />} aria-label="More information" />
</TooltipTrigger>
<TooltipContent>Click to learn more about this feature</TooltipContent>
</Tooltip>
value + onChange on each fieldconst [errors, setErrors] = useState<Record<string, string>>({})error={!!errors.fieldName} and helperText={errors.fieldName}loading prop on submit Button during async operationsdata array — DataTable re-rendersdata.length === 0 before rendering DataTableopen and onOpenChange props on DialogsetOpen(false) in success callbackonOpenChange to clear stateopen + onOpenChange on Drawervalue + onValueChange, or uncontrolled with defaultValuetype="single" + collapsible for FAQ-style, type="multiple" for settingstoast.success("Message") — no state management needed{showWarning && <Alert>...})Run through this checklist before considering the UI complete:
gap-md, p-sm), not arbitrary values (gap-6, p-4)loading prop during async, Skeleton while data loads)error + helperText, Alert for form-level errors, toast.error for async failures)cn() from @jem-open/jem-ui, not template literalsasChild on Button/DialogTrigger/DrawerTrigger instead)TooltipProvider is present in the layout (if using Tooltip)Toaster is added once in the root layout (if using toast)