[
  {
    "code": "// ───────────────────────────────────────────────────────────────────────\n// 0) ★ MOST COMMON: <Card> body has NO padding (content is flush against the edges)\n//    Cause: the bare <Card> has ZERO inner padding — it MUST contain <CardContent>.\n//    Don't hand-roll padding with className=\"p-4\" on the Card either.\n// ❌  <Card><Flex direction=\"col\" gap=\"md\">…fields…</Flex></Card>   // flush, no padding\n// ❌  <Card className=\"p-4\">…fields…</Card>                          // hand-rolled padding\n// ✅  <Card><CardContent><Flex direction=\"col\" gap=\"md\">…fields…</Flex></CardContent></Card>\n//    Titles → <CardHeader><CardTitle>. Only go flush deliberately for a full-bleed table:\n// ✅  <Card><CardContent flush><DataTable/></CardContent></Card>\n//    GENERAL RULE — compose godx-ui primitives FULLY; never hand-roll what one ships:\n//      padding → CardContent (not p-4) · controls → Input/Select/Button (not raw <input>/<select>/<button>)\n//      vertical spacing → Flex direction=\"col\" gap (not space-y-*) · empty rows → DataTable's built-in\n//      empty / <EmptyState> (not a custom data.length===0 guard). If a primitive exists, USE it.\n\n// 1) StatCard shows a DOUBLE border (too thick)\n//    Cause: StatCard IS already a bordered Card. Don't wrap it.\n// ❌  <Card><CardContent><StatCard label=\"x\" value=\"1\" /></CardContent></Card>\n// ✅  dashboard only, with comparison/trend:\n//     <ResponsiveGrid columns={4}><StatCard label=\"売上\" value=\"¥8.2M\" delta=\"+12%\" /></ResponsiveGrid>\n//    A list or detail page has NO StatCard row — a row count is not a KPI (Pagination showTotal).\n\n// 2) Badge renders grey with a ○ (no colour) for localized/tier labels\n//    Cause: it auto-maps only English lifecycle keys.\n// ✅  <Badge status=\"active\">公開中</Badge>                       // lifecycle → keep icon\n// ✅  <Badge status=\"プレミアム\" tone=\"neutral\" icon={null} />   // tier → ONE neutral tone\n//    Colour is for status only; a tier/category/scope is plain text or a neutral pill.\n\n// 3) Table text collapses to one char per line, or a chip wraps\n// ✅  give long columns a width:  { key: \"name\", header: \"氏名\", width: \"w-64\" }\n//    (cells + chips are pinned white-space: nowrap by the library.)\n\n// 4) Empty (icon/action) column header shows a blank grey block\n// ✅  header: \"\" renders a transparent cell — leave it empty, don't inject a space.\n\n// 5) DataTable columns are crushed / squeezed\n//    Cause: the table is nested in a narrow grid column.\n// ❌  <ResponsiveGrid columns={3}><div className=\"lg:col-span-2\"><Card><DataTable/></Card></div></ResponsiveGrid>\n// ✅  Table gets its OWN full-width row: <Card><CardContent flush><DataTable/></CardContent></Card>\n\n// 6) Toolbar (filter bar) has no padding (sticks to the edge)\n//    Cause: it's inside CardContent flush (flush strips padding — that's for tables).\n// ❌  <Card><CardContent flush><Toolbar/><DataTable/></CardContent></Card>\n// ✅  <Toolbar/>  then  <Card><CardContent flush><DataTable/></CardContent></Card>\n\n// 7) Sidebar footer looks washed-out / off-design\n//    Cause: raw opacity-*/text-[11px]. Use semantic tokens.\n// ✅  <div className=\"text-muted-foreground text-xs\">\n//        <div className=\"text-foreground font-medium\">{name}</div><div>{role}</div></div>\n\n// 8) Inertia: \"Objects are not valid as a React child {errors, auth, …}\"\n//    Cause: persistent layout passed as a render fn. Use the ARRAY form.\n// ❌  Page.layout = (page) => <Layout>{page}</Layout>\n// ✅  Page.layout = [Layout]            // Layout is ({children}) => ...\n\n// 9) Inertia v3 hydration mismatch (\"server rendered text didn't match the client\")\n//    Cause: Math.random()/argless new Date() during render (SSR ≠ client).\n// ✅  seed deterministically by index, or compute in an event handler.\n\n// 10) Hide a column on mobile / sign-aware KPI delta\n// ✅  columns: [{ key: \"email\", header: \"メール\", hiddenOnMobile: true }]\n// ✅  <StatCard label=\"売上\" value=\"¥8.2M\" delta=\"+12%\" />   // + green / - red; inverse flips",
    "name": "common-fixes",
    "tagline": "Fix the most common @godxjp/ui consumer mistakes & visual bugs (StatCard double-border, grey Badge, crushed/empty table headers, washed-out sidebar footer, Inertia layout crash, SSR hydration). Before → after.",
    "tags": [
      "fixes",
      "migration",
      "bug",
      "cardstat",
      "statusbadge",
      "datatable",
      "sidebar",
      "gotcha",
      "review"
    ]
  },
  {
    "code": "import { useForm } from \"react-hook-form\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { z } from \"zod\";\nimport { Card, CardHeader, CardTitle, CardContent, CardFooter } from \"@godxjp/ui/data-display\";\nimport { FormField, Input } from \"@godxjp/ui/data-entry\";\nimport { Button } from \"@godxjp/ui/general\";\nimport { Flex } from \"@godxjp/ui/layout\";\n\nconst schema = z.object({\n  name: z.string().min(1, \"氏名は必須です\"),\n  email: z.string().email(\"有効なメールアドレスを入力してください\"),\n});\ntype Values = z.infer<typeof schema>;\n\nexport function SignUpCard() {\n  const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<Values>({ resolver: zodResolver(schema) });\n  const onSubmit = handleSubmit(async (v) => {\n    await fetch(\"/api/signup\", { method: \"POST\", body: JSON.stringify(v) });\n  });\n  return (\n    <Card>\n      <CardHeader><CardTitle>アカウント作成</CardTitle></CardHeader>\n      <CardContent>\n        <form id=\"signup\" onSubmit={onSubmit}>\n          {/* Vertical rhythm comes from Flex direction=\"col\" gap — never space-y-*/gap-* utilities. */}\n          <Flex direction=\"col\" gap=\"md\">\n            <FormField id=\"name\" label=\"氏名\" required error={errors.name?.message}>\n              <Input id=\"name\" {...register(\"name\")} />\n            </FormField>\n            <FormField id=\"email\" label=\"メールアドレス\" required error={errors.email?.message}>\n              <Input id=\"email\" type=\"email\" {...register(\"email\")} />\n            </FormField>\n          </Flex>\n        </form>\n      </CardContent>\n      <CardFooter separated>\n        <Button type=\"submit\" form=\"signup\" loading={isSubmitting}>アカウントを作成</Button>\n      </CardFooter>\n    </Card>\n  );\n}",
    "name": "signup-form",
    "tagline": "Card-wrapped sign-up form using react-hook-form + zod with FormField/Input and a CardFooter action bar (real @godxjp/ui API).",
    "tags": [
      "form",
      "auth",
      "sign-up",
      "zod",
      "validation",
      "react-hook-form"
    ]
  },
  {
    "aliases": [
      "settings-tabs",
      "settings-page",
      "settings-layout"
    ],
    "code": "// Research basis: GitHub, Google Account, Microsoft Account, Atlassian Account settings +\n// the IBM Carbon form grid. Every destination is a URL, so deep links and browser back/forward\n// work for free and the active item is derived from the route — never from local tab state.\n//\n// Desktop (>= lg): persistent vertical local nav on the left + bounded content on the right.\n// Mobile (< lg):   the SAME routes become a horizontal, scrollable tab strip above the content.\n// Styling uses ONLY real semantic token classes (bg-muted / text-foreground / text-muted-foreground)\n// and responsive/visibility utilities — the library ships NO bespoke settings shell/nav CSS classes.\nimport { NavLink, Outlet, Navigate } from \"react-router-dom\";\nimport { Flex } from \"@godxjp/ui/layout\";\nimport { Form, FormField, Input } from \"@godxjp/ui/data-entry\";\nimport { Button } from \"@godxjp/ui/general\";\n\nconst SECTIONS = [\n  { to: \"general\", label: \"基本情報\" },\n  { to: \"security\", label: \"セキュリティ\" },\n  { to: \"notifications\", label: \"通知\" },\n] as const;\n\n// One link renderer for BOTH breakpoints. NavLink gives us the active state (and sets\n// aria-current=\"page\") straight from the URL; semantic tokens only, no invented classes.\nconst link = ({ isActive }: { isActive: boolean }) =>\n  [\n    \"rounded-md px-3 py-2 text-sm no-underline transition-colors whitespace-nowrap\",\n    isActive\n      ? \"bg-muted text-foreground font-medium\"\n      : \"text-muted-foreground hover:text-foreground\",\n  ].join(\" \");\n\nexport function SettingsLayout() {\n  return (\n    <Flex direction=\"col\" gap=\"lg\" className=\"lg:flex-row lg:items-start\">\n      {/* < lg: horizontal, scrollable route tabs. Flex owns the row and the gap — hideFrom /\n          hideBelow own the breakpoint, so neither nav needs a hand-rolled flex or gap utility. */}\n      <Flex as=\"nav\" aria-label=\"設定\" gap={1} hideFrom=\"lg\" className=\"overflow-x-auto\">\n        {SECTIONS.map((s) => (\n          <NavLink key={s.to} to={s.to} className={link} end>{s.label}</NavLink>\n        ))}\n      </Flex>\n      {/* >= lg: persistent vertical local nav (bounded width, does not shrink). */}\n      <Flex as=\"nav\" aria-label=\"設定\" direction=\"col\" gap={1} hideBelow=\"lg\" width={224} shrink={false}>\n        {SECTIONS.map((s) => (\n          <NavLink key={s.to} to={s.to} className={link} end>{s.label}</NavLink>\n        ))}\n      </Flex>\n      {/* Bounded content region (~42rem) — the routed section renders here. min-w-0 lets it shrink. */}\n      <main className=\"min-w-0 max-w-2xl flex-1\"><Outlet /></main>\n    </Flex>\n  );\n}\n\n// A settings section. Horizontal label/control rows on desktop (collapse to stacked on mobile),\n// controls bounded to a semantic width — set ONCE on the <Form>, which every FormField reads\n// (gh#998). FormField OWNS the label↔control relationship — do NOT hand-roll <Label> + <input>. Save feedback = Button loading prop + a toast in onSuccess.\nexport function GeneralSettingsSection({\n  defaults, onSave, saving, error,\n}: { defaults: { name: string; email: string }; onSave: (v: FormData) => void; saving: boolean; error?: string }) {\n  return (\n    <Form layout=\"horizontal\" controlWidth=\"24rem\"\n      onSubmit={(e) => { e.preventDefault(); onSave(new FormData(e.currentTarget)); }}>\n      <FormField id=\"ws-name\" label=\"ワークスペース名\"\n        helper=\"請求書や共有リンクに表示されます。\" error={error}>\n        <Input id=\"ws-name\" name=\"name\" defaultValue={defaults.name} />\n      </FormField>\n      <FormField id=\"ws-email\" label=\"連絡先メール\">\n        <Input id=\"ws-email\" name=\"email\" type=\"email\" defaultValue={defaults.email} />\n      </FormField>\n      <Flex gap=\"sm\"><Button type=\"submit\" loading={saving}>変更を保存</Button></Flex>\n    </Form>\n  );\n}\n\n// Router wiring — each destination is addressable, so deep links + back/forward just work:\n//   <Route path=\"settings\" element={<SettingsLayout/>}>\n//     <Route index element={<Navigate to=\"general\" replace/>} />\n//     <Route path=\"general\" element={<GeneralSettingsSection .../>} />\n//     <Route path=\"security\" element={<SecuritySection/>} />\n//     <Route path=\"notifications\" element={<NotificationsSection/>} />\n//   </Route>\n\n// ── DO / DON'T ────────────────────────────────────────────────────────────────────────────\n// ✅ Tabs (navigation) are fine for a SMALL number (2–4) of PEER views inside ONE task/section\n//    — e.g. \"プロフィール\" ⇄ \"環境設定\" within a single account page — where losing the URL is OK.\n// ⛔ Tabs as the PRIMARY IA for broad settings (Account / Security / Billing / Members / …):\n//    use route-backed local navigation so every area is deep-linkable and back/forward works.\n// ⛔ Do NOT wrap the whole settings page in a Card, and do NOT make desktop a horizontal tab bar\n//    — persistent local nav scales to many sections; a tab strip does not.\n// ⛔ Do NOT invent CSS class names for the shell/nav/content — compose Flex + semantic tokens.",
    "name": "settings-page-responsive",
    "tagline": "Route-backed settings: persistent desktop local navigation, compact mobile tab strip, deep links + browser history, and bounded form content — NO Card wrapper, NO undefined CSS.",
    "tags": [
      "settings",
      "form",
      "tabs",
      "admin",
      "responsive",
      "navigation"
    ]
  },
  {
    "aliases": [
      "settings-section",
      "settings-row",
      "danger-zone",
      "billing-handoff",
      "preferences-rows",
      "account-identity"
    ],
    "code": "// Gate 0 (docs/COMPOSITION-VS-COMPONENT.md): a \"SettingsSection\"/\"SettingsRow\"/\"DangerZone\"\n// FAILS the Framework-Component Test, so compose it. Reference screen: get_exemplar(\"settings\")\n// (docs/showcase/settings-account-sections.tsx). Composition rules: DESIGN.md → Admin Screen Anatomy.\nimport { ExternalLink } from \"lucide-react\";\nimport { Button, Heading } from \"@godxjp/ui/general\";\nimport { Card, CardContent, Descriptions, ListRow, StatusBadge } from \"@godxjp/ui/data-display\";\nimport { Switch } from \"@godxjp/ui/data-entry\";\nimport { AlertDialog } from \"@godxjp/ui/feedback\";\nimport { Flex } from \"@godxjp/ui/layout\";\n\n// Sections are a heading + spacing: the SECTION step (gap=\"xl\") is larger than the ITEM step\n// (gap=\"sm\"), which is what carries the hierarchy — not a box around every block.\n<Flex direction=\"col\" gap=\"xl\">\n\n  {/* 1) IDENTITY — a section, not a Card. Metadata is Descriptions (a real <dl>). */}\n  <Flex direction=\"col\" gap=\"sm\">\n    <Flex justify=\"between\" align=\"center\">\n      <Heading level={2}>アカウント情報</Heading>\n      <Button variant=\"outline\" size=\"sm\">編集</Button>\n    </Flex>\n    <Descriptions columns={2}>\n      <Descriptions.Item label=\"表示名\">藤原 慧</Descriptions.Item>\n      <Descriptions.Item label=\"メールアドレス\">satoshi@example.co.jp</Descriptions.Item>\n      <Descriptions.Item label=\"従業員番号\" mono>EMP-1042-JP</Descriptions.Item>\n      <Descriptions.Item label=\"利用開始日\">\n        <time dateTime=\"2023-04-01\">\n          {new Intl.DateTimeFormat(locale, { dateStyle: \"long\", timeZone: \"UTC\" }).format(since)}\n        </time>\n      </Descriptions.Item>\n    </Descriptions>\n  </Flex>\n\n  {/* 2) PREFERENCE ROWS — a list of setting rows IS a real boundary, so it gets one Card:\n       CardContent flush + ListRow, whose trailing slot is the control. The visible title is the\n       control's <label htmlFor>. NEVER a flex div with border-b rows. */}\n  <Flex direction=\"col\" gap=\"sm\">\n    <Heading level={2}>通知の環境設定</Heading>\n    <Card>\n      <CardContent flush>\n        <ListRow\n          align=\"start\"\n          overflow=\"wrap\"\n          title={<label htmlFor=\"pref-digest\">週次サマリーをメールで受け取る</label>}\n          description=\"毎週月曜日の朝に未処理の承認をまとめて送信します。\"\n          trailing={<Switch id=\"pref-digest\" checked={digest} onCheckedChange={setDigest} />}\n        />\n      </CardContent>\n    </Card>\n  </Flex>\n\n  {/* 3) BILLING HANDOFF — a section. The state is a canonical StatusBadge (never a page-local\n       status→colour map); money through Intl. */}\n  <Flex direction=\"col\" gap=\"sm\">\n    <Flex justify=\"between\" align=\"center\">\n      <Heading level={2}>請求とサブスクリプション</Heading>\n      <StatusBadge status=\"past_due\" variant=\"outline\" />\n    </Flex>\n    <Descriptions columns={2}>\n      <Descriptions.Item label=\"契約プラン\">ビジネス（年額）</Descriptions.Item>\n      <Descriptions.Item label=\"次回請求予定額\">\n        {new Intl.NumberFormat(locale, { style: \"currency\", currency: \"JPY\" }).format(1536000)}\n      </Descriptions.Item>\n    </Descriptions>\n    <div>\n      <Button variant=\"outline\" asChild>\n        <a href={portalUrl} target=\"_blank\" rel=\"noreferrer\">\n          <ExternalLink aria-hidden=\"true\" />\n          請求ポータルを開く\n          <span className=\"sr-only\">（新しいタブで開きます）</span>\n        </a>\n      </Button>\n    </div>\n  </Flex>\n\n  {/* 4) DANGER ZONE — a real boundary: Card accent=\"destructive\" (the rail comes from\n       --destructive). The heading and each row say what is lost; no decorative icon. */}\n  <Flex direction=\"col\" gap=\"sm\">\n    <Heading level={2}>取り消せない操作</Heading>\n    <Card accent=\"destructive\">\n      <CardContent flush>\n        <ListRow\n          align=\"start\"\n          overflow=\"wrap\"\n          title=\"ワークスペースと全データを完全に削除する\"\n          description=\"勤怠記録・申請履歴・監査ログがすべて削除されます。\"\n          trailing={<Button variant=\"destructive\" size=\"sm\" onClick={() => setConfirmOpen(true)}>削除</Button>}\n        />\n      </CardContent>\n    </Card>\n  </Flex>\n</Flex>\n\n<AlertDialog\n  open={confirmOpen}\n  onOpenChange={setConfirmOpen}\n  title=\"ワークスペースを完全に削除しますか？\"\n  description=\"この操作は取り消せません。確認のため識別子を入力してください。\"\n  challenge={workspaceSlug}\n  variant=\"destructive\"\n  onConfirm={destroy}\n/>\n\n// ── DO / DON'T ────────────────────────────────────────────────────────────────────────────\n// ✅ Section step > item step (gap=\"xl\" between sections, gap=\"sm\" inside one).\n// ✅ A Card only where there is a real boundary: a list of setting rows, the danger zone.\n// ✅ Every setting row is a ListRow; its control lives in `trailing`, titled by <label htmlFor>.\n// ⛔ Do NOT add SettingsSection / SettingsRow / DangerZone to src/components — they fail Gate 0.\n// ⛔ Do NOT give every block Card + CardHeader + CardTitle + CardDescription (ui-audit card-per-section).\n// ⛔ Do NOT hand-roll padding (p-4), a status colour map, or a <dl> replacement.",
    "name": "settings-section-rows",
    "tagline": "The four canonical settings blocks — identity · preference rows · billing handoff · danger zone. Identity and billing are SECTIONS (Heading + Descriptions, no surface); preference rows and the danger zone are Cards because they are real boundaries. A COMPOSITION pattern: godx-ui ships no SettingsSection/SettingsRow/DangerZone component and never will.",
    "tags": [
      "settings",
      "account",
      "billing",
      "danger",
      "preferences",
      "listrow",
      "card",
      "admin"
    ]
  },
  {
    "aliases": [
      "pricing-table",
      "plan-picker",
      "pricing-card"
    ],
    "code": "import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Badge, FeatureList } from \"@godxjp/ui/data-display\";\nimport { ResponsiveGrid } from \"@godxjp/ui/layout\";\nimport { Button, Text } from \"@godxjp/ui/general\";\nimport { useTranslation } from \"@godxjp/ui/i18n\";\n\ntype Plan = {\n  id: string;\n  name: string;\n  summary: string;\n  /** Minor units of the ISO 4217 currency; formatted with Intl, never hand-built. */\n  price: number;\n  currency: string;\n  recommended?: boolean;\n  current?: boolean;\n  features: { state: \"included\" | \"excluded\" | \"limited\"; label: string; description?: string }[];\n};\n\nexport function PricingPlans({ plans, onChoose }: { plans: Plan[]; onChoose: (id: string) => void }) {\n  const { t, locale } = useTranslation();\n  return (\n    // 3 columns from the sm container step, 1 below it — the geometry is the preset's, not yours.\n    <ResponsiveGrid preset=\"pricing-plans\" gap=\"md\">\n      {plans.map((plan) => {\n        const money = new Intl.NumberFormat(locale, { style: \"currency\", currency: plan.currency });\n        const digits = money.resolvedOptions().maximumFractionDigits ?? 0;\n        return (\n          // \"featured\" is the one recommended plan: a brand-toned perimeter, not a bigger card.\n          <Card key={plan.id} variant={plan.recommended ? \"featured\" : \"default\"}>\n            <CardHeader>\n              <CardTitle>{plan.name}</CardTitle>\n              <CardDescription>{plan.summary}</CardDescription>\n              {plan.current ? <Badge tone=\"success\">{t(\"billing.currentPlan\")}</Badge> : null}\n            </CardHeader>\n            <CardContent>\n              <Text size=\"xl\" weight=\"semibold\" tabular>\n                {money.format(plan.price / 10 ** digits)}\n              </Text>\n              {/* FeatureList owns the included / limited / excluded glyph, colour and sr-only prefix. */}\n              <FeatureList items={plan.features} />\n            </CardContent>\n            <CardFooter>\n              <Button\n                fullWidth\n                variant={plan.recommended ? \"default\" : \"outline\"}\n                disabled={plan.current}\n                onClick={() => onChoose(plan.id)}\n              >\n                {plan.current ? t(\"billing.currentPlan\") : t(\"billing.choosePlan\")}\n              </Button>\n            </CardFooter>\n          </Card>\n        );\n      })}\n    </ResponsiveGrid>\n  );\n}",
    "name": "pricing-plans",
    "tagline": "Pricing plan cards — ResponsiveGrid preset + Card featured + FeatureList + tabular price. A composition, not a component (consumer request, debated 2026-10-09 on #1205).",
    "tags": [
      "pricing",
      "plans",
      "billing",
      "card",
      "composition"
    ]
  },
  {
    "code": "import { AlertDialog, toast } from \"@godxjp/ui/feedback\";\n\n// The preset covers a typed challenge: confirm stays disabled until the input equals `slug`.\nexport function DeleteProjectDialog({ open, onOpenChange, slug }: { open: boolean; onOpenChange: (v: boolean) => void; slug: string }) {\n  return (\n    <AlertDialog\n      open={open}\n      onOpenChange={onOpenChange}\n      title=\"プロジェクトを削除\"\n      description={`この操作は取り消せません。確認のためプロジェクト名 \"${slug}\" と入力してください。`}\n      challenge={slug}\n      confirmLabel=\"完全に削除\"\n      onConfirm={() => {\n        toast.success(\"削除しました\");\n        onOpenChange(false);\n      }}\n    />\n  );\n}",
    "name": "confirm-destructive",
    "tagline": "Type-to-confirm destructive dialog — the AlertDialog PRESET with `challenge` (the compound AlertDialog* parts are the older way in; a body the preset does not cover is Dialog variant=\"destructive\").",
    "tags": [
      "dialog",
      "confirm",
      "destructive",
      "delete"
    ]
  },
  {
    "aliases": [
      "cascading-selects",
      "country-state-city",
      "chained-selects",
      "linked-selects"
    ],
    "code": "// Live page: /showcase/form-dependent-selects (docs/showcase/form-dependent-selects.tsx).\n// Three rules: (1) child disabled until parent has a value + helper saying why;\n// (2) child loading while its options fetch; (3) parent change resets EVERY descendant to \"\".\n// Reset to \"\", Select's controlled empty value — undefined flips it to uncontrolled.\nimport * as React from \"react\";\nimport { Form, FormField, Select } from \"@godxjp/ui/data-entry\";\nimport { Flex } from \"@godxjp/ui/layout\";\nimport { useTranslation } from \"@godxjp/ui/i18n\";\n\ntype Option = { value: string; label: string };\ntype Address = { country: string; region: string; city: string };\n\n// Fetch options for a parent value. The answer is stored WITH the parent it answers, so\n// `loading` is derived and a slow answer for an OLD parent is never shown on the new one.\nfunction useChildOptions(parent: string, fetchOptions: (parent: string) => Promise<Option[]>) {\n  const [answer, setAnswer] = React.useState<{ parent: string; options: Option[] }>({ parent: \"\", options: [] });\n  React.useEffect(() => {\n    if (!parent) return;\n    let live = true;\n    void fetchOptions(parent).then((options) => { if (live) setAnswer({ parent, options }); });\n    return () => { live = false; };\n  }, [parent, fetchOptions]);\n  const ready = parent !== \"\" && answer.parent === parent;\n  return { options: ready ? answer.options : [], loading: parent !== \"\" && !ready };\n}\n\nexport function AddressFields({\n  fetchRegions,\n  fetchCities,\n}: {\n  fetchRegions: (country: string) => Promise<Option[]>; // stable (module-level or useCallback)\n  fetchCities: (region: string) => Promise<Option[]>;\n}) {\n  const { t, locale } = useTranslation();\n  const [value, setValue] = React.useState<Address>({ country: \"\", region: \"\", city: \"\" });\n  const regions = useChildOptions(value.country, fetchRegions);\n  const cities = useChildOptions(value.region, fetchCities);\n\n  // ISO 3166-1 alpha-2 → the reader's language, no catalogue needed.\n  const countries = React.useMemo(() => {\n    const names = new Intl.DisplayNames([locale], { type: \"region\" });\n    return [\"JP\", \"US\", \"VN\"].map((code) => ({ value: code, label: names.of(code) ?? code }));\n  }, [locale]);\n\n  return (\n    <Form controlWidth=\"20rem\">\n      <Flex direction=\"col\" gap=\"md\">\n        <FormField id=\"country\" label={t(\"address.country\")}>\n          <Select\n            value={value.country}\n            onValueChange={(country: string) => setValue({ country, region: \"\", city: \"\" })}\n            options={countries}\n            placeholder={t(\"address.countryPlaceholder\")}\n          />\n        </FormField>\n        <FormField\n          id=\"region\"\n          label={t(\"address.region\")}\n          helper={value.country ? undefined : t(\"address.selectCountryFirst\")}\n        >\n          <Select\n            value={value.region}\n            onValueChange={(region: string) => setValue({ ...value, region, city: \"\" })}\n            options={regions.options}\n            disabled={!value.country}\n            loading={regions.loading}\n            placeholder={t(\"address.regionPlaceholder\")}\n          />\n        </FormField>\n        <FormField\n          id=\"city\"\n          label={t(\"address.city\")}\n          helper={value.region ? undefined : t(\"address.selectRegionFirst\")}\n        >\n          <Select\n            value={value.city}\n            onValueChange={(city: string) => setValue({ ...value, city })}\n            options={cities.options}\n            disabled={!value.region}\n            loading={cities.loading}\n            placeholder={t(\"address.cityPlaceholder\")}\n          />\n        </FormField>\n      </Flex>\n    </Form>\n  );\n}",
    "name": "dependent-selects",
    "tagline": "Country → state → city: three Selects where each child is disabled until its parent has a value (FormField helper says why), loading while its options fetch, and reset to \"\" when the parent changes. A composition, not a component — reach for Cascader only when it is ONE field.",
    "tags": [
      "form",
      "select",
      "dependent",
      "cascading",
      "address",
      "loading",
      "reset"
    ]
  },
  {
    "aliases": [
      "card-selection",
      "selectable-cards",
      "bulk-delete-cards",
      "grid-bulk-actions"
    ],
    "code": "// Live page: /showcase/card-grid-selection (docs/showcase/card-grid-selection.tsx).\n// DataTable.BulkActions reads the table context only when one exists — outside a table pass\n// `count`: it renders nothing at 0, and the localised \"N selected\" + a labelled region above 0.\n// The Checkbox's inline label is the item name: the name is the click target and the\n// accessible name. Do NOT make the whole Card an onClick div.\nimport * as React from \"react\";\nimport { FolderOpen, Trash2 } from \"lucide-react\";\nimport { Card, CardContent, DataTable, EmptyState } from \"@godxjp/ui/data-display\";\nimport { Checkbox } from \"@godxjp/ui/data-entry\";\nimport { AlertDialog, Toaster, toast } from \"@godxjp/ui/feedback\";\nimport { Button } from \"@godxjp/ui/general\";\nimport { Flex, ResponsiveGrid } from \"@godxjp/ui/layout\";\nimport { useTranslation } from \"@godxjp/ui/i18n\";\n\ntype Item = { id: string; name: string };\n\nexport function SelectableCardGrid({ source }: { source: Item[] }) {\n  const { t } = useTranslation();\n  const [items, setItems] = React.useState(source);\n  const [selected, setSelected] = React.useState<Set<string>>(new Set());\n  const [confirmOpen, setConfirmOpen] = React.useState(false);\n  const [confirmCount, setConfirmCount] = React.useState(0); // frozen: selection clears on confirm\n\n  const all = items.length > 0 && items.every((i) => selected.has(i.id));\n  const selectAll: boolean | \"indeterminate\" = all ? true : selected.size > 0 ? \"indeterminate\" : false;\n\n  const deleteSelected = () => {\n    const removed = new Set(selected);\n    setItems((cur) => cur.filter((i) => !removed.has(i.id)));\n    setSelected(new Set());\n    toast(t(\"files.deleted\", { count: removed.size }), {\n      action: {\n        label: t(\"files.undo\"),\n        // restore in the SOURCE order, not appended at the end\n        onClick: () => setItems((cur) => source.filter((i) => removed.has(i.id) || cur.some((c) => c.id === i.id))),\n      },\n    });\n  };\n\n  if (items.length === 0) {\n    return (\n      <EmptyState\n        icon={FolderOpen}\n        variant=\"section\"\n        title={t(\"files.emptyTitle\")}\n        action={<Button variant=\"outline\" onClick={() => setItems(source)}>{t(\"files.reset\")}</Button>}\n      />\n    );\n  }\n\n  return (\n    <Flex direction=\"col\" gap=\"md\">\n      <Flex direction=\"row\" align=\"center\" gap=\"md\" wrap>\n        <Checkbox\n          checked={selectAll}\n          onCheckedChange={(next) => setSelected(next === true ? new Set(items.map((i) => i.id)) : new Set())}\n        >\n          {t(\"files.selectAll\")}\n        </Checkbox>\n        <DataTable.BulkActions count={selected.size}>\n          <Button variant=\"destructive\" size=\"sm\" onClick={() => { setConfirmCount(selected.size); setConfirmOpen(true); }}>\n            <Trash2 aria-hidden=\"true\" />\n            {t(\"files.delete\")}\n          </Button>\n        </DataTable.BulkActions>\n      </Flex>\n      <ResponsiveGrid columns={{ base: 1, sm: 2, lg: 3 }}>\n        {items.map((item) => (\n          <Card key={item.id} className={selected.has(item.id) ? \"border-primary\" : undefined}>\n            <CardContent>\n              <Checkbox\n                checked={selected.has(item.id)}\n                onCheckedChange={(on) =>\n                  setSelected((prev) => {\n                    const next = new Set(prev);\n                    if (on === true) next.add(item.id); else next.delete(item.id);\n                    return next;\n                  })\n                }\n              >\n                {item.name}\n              </Checkbox>\n            </CardContent>\n          </Card>\n        ))}\n      </ResponsiveGrid>\n      <AlertDialog\n        open={confirmOpen}\n        onOpenChange={setConfirmOpen}\n        variant=\"destructive\"\n        title={t(\"files.confirmTitle\", { count: confirmCount })}\n        confirmLabel={t(\"files.delete\")}\n        onConfirm={deleteSelected}\n      />\n      <Toaster />{/* once per app — usually in the root layout, not here */}\n    </Flex>\n  );\n}",
    "name": "card-grid-selection",
    "tagline": "Select cards in a grid and act on them in bulk — Card + Checkbox per item, the real DataTable.BulkActions driven by count outside a table, indeterminate select-all, AlertDialog before delete, toast with Undo after, EmptyState when nothing is left.",
    "tags": [
      "selection",
      "bulk",
      "cards",
      "grid",
      "delete",
      "undo",
      "empty",
      "checkbox"
    ]
  },
  {
    "code": "// Server (Laravel): defer the heavy prop\n//   Inertia::render('crm/coupons/index', [\n//     'coupons' => Inertia::defer(fn () => Coupon::all()),\n//   ]);\nimport { Card, CardContent, DataTable } from \"@godxjp/ui/data-display\";\nimport type { ColumnDef } from \"@godxjp/ui/data-display\";\nimport { SkeletonTable } from \"@godxjp/ui/feedback\";\n\ntype Coupon = { id: string; name: string };\nconst columns: ColumnDef<Coupon>[] = [{ key: \"name\", header: \"クーポン名\" }];\n\n// coupons is undefined until the deferred request resolves\nexport default function Coupons({ coupons }: { coupons?: Coupon[] }) {\n  return (\n    <Card>\n      <CardContent flush>\n        {!coupons\n          ? <SkeletonTable rows={10} columns={6} />\n          : <DataTable data={coupons} columns={columns} getRowId={(c) => c.id} />}\n      </CardContent>\n    </Card>\n  );\n}",
    "name": "deferred-loading",
    "tagline": "Inertia deferred props with a Skeleton fallback — SkeletonTable while data loads, then DataTable (real @godxjp/ui API).",
    "tags": [
      "loading",
      "skeleton",
      "deferred",
      "inertia",
      "table"
    ]
  },
  {
    "code": "import { Head, router } from \"@inertiajs/react\"\nimport { useMemo, useState } from \"react\"\nimport { PageContainer, Flex } from \"@godxjp/ui/layout\"\nimport { Card, CardContent, DataTable, EmptyState, Badge, type ColumnDef } from \"@godxjp/ui/data-display\"\nimport { SearchInput, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@godxjp/ui/data-entry\"\nimport { Toolbar, ToolbarGroup, Pagination } from \"@godxjp/ui/navigation\"\nimport { formatDate } from \"@godxjp/ui/datetime\"\nimport { withCrmLayout } from \"@/layouts/crm-layout\" // see \"inertia-persistent-layout\"\n\ntype Coupon = { id: string; name: string; status: string; scope: string; validFrom: string; validTo: string; usage: number }\nconst PAGE_SIZE = 10\n\nfunction Coupons({ coupons }: { coupons: Coupon[] }) {\n  const [q, setQ] = useState(\"\")\n  const [status, setStatus] = useState(\"all\")\n  const [page, setPage] = useState(1)\n\n  const filtered = useMemo(() => coupons.filter((c) => {\n    if (q && !c.name.toLowerCase().includes(q.toLowerCase())) return false\n    if (status !== \"all\" && c.status !== status) return false\n    return true\n  }), [coupons, q, status])\n  const paged = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)\n\n  // ColumnDef = { key, header, render?, align?: \"left\"|\"center\"|\"right\", sortable?, width? }\n  const columns: ColumnDef<Coupon>[] = [\n    { key: \"name\", header: \"クーポン名\", render: (c) => <span className=\"font-medium\">{c.name}</span> },\n    { key: \"scope\", header: \"スコープ\" }, // a category is plain text; Badge is for the status column only\n    { key: \"status\", header: \"ステータス\", render: (c) => <Badge status={c.status} /> },\n    { key: \"valid\", header: \"有効期間\", render: (c) => `${formatDate(c.validFrom)} 〜 ${formatDate(c.validTo)}` },\n    { key: \"usage\", header: \"利用数\", align: \"right\", render: (c) => c.usage.toLocaleString() },\n  ]\n\n  return (\n    <>\n      <Head title=\"クーポン管理\" />\n      {/* RULE: every page wraps in PageContainer; vertical rhythm via Flex direction=\"col\"/ResponsiveGrid, never p-* or gap-* */}\n      {/* No subtitle that restates the title, no StatCard row: the total is Pagination's showTotal. */}\n      <PageContainer title=\"クーポン管理\">\n        <Flex direction=\"col\" gap=\"lg\">\n          {/* Toolbar is the filter-bar primitive (standalone, above the table surface). It WRAPS and\n              never scrolls sideways (rule #51); past 2–4 filters, see the \"list-filters\" pattern. Never a\n              hand-built Flex/Card of FormFields (ui-audit hand-rolled-filter-bar). */}\n          <Toolbar hasActiveFilters={q !== \"\" || status !== \"all\"} onClear={() => { setQ(\"\"); setStatus(\"all\"); setPage(1) }}>\n            {/* SearchInput is value + onSearch(v) — NOT onChange */}\n            <SearchInput placeholder=\"クーポン名で検索\" value={q} onSearch={(v) => { setQ(v); setPage(1) }} />\n            {/* Status view of the SAME table = a filter: Segmented with count for 2-4 statuses, this\n                Select past 4. Never Tabs: Tabs switch panels with their own columns (gh#1324). */}\n            <ToolbarGroup label=\"ステータス\">\n              <Select value={status} onValueChange={(v) => { setStatus(v); setPage(1) }}>\n                <SelectTrigger><SelectValue /></SelectTrigger>\n                <SelectContent>\n                  <SelectItem value=\"all\">全ステータス</SelectItem>\n                  <SelectItem value=\"公開中\">公開中</SelectItem>\n                  <SelectItem value=\"下書き\">下書き</SelectItem>\n                </SelectContent>\n              </Select>\n            </ToolbarGroup>\n          </Toolbar>\n\n          <Card>\n            <CardContent flush>\n              {filtered.length === 0\n                ? <EmptyState title=\"該当するクーポンがありません\" description=\"検索条件を変更してください。\" /> /* one sentence, no icon */\n                : <DataTable data={paged} columns={columns} getRowId={(c) => c.id} onRowClick={(c) => router.visit(`/coupons/${c.id}`)} />}\n            </CardContent>\n          </Card>\n\n          {/* Pagination ONLY for real multi-page data — hide it while empty or single-page. */}\n          {filtered.length > PAGE_SIZE && (\n            <Pagination value={page} total={filtered.length} pageSize={PAGE_SIZE} showTotal onValueChange={(p) => setPage(p)} />\n          )}\n        </Flex>\n      </PageContainer>\n    </>\n  )\n}\n\nCoupons.layout = withCrmLayout\nexport default Coupons",
    "name": "inertia-list-page",
    "tagline": "Inertia + @godxjp/ui list page — PageContainer + Toolbar (above the table) + DataTable + Badge + Pagination. No KPI row, no restating subtitle.",
    "tags": [
      "inertia",
      "list",
      "table",
      "page",
      "filter",
      "pagination",
      "datatable",
      "crm"
    ]
  },
  {
    "code": "import { Head, router } from \"@inertiajs/react\"\nimport { PageContainer } from \"@godxjp/ui/layout\"\nimport { Card, CardContent, EmptyState, Descriptions, Badge } from \"@godxjp/ui/data-display\"\nimport { Button } from \"@godxjp/ui/general\"\nimport { formatDate } from \"@godxjp/ui/datetime\"\nimport { ArrowLeft } from \"lucide-react\"\nimport { withCrmLayout } from \"@/layouts/crm-layout\"\n\n// Detail routes pass the param as an Inertia prop:\n//   Route::get('/members/{id}', fn ($id) => Inertia::render('crm/members/show', ['id' => $id]))\nfunction MemberShow({ id }: { id: string }) {\n  const member = MEMBERS.find((m) => m.id === id)\n\n  if (!member) {\n    return (\n      <>\n        <Head title=\"会員詳細\" />\n        <PageContainer title=\"会員詳細\">\n          <EmptyState title=\"会員が見つかりません\" description={`ID「${id}」は存在しません。`} />\n          <Button variant=\"outline\" onClick={() => router.visit(\"/members\")}><ArrowLeft className=\"size-4\" />一覧へ戻る</Button>\n        </PageContainer>\n      </>\n    )\n  }\n\n  return (\n    <>\n      <Head title={member.name} />\n      <PageContainer title={member.name}>\n        {/* Figures about one member are facts, not KPIs: Descriptions rows, not a StatCard row. */}\n        <Card>\n          <CardContent>\n            {/* Descriptions is COMPOUND — value goes in children, not a prop */}\n            <Descriptions columns={2}>\n              <Descriptions.Item label=\"会員ID\">{member.id}</Descriptions.Item>\n              <Descriptions.Item label=\"ランク\">{member.rank}</Descriptions.Item>\n              <Descriptions.Item label=\"ステータス\"><Badge status={member.status} /></Descriptions.Item>\n              <Descriptions.Item label=\"登録日\">{formatDate(member.registeredAt)}</Descriptions.Item>\n              <Descriptions.Item label=\"累計購入額\">{`¥${member.total.toLocaleString()}`}</Descriptions.Item>\n              <Descriptions.Item label=\"来店回数\">{member.visits}</Descriptions.Item>\n            </Descriptions>\n          </CardContent>\n        </Card>\n      </PageContainer>\n    </>\n  )\n}\n\nMemberShow.layout = withCrmLayout\nexport default MemberShow",
    "name": "inertia-detail-page",
    "tagline": "Inertia detail page — receives {id} prop, Descriptions (compound) in one Card + EmptyState fallback. No KPI tile row.",
    "tags": [
      "inertia",
      "detail",
      "show",
      "page",
      "keyvaluegrid",
      "crm"
    ]
  },
  {
    "code": "// resources/js/layouts/crm-layout.tsx\nimport { router, usePage } from \"@inertiajs/react\"\nimport { AppShell, Sidebar } from \"@godxjp/ui/layout\"\nimport { LayoutDashboard } from \"lucide-react\"\nimport type { ReactNode } from \"react\"\n\nexport function CrmLayout({ children }: { children: ReactNode }) {\n  const { url } = usePage()\n  const sections = [{ label: \"メイン\", items: [{ id: \"/dashboard\", label: \"ダッシュボード\", icon: LayoutDashboard }] }]\n  return (\n    <AppShell sidebar={<Sidebar activeId={url} onSelect={(id) => router.visit(id)} sections={sections} product={{ name: \"JOVY CRM\" }} />}>\n      {children}\n    </AppShell>\n  )\n}\n\n// ⚠️ GOTCHA 1 — persistent layout MUST be the ARRAY form.\n// A render fn `(page) => <CrmLayout>{page}</CrmLayout>` is indistinguishable from a\n// component; Inertia React calls it with the page-PROPS object and renders that\n// object as a child → \"Objects are not valid as a React child {errors, auth, …}\".\nexport const withCrmLayout = [CrmLayout]   // ✅ array → Inertia passes the page as children\n// page usage:  Dashboard.layout = withCrmLayout\n\n// ⚠️ GOTCHA 2 — Inertia v3 SSRs even in `npm run dev`. NEVER call Math.random() or\n// argless new Date() during render (e.g. fabricating chart/demo numbers) → React\n// hydration mismatch (\"server rendered text didn't match the client\"). Seed\n// deterministically by index, or compute inside an event handler:\nconst seeded = (n: number) => { const x = Math.sin((n + 1) * 99.71) * 1e4; return x - Math.floor(x) }",
    "name": "inertia-persistent-layout",
    "tagline": "Inertia persistent layout (AppShell+Sidebar) — the array-form gotcha + the SSR/Math.random gotcha.",
    "tags": [
      "inertia",
      "layout",
      "appshell",
      "sidebar",
      "ssr",
      "hydration",
      "gotcha"
    ]
  },
  {
    "aliases": [
      "error-surface",
      "errorsurface",
      "403",
      "404",
      "500",
      "503",
      "exception-page"
    ],
    "code": "// @godxjp/ui SHIPS <ErrorSurface>. Do NOT hand-compose an error page from\n// the auth CenteredShell + a generic Card + a local \".canonical-auth-card\" — that workaround IS the regression.\n//\n//    403 / 404  → mode=\"application\": the surface is the BODY of the AppShell you ALREADY render\n//                 (sidebar + topbar + breadcrumb are PRESERVED, never reconstructed)\n//    500 / 503  → mode=\"system\":      the surface OWNS the page (CenteredShell align=\"center\",\n//                 package-owned geometry at 1440 / 1024 / 390 — no min-h-dvh, no media query)\n// Docs page: docs/layout/error-surface/\n\nimport { Button, Logo, Text } from \"@godxjp/ui/general\";\nimport { AppShell, ErrorSurface, PageContainer, Sidebar } from \"@godxjp/ui/layout\";\n\n// ── WHAT THE COMPONENT OWNS ───────────────────────────────────────────────────────────\n//  • status → icon + tone (ShieldAlert/warning · SearchX/muted · ServerCrash/destructive ·\n//    Wrench/warning). You pass the NUMBER; you never write the lookup.\n//  • EXACTLY ONE recovery action — a single slot; a 2nd element is dropped with a dev error.\n//  • semantic metadata slots as real <dt>/<dd> pairs, with LIBRARY-owned localized labels:\n//    requestId · permission · organization · maintenance{start,end,timeZone,progress}\n//  • heading order (h2 in application mode under the PageContainer h1, h1 in system mode)\n//  • the status code announced as \"HTTP status 403\", not the cardinal number \"403\"\n// PRODUCT COPY stays yours: title / description / action come from the APP's own t().\n\n// ── 403 / 404 — APPLICATION mode: PRESERVE the authenticated shell ─────────────────────\nexport function ForbiddenPage() {\n  return (\n    <AppShell sidebar={<Sidebar activeId=\"reports\" sections={sections} />}>\n      <PageContainer title={t(\"reports.title\")} breadcrumb={[{ label: t(\"nav.home\"), to: \"/\" }, { label: t(\"reports.title\") }]}>\n        <ErrorSurface\n          mode=\"application\"\n          status={403}\n          title={t(\"errors.403.title\")}\n          description={t(\"errors.403.description\")}\n          permission=\"reports.view\"        {/* WHY: the missing role … */}\n          organization=\"Acme KK\"           {/* … vs the wrong workspace */}\n          action={<Button onClick={goHome}>{t(\"errors.backHome\")}</Button>}\n        />\n      </PageContainer>\n    </AppShell>\n  );\n}\n\n// ── 500 / 503 — SYSTEM mode: package-owned viewport-centred geometry ───────────────────\n// The component renders CenteredShell align=\"center\" itself → the column centres in the 100dvh\n// shell at 1440 / 1024 / 390. NO className, NO min-h-dvh, NO media query. Tall copy still scrolls\n// from the top (auto offsets collapse), so a long JA/VI message is never clipped.\nexport function MaintenancePage() {\n  return (\n    <ErrorSurface\n      mode=\"system\"\n      status={503}\n      brand={<Logo glyph=\"G\" />}\n      title={t(\"errors.503.title\")}\n      description={t(\"errors.503.description\")}\n      maintenance={{\n        start: \"2026-08-02T18:00:00Z\",  // ISO-8601 instants + an IANA zone → Intl.formatRange\n        end: \"2026-08-02T20:00:00Z\",\n        timeZone: \"Asia/Tokyo\",\n        progress: 40,                   // 0-100, SERVER-SENT (a client clock breaks hydration)\n      }}\n      footer={<Text size=\"xs\" tone=\"muted\">2026 GodX</Text>}\n      action={<Button onClick={reload}>{t(\"errors.reload\")}</Button>}\n    />\n  );\n}\n\n// ── INERTIA / SSR — one Error page component, one expression ───────────────────────────\n// Laravel: Handler::render() → Inertia::render('Error', ['status' => $response->getStatusCode()])\n//\n//   export default function Error({ status, requestId }) {\n//     const isSystem = status >= 500\n//     return (\n//       <ErrorSurface\n//         mode={isSystem ? \"system\" : \"application\"}\n//         status={status}\n//         requestId={requestId}\n//         title={t(`errors.${status}.title`)}\n//         description={t(`errors.${status}.description`)}\n//         action={<Button asChild><Link href=\"/\">{t(\"errors.backHome\")}</Link></Button>}\n//       />\n//     )\n//   }\n//   // persistent layout ⇒ 403/404 keep the authenticated shell; 500/503 own the page\n//   Error.layout = (page) => (page.props.status < 500 ? <CrmLayout>{page}</CrmLayout> : page)\n//\n// SSR rules:\n//  • ErrorSurface has NO client state / effects / portals → it renders fully server-side. An\n//    exception page must be readable without hydration.\n//  • Pass the maintenance window as a SERVER-SENT ISO-8601 string + an explicit IANA timeZone.\n//    An argless new Date() or the server's local zone ⇒ hydration mismatch.\n//  • A 500 page must NOT import the app's query client / data providers — the failure may be\n//    inside them. ErrorSurface needs only the AppProvider that supplies the locale.\n//\n// THEMING: retune with tokens, never a className —\n//   --error-surface-max-width | -gap | -padding-block(-compact) | -meta-gap | -meta-row-gap |\n//   --error-surface-meta-border (default 'none', rule #44) | --error-surface-progress-max-width\n//\n// ANTI-PATTERNS: the auth CenteredShell + a generic Card (the workaround; it is the\n// UNAUTHENTICATED root with auth-card geometry) · rebuilding nav on the 403 page · two CTAs ·\n// className=\"min-h-dvh flex …\" · a hand-built \"18:00 - 20:00 JST\" string · writing the request id\n// into 'description' as prose instead of using the semantic slot.",
    "name": "error-pages",
    "tagline": "400 / 403 / 404 / 500 / 503 exception pages. USE THE COMPONENT: import { ErrorSurface } from \"@godxjp/ui/layout\". `mode=\"application\"` (400/403/404) is the body you put inside the AppShell the route already renders; `mode=\"system\"` (500/503) owns the page via CenteredShell align=\"center\".",
    "tags": [
      "error",
      "errorsurface",
      "403",
      "404",
      "500",
      "503",
      "maintenance",
      "inertia",
      "ssr",
      "layout",
      "emptystate",
      "centeredshell"
    ]
  },
  {
    "code": "import { Badge } from \"@godxjp/ui/data-display\"\n\n// Badge auto-colours a fixed set of English LIFECYCLE keys:\n//   active/completed (success ✓) · draft (neutral ○) · pending/temporary (warning ⏱)\n//   scheduled/sending (info) · cancelled (neutral) · failed/deleted/bounced (destructive ✕)\n// Anything else (localized labels, tiers) falls back to neutral grey ○ unless you override.\n\n// 1) Lifecycle with localized text — map to the key, keep JP via children (icon stays):\n<Badge status=\"active\">公開中</Badge>        // green ✓ 公開中\n\n// 2) Unknown label — set tone explicitly (no icon, since the key is unknown):\n<Badge status=\"公開中\" tone=\"success\" />\n\n// 3) Tier / category — ONE neutral tone for all of them, no glyph. Colour means status;\n//    a rainbow of tiers reads as several statuses. Prefer plain text in a table column.\n<Badge status=\"プレミアム\" tone=\"neutral\" icon={null} />\n<Badge status=\"ゴールド\"   tone=\"neutral\" icon={null} />\n\n// tone: \"success\" | \"warning\" | \"destructive\" | \"info\" | \"neutral\"  (import type ToneProp)\n// RULE: a chip never wraps — it is pinned white-space: nowrap, so it stays one line in\n// narrow table cells. Centralize the domain→tone map in ONE small consumer wrapper and\n// import that instead of the raw Badge across pages.",
    "name": "badge-coloring",
    "tagline": "Colour a Badge for a localized STATUS via tone + icon; tiers and categories take one neutral tone (colour means status).",
    "tags": [
      "statusbadge",
      "badge",
      "tone",
      "color",
      "status",
      "tier",
      "table"
    ]
  },
  {
    "aliases": [
      "loading-states",
      "data-state",
      "query-states"
    ],
    "code": "import { DataState, classifyQueryError } from \"@godxjp/ui/query\";\nimport { EmptyState } from \"@godxjp/ui/data-display\";\nimport { SkeletonTable } from \"@godxjp/ui/feedback\";\nimport { Building2, Inbox } from \"lucide-react\";\n\n// ONE primary state renders at a time; DataState makes them mutually exclusive:\n//   prerequisite → skeleton(loading) → data → empty → error   (never two at once).\n//\n// enabled:false is PREREQUISITE/idle, NOT loading. TanStack reports isPending while a query is\n// disabled, but fetchStatus stays \"idle\" (no request in flight) — DataState renders the prerequisite\n// slot, so a disabled query shows an instruction, NOT an endless skeleton.\n//   const query = useQuery({ queryKey: [\"members\", orgId], queryFn, enabled: Boolean(orgId) });\n//\n// A background refetch over EXISTING data keeps the content on screen (no skeleton flash) and\n// announces the busy state politely — stale/placeholder refresh is handled for you.\n//\n// Errors are classified by CAUSE, not blanket-retried:\n//   auth (401)         → handled app-wide by <AuthExpiryProvider onAuthExpired> at the root:\n//                        auto-redirect to sign-in ONCE, skeleton stays (NOT a retry, NOT an alert)\n//   forbidden (403)    → permission message + access path            (no retry)\n//   notFound (404)     → contextual not-found                        (no retry)\n//   validation (400/422)→ corrective guidance                        (no retry)\n//   transient (408/429/5xx/network) → Retry offered automatically\n//   unknown            → neutral; opt into Retry via showRetry/onRetry only if it can help\n// App root (once): <AuthExpiryProvider onAuthExpired={() => redirectToSignIn(location.href)}>\nexport function MembersPanel({ query, orgId }: { query: any; orgId?: string }) {\n  return (\n    <DataState\n      query={query}\n      prerequisite={<EmptyState icon={Building2} variant=\"section\" title=\"組織を選択してください\"\n        description=\"メンバーを表示するには、上のセレクタで組織を選びます。\" />}\n      skeleton={<SkeletonTable rows={8} columns={4} />}\n      empty={<EmptyState icon={Inbox} variant=\"section\" title=\"メンバーがいません\"\n        description=\"この組織にはまだメンバーが登録されていません。\" />}\n      isEmpty={(data) => data.items.length === 0}\n    >\n      {(data) => <MemberTable items={data.items} />}\n    </DataState>\n  );\n}\n\n// Need a bespoke error surface? Pass errorRenderer and branch on the classified category — the\n// default detail is always a localized message (never raw token / endpoint / stack text):\n//   errorRenderer={(error, retry) => {\n//     const { category } = classifyQueryError(error);\n//     if (category === \"forbidden\") return <NoAccess />;\n//     if (category === \"transient\") return <Retryable onRetry={retry} />;\n//     return <GenericError />;\n//   }}\n// RULE: pagination/footer chrome NEVER renders outside the populated-data branch (see data-table-page).",
    "name": "async-data-state",
    "tagline": "The full async state machine — ONE primary state at a time: prerequisite, disabled-vs-loading, stale refresh, populated, real-empty, and cause-aware error (401/403/404/422/transient) with correct recovery.",
    "tags": [
      "async",
      "loading",
      "query",
      "error",
      "empty",
      "prerequisite",
      "retry",
      "react-query"
    ]
  },
  {
    "aliases": [
      "filter-bar",
      "table-state",
      "list-page"
    ],
    "code": "import { useState } from \"react\";\nimport { DataState } from \"@godxjp/ui/query\";\nimport { Card, CardContent, DataTable, EmptyState, type ColumnDef } from \"@godxjp/ui/data-display\";\nimport { SkeletonTable } from \"@godxjp/ui/feedback\";\nimport { SearchInput, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@godxjp/ui/data-entry\";\nimport { Toolbar, ToolbarGroup, Pagination } from \"@godxjp/ui/navigation\";\nimport { Flex } from \"@godxjp/ui/layout\";\nimport { useQuery, keepPreviousData } from \"@tanstack/react-query\";\n\ntype Row = { id: string; name: string; status: string };\n\nexport function MembersTablePage({ orgId }: { orgId?: string }) {\n  const [q, setQ] = useState(\"\");\n  const [status, setStatus] = useState(\"all\");\n  const [page, setPage] = useState(1);\n  const pageSize = 20;\n\n  // placeholderData: keepPreviousData → on page/filter change the PREVIOUS page stays visible\n  // (a stale refresh) instead of flashing a skeleton. enabled gates on the prerequisite (orgId).\n  const query = useQuery({\n    queryKey: [\"members\", orgId, q, status, page],\n    queryFn: () => fetchMembers({ orgId: orgId!, q, status, page, pageSize }),\n    enabled: Boolean(orgId),\n    placeholderData: keepPreviousData,\n  });\n\n  const columns: ColumnDef<Row>[] = [\n    { key: \"name\", header: \"氏名\", render: (r) => <span className=\"font-medium\">{r.name}</span> },\n    { key: \"status\", header: \"ステータス\" },\n  ];\n\n  return (\n    <Flex direction=\"col\" gap=\"lg\">\n      {/* Toolbar (filter bar) is STANDALONE and always visible so the user can change filters even\n          while a query is idle/loading/empty. Changing a filter resets to page 1. It WRAPS, never\n          scrolls sideways (rule #51); more than 2–4 filters → the \"list-filters\" pattern. Never a\n          hand-built Flex/Card of FormFields (ui-audit hand-rolled-filter-bar). */}\n      <Toolbar hasActiveFilters={q !== \"\" || status !== \"all\"}\n        onClear={() => { setQ(\"\"); setStatus(\"all\"); setPage(1); }}>\n        <SearchInput placeholder=\"氏名で検索\" value={q} onSearch={(v) => { setQ(v); setPage(1); }} />\n        <ToolbarGroup label=\"ステータス\">\n          <Select value={status} onValueChange={(v) => { setStatus(v); setPage(1); }}>\n            <SelectTrigger><SelectValue /></SelectTrigger>\n            <SelectContent>\n              <SelectItem value=\"all\">すべて</SelectItem>\n              <SelectItem value=\"active\">有効</SelectItem>\n              <SelectItem value=\"invited\">招待中</SelectItem>\n            </SelectContent>\n          </Select>\n        </ToolbarGroup>\n      </Toolbar>\n\n      {/* The table surface owns exactly ONE card. Pagination lives INSIDE the success branch, so it\n          is hidden during prerequisite / loading / empty / error and for single-page results. */}\n      <DataState\n        query={query}\n        prerequisite={<EmptyState variant=\"section\" title=\"組織を選択してください\" />}\n        skeleton={<Card><CardContent flush><SkeletonTable rows={8} columns={2} /></CardContent></Card>}\n        empty={<EmptyState variant=\"section\" title=\"該当するメンバーがいません\" description=\"検索条件を変更してください。\" />}\n        isEmpty={(d) => d.items.length === 0}\n      >\n        {(d) => {\n          const totalPages = Math.ceil(d.total / pageSize);\n          return (\n            <>\n              <Card><CardContent flush><DataTable data={d.items} columns={columns} getRowId={(r) => r.id} /></CardContent></Card>\n              {totalPages > 1 && (\n                <Pagination value={page} total={d.total} pageSize={pageSize} showTotal onValueChange={setPage} />\n              )}\n            </>\n          );\n        }}\n      </DataState>\n    </Flex>\n  );\n}\n// Responsive: Pagination's showTotal range + controls stack at narrow widths automatically.\n// A transient retry keeps q/status/page (they live in component state, not the query) — the\n// same page reloads in place; the user never loses their filters.",
    "name": "data-table-page",
    "tagline": "Filter (Toolbar) + table surface + bottom pagination driven by a query state machine — pagination shows ONLY for successful multi-page data; filters/page survive a transient retry.",
    "tags": [
      "table",
      "pagination",
      "filter",
      "async",
      "toolbar",
      "datatable"
    ]
  },
  {
    "aliases": [
      "more-filters",
      "many-filters",
      "filter-sheet",
      "advanced-filters",
      "add-filter"
    ],
    "code": "// Research basis: Shopify Polaris Filters (\"include no more than 2 or 3 promoted filters\"; a\n// More button opens a sheet with the rest), Linear (one Filter menu), IBM Carbon filtering\n// (filters hidden in a drawer/menu show how many are applied), WCAG 2.2 SC 1.4.10 Reflow\n// (search fields and controls are not in the 2-D exception: they reflow, they do not scroll).\n//\n// RULE #51: a filter bar WRAPS — stacked below 640px, extra rows above — at every width.\n// ❌ FilterBar overflow=\"scroll\" (removed in 33.0.0), <ScrollArea orientation=\"horizontal\"> or an\n//    overflow-x-auto box around filters: the strip runs off-screen and clips its first label.\n// ❌ your own <Flex>/<Card> of FormFields above the table, worst of all justify=\"between\" (filters\n//    pushed to the far ends): ui-audit hand-rolled-filter-bar. Filters pack at the START; the\n//    only far-end element is FilterBar's `actions` slot.\n// ✅ the common filters inline + ONE \"More filters\" Sheet for the rest + wrapping chips.\nimport { useState } from \"react\";\nimport { SlidersHorizontal } from \"lucide-react\";\nimport { Button } from \"@godxjp/ui/general\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@godxjp/ui/data-entry\";\nimport {\n  Sheet, SheetBody, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader,\n  SheetTitle, SheetTrigger,\n} from \"@godxjp/ui/feedback\";\nimport { Flex } from \"@godxjp/ui/layout\";\nimport { FilterBar, FilterBarGroup } from \"@godxjp/ui/navigation\";\n\ntype Option = { value: string; label: string };\ntype Filters = Record<\"q\" | \"status\" | \"owner\" | \"region\" | \"channel\" | \"period\", string>;\nexport const NO_FILTERS: Filters = { q: \"\", status: \"\", owner: \"\", region: \"\", channel: \"\", period: \"\" };\n\nconst STATUS: Option[] = [{ value: \"open\", label: \"Open\" }, { value: \"paid\", label: \"Paid\" }, { value: \"void\", label: \"Void\" }];\nconst OWNER: Option[] = [{ value: \"me\", label: \"Me\" }, { value: \"team\", label: \"My team\" }];\n// Everything people reach for less often. Order = the order of the sheet.\nconst MORE: { key: \"region\" | \"channel\" | \"period\"; label: string; options: Option[] }[] = [\n  { key: \"region\", label: \"Region\", options: [{ value: \"kanto\", label: \"Kanto\" }, { value: \"kansai\", label: \"Kansai\" }] },\n  { key: \"channel\", label: \"Sales channel\", options: [{ value: \"web\", label: \"Web\" }, { value: \"store\", label: \"Store\" }] },\n  { key: \"period\", label: \"Period\", options: [{ value: \"month\", label: \"This month\" }, { value: \"quarter\", label: \"This quarter\" }] },\n];\nconst LABELS: Record<string, { label: string; options: Option[] }> = {\n  status: { label: \"Status\", options: STATUS },\n  owner: { label: \"Owner\", options: OWNER },\n  ...Object.fromEntries(MORE.map((f) => [f.key, f])),\n};\n\nexport function OrderFilters({ value, onValueChange, resultCount }: {\n  value: Filters; onValueChange: (next: Filters) => void; resultCount: number;\n}) {\n  const [moreOpen, setMoreOpen] = useState(false);\n  const set = (key: keyof Filters, v: string) => onValueChange({ ...value, [key]: v });\n\n  // ONE chip per applied filter — inline AND hidden ones, so nothing applied is out of sight.\n  const chips = Object.entries(LABELS)\n    .filter(([key]) => value[key as keyof Filters] !== \"\")\n    .map(([key, f]) => ({\n      value: key,\n      label: `${f.label}: ${f.options.find((o) => o.value === value[key as keyof Filters])?.label ?? \"\"}`,\n    }));\n  if (value.q) chips.unshift({ value: \"q\", label: `Search: ${value.q}` });\n  const hiddenApplied = MORE.filter((f) => value[f.key] !== \"\").length;\n\n  return (\n    <FilterBar\n      search={{ value: value.q, onValueChange: (v) => set(\"q\", v), placeholder: \"Search orders\", ariaLabel: \"Search orders\" }}\n      // The 2–4 most-used filters, INLINE. Typed filters wire each caption as the real <label>.\n      filters={[\n        { value: \"status\", label: \"Status\", options: STATUS, selected: value.status, onSelectedChange: (v) => set(\"status\", v), placeholder: \"All\" },\n        { value: \"owner\", label: \"Owner\", options: OWNER, selected: value.owner, onSelectedChange: (v) => set(\"owner\", v), placeholder: \"Anyone\" },\n      ]}\n      chips={chips}\n      onChipRemove={(key) => set(key as keyof Filters, \"\")}\n      hasActiveFilters={chips.length > 0}\n      onClear={() => onValueChange(NO_FILTERS)}\n      resultCount={resultCount}\n    >\n      {/* Children render after the typed filters, before the reset: ONE \"More filters\" control.\n          count = hidden filters applied, so the closed sheet still says so. Button paints a 0 pill by\n          default (showZero = true) — showZero={false} hides it when nothing hidden is applied (#1333). */}\n      <Sheet open={moreOpen} onOpenChange={setMoreOpen}>\n        <SheetTrigger asChild>\n          <Button variant=\"outline\" count={hiddenApplied} showZero={false} countLabel=\"applied\">\n            <SlidersHorizontal aria-hidden=\"true\" />\n            More filters\n          </Button>\n        </SheetTrigger>\n        <SheetContent side=\"right\">\n          <SheetHeader>\n            <SheetTitle>More filters</SheetTitle>\n            <SheetDescription>Applied filters also show as chips above the list.</SheetDescription>\n          </SheetHeader>\n          <SheetBody>\n            {/* A Sheet scrolls VERTICALLY; the fields stack. The same FilterBarGroup labels them. */}\n            <Flex direction=\"col\" gap=\"md\">\n              {MORE.map((f) => (\n                <FilterBarGroup key={f.key} label={f.label} controlId={`more-${f.key}`}>\n                  <Select value={value[f.key]} onValueChange={(v: string) => set(f.key, v)}>\n                    <SelectTrigger id={`more-${f.key}`}><SelectValue placeholder=\"All\" /></SelectTrigger>\n                    <SelectContent>\n                      {f.options.map((o) => <SelectItem key={o.value} value={o.value}>{o.label}</SelectItem>)}\n                    </SelectContent>\n                  </Select>\n                </FilterBarGroup>\n              ))}\n            </Flex>\n          </SheetBody>\n          <SheetFooter>\n            <SheetClose asChild><Button>Show results</Button></SheetClose>\n          </SheetFooter>\n        </SheetContent>\n      </Sheet>\n    </FilterBar>\n  );\n}\n// Phone width: the bar stacks (search, each filter, More filters, reset) and the chips wrap.\n// A row of filter CHIPS (ToggleGroup) takes `wrap` — it does not scroll sideways either.\n// Few filters (≤ 4)? Drop the Sheet: the plain bar in the data-table-page pattern is enough.",
    "name": "list-filters",
    "tagline": "A list page with MANY filters (cardinal rule #51) — FilterBar WRAPS; the 2–4 most-used filters stay inline, the rest sit behind ONE 'More filters' Button (count = hidden filters applied) that opens a Sheet; every applied filter is a removable chip. Never a sideways-scrolling strip.",
    "tags": [
      "filter",
      "filters",
      "list",
      "toolbar",
      "filter-bar",
      "sheet",
      "chips",
      "search",
      "responsive",
      "datatable"
    ]
  },
  {
    "aliases": [
      "filter-layouts",
      "faceted-filter",
      "faceted-sidebar",
      "saved-views-filter",
      "docked-filter-panel",
      "filter-panel",
      "filter-menu"
    ],
    "code": "// Live gallery: docs page navigation/filter-bar-layouts (every layout working, JA/EN/VI data,\n// measured at 1280 and 390: filters 12px apart, no sideways overflow).\n//\n// THE ONE RULE ALL ELEVEN SHARE: filters are PACKED at the start of the bar with its one gap.\n// The only element at the far end is FilterBar's actions slot (with the clear button). A Flex/Card\n// of FormFields above a table — worst with justify=\"between\" — is ui-audit hand-rolled-filter-bar.\n//\n// a  inline packed ........ 2–4 frequent filters · Ant Design ProTable query form (collapsed)\n//    <FilterBar search={…} filters={[…]} resultCount={n} onClear={…}> + FilterBarGroup(Switch)\n// b  search + Filter menu .. search does most of the work · Linear, GitHub issues\n// c  promoted + More ....... more than 2–4 filters · Shopify Polaris IndexFilters → pattern list-filters\n// d  saved views ........... a view = a whole filter set · Polaris IndexTable views, Jira\n//    views={<Tabs controls=\"results-id\">…<TabsTrigger count={n} countLabel=\"件\">…} on FilterBar\n// e  two rows .............. the query is the main input · Gmail, Amazon\n//    <Flex direction=\"col\" gap=\"sm\"><SearchInput …/><FilterBar filters={[…]}>…</FilterBar></Flex>\n// f  actions at the end .... page actions + pinned bar · ProTable toolbar, Stripe lists\n//    <FilterBar sticky search filters actions={<><Button>CSV</Button><Button>追加</Button></>} />\n// g  faceted sidebar ....... many multi-select facets with counts · Amazon, Rakuten\n// h  compact in a card ..... a secondary table owns its filters · GitHub file / Actions tables\n//    <Card><CardContent><Flex direction=\"col\" gap=\"sm\"><CardTitle/><FilterBar clearDisplay=\"icon\">…\n//    </FilterBar></Flex></CardContent><CardContent flush><DataTable density=\"compact\"/></CardContent></Card>\n//    (FilterBar has no size prop; density comes from the app/PageContainer density axis.)\n// i  date range ............ time is the first filter · Stripe, Google Analytics\n//    <FilterBarGroup label=\"期間\"><Segmented options={7日/30日/四半期/期間指定} /></FilterBarGroup>\n//    + <DatePicker range /> in a second FilterBarGroup only when 期間指定 is picked\n// j  phone ................. below 640px every FilterBar stacks; extra filters in <SheetContent side=\"bottom\">\n// k  docked panel .......... long sessions on one table · Airtable/Notion view filters, Jira, Gmail\n//\n// SUBMIT-DRIVEN SHAPES (docs page navigation/search-form) — when a live bar is the wrong tool:\n// l  検索条件フォーム ....... many conditions / heavy query / users expect 検索 · Ant Design\n//    ProComponents QueryFilter → component SearchForm, pattern search-form\n// m  condition builder ...... field / operator / value rows, all · any · \"1 AND (2 OR 3)\", saved\n//    views · kintone filters, Salesforce filter logic → pattern condition-builder\n// n  advanced search ........ label | input | what the row means · Google Advanced Search → pattern search-form\n// o  list view + docked panel  list-view picker, pin, search, Filters panel · Salesforce Lightning\n//    list views → pattern condition-builder\n//\n// Below: b, g and k in full — the three that compose more than FilterBar.\nimport * as React from \"react\";\nimport { ListFilter, PanelRightClose, PanelRightOpen, SlidersHorizontal } from \"lucide-react\";\nimport { Card, CardContent, CardHeader, CardTitle, DataTable, type ColumnDef } from \"@godxjp/ui/data-display\";\nimport { Checkbox } from \"@godxjp/ui/data-entry\";\nimport {\n  Sheet, SheetBody, SheetClose, SheetContent, SheetFooter, SheetHeader, SheetTitle, SheetTrigger,\n} from \"@godxjp/ui/feedback\";\nimport { Button } from \"@godxjp/ui/general\";\nimport { useMediaQuery } from \"@godxjp/ui/hooks\";\nimport { Flex, MasterDetail, ResizableHandle, ResizablePanel, ResizablePanelGroup } from \"@godxjp/ui/layout\";\nimport {\n  DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuLabel, DropdownMenuTrigger,\n  FilterBar, FilterBarGroup,\n} from \"@godxjp/ui/navigation\";\nimport type { PanelImperativeHandle } from \"react-resizable-panels\";\n\ntype Row = { id: string; name: string; country: string };\ntype Option = { value: string; label: string };\nconst COUNTRIES: Option[] = [{ value: \"vn\", label: \"Việt Nam\" }, { value: \"ph\", label: \"Philippines\" }];\nconst toggle = (list: string[], v: string, on: boolean) => (on ? [...list, v] : list.filter((x) => x !== v));\n\n// ── b. Search first + ONE Filter menu + chips (Linear / GitHub) ──────────────────────────────\nexport function SearchFirstMenu({ q, setQ, countries, setCountries }: {\n  q: string; setQ: (v: string) => void; countries: string[]; setCountries: (v: string[]) => void;\n}) {\n  const chips = countries.map((c) => ({ value: c, label: \"国: \" + COUNTRIES.find((o) => o.value === c)?.label }));\n  return (\n    <FilterBar\n      search={{ value: q, onValueChange: setQ, placeholder: \"Search\", ariaLabel: \"Search\" }}\n      chips={chips}\n      onChipRemove={(v) => setCountries(countries.filter((c) => c !== v))}\n      hasActiveFilters={chips.length > 0 || q !== \"\"}\n      onClear={() => { setQ(\"\"); setCountries([]); }}\n      clearDisplay=\"icon\"\n    >\n      <DropdownMenu>\n        <DropdownMenuTrigger asChild>\n          <Button variant=\"outline\" count={chips.length} showZero={false} countLabel=\"filters applied\">\n            <ListFilter aria-hidden=\"true\" />Filter\n          </Button>\n        </DropdownMenuTrigger>\n        <DropdownMenuContent>\n          <DropdownMenuLabel>国 · Country</DropdownMenuLabel>\n          {COUNTRIES.map((c) => (\n            <DropdownMenuCheckboxItem key={c.value} checked={countries.includes(c.value)}\n              onCheckedChange={(on) => setCountries(toggle(countries, c.value, on === true))}>\n              {c.label}\n            </DropdownMenuCheckboxItem>\n          ))}\n        </DropdownMenuContent>\n      </DropdownMenu>\n    </FilterBar>\n  );\n}\n\n// Facets: a column of FilterBarGroups holding checkboxes — shared by g and k.\nfunction Facets({ countries, setCountries }: { countries: string[]; setCountries: (v: string[]) => void }) {\n  return (\n    <FilterBarGroup label=\"国 · Country\">\n      <Flex direction=\"col\" gap=\"xs\">\n        {COUNTRIES.map((c) => (\n          <Checkbox key={c.value} checked={countries.includes(c.value)}\n            onCheckedChange={(on) => setCountries(toggle(countries, c.value, on === true))}>\n            {c.label}\n          </Checkbox>\n        ))}\n      </Flex>\n    </FilterBarGroup>\n  );\n}\n\n// ── g. Faceted sidebar (Amazon / Rakuten): MasterDetail rail on the left; a Sheet when narrow ─\nexport function FacetedList({ rows, columns, countries, setCountries }: {\n  rows: Row[]; columns: ColumnDef<Row>[]; countries: string[]; setCountries: (v: string[]) => void;\n}) {\n  const wide = useMediaQuery(\"(min-width: 768px)\");\n  const table = <Card><CardContent flush><DataTable data={rows} columns={columns} getRowId={(r) => r.id} /></CardContent></Card>;\n  if (wide) {\n    return (\n      <MasterDetail rail=\"master\" railWidth=\"narrow\" collapseBelow={false} masterLabel=\"絞り込み\"\n        detailLabel=\"一覧\" master={<Facets countries={countries} setCountries={setCountries} />}>\n        {table}\n      </MasterDetail>\n    );\n  }\n  return (\n    <Flex direction=\"col\" gap=\"sm\">\n      <FilterBar hasActiveFilters={countries.length > 0} onClear={() => setCountries([])}>\n        <Sheet>\n          <SheetTrigger asChild>\n            <Button variant=\"outline\" count={countries.length} showZero={false} countLabel=\"filters applied\">\n              <SlidersHorizontal aria-hidden=\"true\" />絞り込み\n            </Button>\n          </SheetTrigger>\n          <SheetContent side=\"left\">\n            <SheetHeader><SheetTitle>絞り込み</SheetTitle></SheetHeader>\n            <SheetBody><Facets countries={countries} setCountries={setCountries} /></SheetBody>\n            <SheetFooter><SheetClose asChild><Button>結果を表示</Button></SheetClose></SheetFooter>\n          </SheetContent>\n        </Sheet>\n      </FilterBar>\n      {table}\n    </Flex>\n  );\n}\n\n// ── k. Docked filter panel (Airtable / Jira / Gmail side panel) ──────────────────────────────\n// Persistent (not an overlay), resizable, collapsible, remembers open/closed; chips stay in the\n// bar above the table; below 1024px the same facets open in a Sheet instead.\nconst DOCK_KEY = \"my-app:list-filter-panel-open\";\nexport function DockedFilterList({ rows, columns, countries, setCountries }: {\n  rows: Row[]; columns: ColumnDef<Row>[]; countries: string[]; setCountries: (v: string[]) => void;\n}) {\n  const docked = useMediaQuery(\"(min-width: 1024px)\");\n  const panelRef = React.useRef<PanelImperativeHandle | null>(null);\n  const [open, setOpen] = React.useState(() => {\n    try { return localStorage.getItem(DOCK_KEY) !== \"false\"; } catch { return true; }\n  });\n  const remember = (next: boolean) => {\n    setOpen(next);\n    try { localStorage.setItem(DOCK_KEY, String(next)); } catch { /* not remembered, still works */ }\n  };\n  const chips = countries.map((c) => ({ value: c, label: \"国: \" + COUNTRIES.find((o) => o.value === c)?.label }));\n  const table = <Card><CardContent flush><DataTable data={rows} columns={columns} getRowId={(r) => r.id} /></CardContent></Card>;\n  return (\n    <Flex direction=\"col\" gap=\"sm\">\n      <FilterBar\n        chips={chips}\n        onChipRemove={(v) => setCountries(countries.filter((c) => c !== v))}\n        hasActiveFilters={chips.length > 0}\n        onClear={() => setCountries([])}\n        actions={docked ? (\n          <Button variant=\"outline\" size=\"sm\" aria-expanded={open} aria-controls=\"list-filter-panel\"\n            count={chips.length} showZero={false} countLabel=\"filters applied\"\n            onClick={() => { if (open) panelRef.current?.collapse(); else panelRef.current?.expand(); remember(!open); }}>\n            {open ? <PanelRightClose aria-hidden=\"true\" /> : <PanelRightOpen aria-hidden=\"true\" />}\n            {open ? \"フィルターパネルを閉じる\" : \"フィルターパネルを開く\"}\n          </Button>\n        ) : (\n          <Sheet>\n            <SheetTrigger asChild>\n              <Button variant=\"outline\" size=\"sm\" count={chips.length} showZero={false} countLabel=\"filters applied\">\n                <SlidersHorizontal aria-hidden=\"true\" />フィルター\n              </Button>\n            </SheetTrigger>\n            <SheetContent side=\"right\">\n              <SheetHeader><SheetTitle>フィルター</SheetTitle></SheetHeader>\n              <SheetBody><Facets countries={countries} setCountries={setCountries} /></SheetBody>\n            </SheetContent>\n          </Sheet>\n        )}\n      />\n      {docked ? (\n        <ResizablePanelGroup orientation=\"horizontal\">\n          <ResizablePanel id=\"list-main\" defaultSize=\"72%\" minSize=\"50%\">{table}</ResizablePanel>\n          <ResizableHandle />\n          <ResizablePanel id=\"list-filter-panel\" panelRef={panelRef} collapsible collapsedSize=\"0%\"\n            defaultSize={open ? \"28%\" : \"0%\"} minSize=\"20%\" maxSize=\"40%\"\n            onResize={(size) => { const next = size.asPercentage > 0; if (next !== open) remember(next); }}>\n            {/* Render nothing while collapsed: content squeezed to 0px wraps one glyph per line. */}\n            {open && (\n              <Card>\n                <CardHeader><CardTitle level={3}>フィルター</CardTitle></CardHeader>\n                <CardContent><Facets countries={countries} setCountries={setCountries} /></CardContent>\n              </Card>\n            )}\n          </ResizablePanel>\n        </ResizablePanelGroup>\n      ) : table}\n    </Flex>\n  );\n}\n// SplitPane is the non-resizable alternative for k (its aside is always inline-end; aside={null}\n// closes it). A LEFT docked panel is g's MasterDetail rail=\"master\".",
    "name": "filter-bar-layouts",
    "tagline": "Eleven list-filter layouts from leading products, all FilterBar + existing primitives: (a) inline packed, (b) search-first + ONE Filter menu + chips, (c) promoted + More-filters Sheet, (d) saved views above the bar, (e) wide search then filters, (f) actions at the end + sticky, (g) faceted sidebar, (h) compact bar in a table card, (i) date range + quick ranges, (j) phone stack + Sheet, (k) docked side panel. Filters always pack at the START.",
    "tags": [
      "filter",
      "filters",
      "layout",
      "faceted",
      "facets",
      "saved views",
      "docked",
      "panel",
      "sidebar",
      "chips",
      "date range",
      "mobile",
      "list",
      "datatable"
    ]
  },
  {
    "aliases": [
      "検索条件",
      "検索フォーム",
      "query-filter",
      "query filter",
      "advanced search",
      "advanced-search",
      "search-panel"
    ],
    "code": "// Live examples: docs page navigation/search-form (l = SearchForm label-top and label-left,\n// n = advanced-search form). Research: Ant Design ProComponents QueryFilter (defaultCollapsed\n// true, defaultFormItemsNumber, layout vertical/horizontal, labelWidth, search/reset text);\n// Google Advanced Search (rows: all these words / this exact phrase / any of these words /\n// none of these words / numbers ranging from; then narrow by language, region, last update,\n// site, terms appearing, file type).\n//\n// WHICH SHAPE (rule #51): few filters applied as they change → FilterBar. Many conditions, a heavy\n// query, or users who expect a 検索 button → SearchForm. Arbitrary field × operator conditions,\n// all/any/custom logic, saved views → pattern condition-builder.\nimport { useState, type FormEvent } from \"react\";\nimport { Card, CardContent, DataTable, type ColumnDef } from \"@godxjp/ui/data-display\";\nimport { DatePicker, FormField, Input, NumberInput, RadioGroup, Select } from \"@godxjp/ui/data-entry\";\nimport { Flex } from \"@godxjp/ui/layout\";\nimport { Text } from \"@godxjp/ui/general\";\nimport { SearchForm } from \"@godxjp/ui/navigation\";\n\ntype Option = { value: string; label: string };\ntype Conditions = {\n  name: string; match: \"partial\" | \"exact\"; nationality: string; occupation: string;\n  monthsMin: number | null; monthsMax: number | null;\n};\nconst EMPTY: Conditions = { name: \"\", match: \"partial\", nationality: \"\", occupation: \"\", monthsMin: null, monthsMax: null };\n\nexport function TraineeSearch<T extends { id: string }>({ rows, columns, nationalities, occupations, search }: {\n  rows: T[]; columns: ColumnDef<T>[]; nationalities: Option[]; occupations: Option[];\n  search: (c: Conditions) => T[];\n}) {\n  // Inputs edit the DRAFT; 検索 copies it to APPLIED; クリア empties both.\n  const [draft, setDraft] = useState<Conditions>(EMPTY);\n  const [applied, setApplied] = useState<Conditions>(EMPTY);\n  const set = (patch: Partial<Conditions>) => setDraft((cur) => ({ ...cur, ...patch }));\n  const results = search(applied);\n  const summary = [\n    applied.name && \"氏名: \" + applied.name + (applied.match === \"exact\" ? \"（完全一致）\" : \"（部分一致）\"),\n    applied.nationality && \"国籍: \" + nationalities.find((o) => o.value === applied.nationality)?.label,\n    (applied.monthsMin != null || applied.monthsMax != null) && \"在籍月数: \" + (applied.monthsMin ?? \"\") + \"〜\" + (applied.monthsMax ?? \"\"),\n  ].filter(Boolean).join(\" / \") || \"指定なし\";\n\n  return (\n    <Flex direction=\"col\" gap=\"md\">\n      <Card>\n        <CardContent>\n          {/* layout=\"horizontal\" labelWidth=\"7rem\" columns={{ base: 1, lg: 2 }} = label-left 表形式 */}\n          <SearchForm\n            controlWidth=\"100%\"\n            onSubmit={(e: FormEvent) => { e.preventDefault(); setApplied(draft); }}\n            onReset={() => { setDraft(EMPTY); setApplied(EMPTY); }}\n            summary={summary}\n            resultCount={results.length}\n          >\n            <FormField label=\"氏名\" id=\"ts-name\">\n              <Flex direction=\"col\" gap=\"xs\">\n                <Input id=\"ts-name\" value={draft.name} onChange={(e) => set({ name: e.target.value })} />\n                <RadioGroup aria-label=\"一致条件\" orientation=\"horizontal\" value={draft.match}\n                  options={[{ value: \"partial\", label: \"部分一致\" }, { value: \"exact\", label: \"完全一致\" }]}\n                  onValueChange={(v) => set({ match: v === \"exact\" ? \"exact\" : \"partial\" })} />\n              </Flex>\n            </FormField>\n            <FormField label=\"国籍\" id=\"ts-nat\">\n              <Select id=\"ts-nat\" options={nationalities} value={draft.nationality} placeholder=\"すべて\" allowClear\n                onValueChange={(v: string) => set({ nationality: v })} />\n            </FormField>\n            <FormField label=\"入国日\" id=\"ts-entry\">\n              <DatePicker id=\"ts-entry\" range />\n            </FormField>\n            {/* past the first row: behind 詳細条件を開く, still mounted */}\n            <FormField label=\"在籍月数\" id=\"ts-min\">\n              <Flex gap=\"xs\" align=\"center\">\n                <NumberInput id=\"ts-min\" aria-label=\"下限\" min={0} value={draft.monthsMin} onValueChange={(v) => set({ monthsMin: v })} />\n                <Text tone=\"muted\">〜</Text>\n                <NumberInput aria-label=\"上限\" min={0} value={draft.monthsMax} onValueChange={(v) => set({ monthsMax: v })} />\n              </Flex>\n            </FormField>\n            <FormField label=\"職種\" id=\"ts-occ\">\n              <Select id=\"ts-occ\" options={occupations} value={draft.occupation} placeholder=\"すべて\" allowClear\n                onValueChange={(v: string) => set({ occupation: v })} />\n            </FormField>\n          </SearchForm>\n        </CardContent>\n      </Card>\n      <Card><CardContent flush><DataTable data={results} columns={columns} getRowId={(r) => r.id} /></CardContent></Card>\n    </Flex>\n  );\n}\n\n// ADVANCED-SEARCH SHAPE (Google): one <Form role=\"search\" layout=\"horizontal\" labelWidth=\"12rem\"\n// controlWidth=\"100%\" onSubmit>; each row is <ResponsiveGrid columns={{ base: 1, lg: 2 }}> holding\n// the FormField and a <Text tone=\"muted\"> that says which search-box syntax the row stands for\n// (\"Put a minus sign before words you don't want: -withdrawn\"); two <Heading level={3}> sections\n// (find… / then narrow the results by…); ONE submit Button at the end of the form, and the\n// equivalent query string shown with the result count after the search.",
    "name": "search-form",
    "tagline": "「検索条件フォーム」 — SearchForm (Ant Design ProComponents QueryFilter): labelled conditions in aligned columns (label-top or label-left 「表形式」), ranges From〜To, 「部分一致」/「完全一致」, explicit 「検索」 + 「クリア」 (search on SUBMIT), the rest behind 「詳細条件を開く」, then a 「検索条件」 summary + result count. Plus the Google Advanced Search shape (label | input | what the row means).",
    "tags": [
      "search",
      "search form",
      "query",
      "filter",
      "検索",
      "検索条件",
      "japanese",
      "business",
      "advanced search",
      "list",
      "datatable"
    ]
  },
  {
    "aliases": [
      "絞り込み条件",
      "条件ビルダー",
      "filter-builder",
      "query-builder",
      "kintone filter",
      "kintone-filter",
      "salesforce list view filter",
      "salesforce-filter",
      "filter logic",
      "list-view-filter"
    ],
    "code": "// Live examples: docs page navigation/search-form (m = builder, o = list view with the builder in a\n// docked panel). Research: kintone's filter rows are field / operator / value (「次のいずれかを\n// 含む」, ≦ …) combined with 「すべての条件を満たす」, conditions added with 「絞り込み条件を\n// 追加」; the kintone query language joins conditions with and / or and groups them with\n// parentheses, sorted by order by. Salesforce numbers each filter (Filter 1, 2, …) and combines\n// them with AND / OR / NOT, e.g. (1 AND 2) OR 3; list views are switched from a picker, pinned\n// as the default, and edited in a Filters panel (Add Filter → field / operator / value).\n//\n// A COMPOSITION, not a component: only one of the design systems the library follows names a\n// condition builder, the operators are domain policy, and every piece is an existing primitive.\nimport { useRef, useState } from \"react\";\nimport { Minus, Plus } from \"lucide-react\";\nimport { Badge } from \"@godxjp/ui/data-display\";\nimport { Form, FormField, Input, NumberInput, RadioGroup, Select } from \"@godxjp/ui/data-entry\";\nimport { Button } from \"@godxjp/ui/general\";\nimport { Flex, ResponsiveGrid } from \"@godxjp/ui/layout\";\n\ntype Kind = \"text\" | \"choice\" | \"number\";\ntype Field = { value: string; label: string; kind: Kind; options?: { value: string; label: string }[] };\ntype Row = { id: number; field: string; operator: string; value: string };\nexport type Conditions = { match: \"all\" | \"any\" | \"custom\"; logic: string; rows: Row[] };\n\nconst OPERATORS: Record<Kind, { value: string; label: string }[]> = {\n  text: [{ value: \"contains\", label: \"次のキーワードを含む\" }, { value: \"notContains\", label: \"次のキーワードを含まない\" }, { value: \"eq\", label: \"＝\" }, { value: \"neq\", label: \"≠\" }],\n  choice: [{ value: \"in\", label: \"次のいずれかを含む\" }, { value: \"notIn\", label: \"次のいずれも含まない\" }],\n  number: [{ value: \"eq\", label: \"＝\" }, { value: \"neq\", label: \"≠\" }, { value: \"gte\", label: \"≥（以上）\" }, { value: \"lte\", label: \"≤（以下）\" }],\n};\n\n/** Numbered-row logic: \"1 AND (2 OR 3)\", \"NOT 2\". null = malformed → show the field error. */\nexport function compileLogic(expr: string, count: number): ((hits: boolean[]) => boolean) | null {\n  const tokens = expr.toUpperCase().match(/\\d+|AND|OR|NOT|\\(|\\)|\\S/g) ?? [];\n  let pos = 0;\n  type Node = (h: boolean[]) => boolean;\n  const or = (): Node | null => { let l = and(); while (l && tokens[pos] === \"OR\") { pos++; const r = and(); if (!r) return null; const a = l; l = (h) => a(h) || r(h); } return l; };\n  const and = (): Node | null => { let l = not(); while (l && tokens[pos] === \"AND\") { pos++; const r = not(); if (!r) return null; const a = l; l = (h) => a(h) && r(h); } return l; };\n  const not = (): Node | null => {\n    if (tokens[pos] === \"NOT\") { pos++; const i = not(); return i ? (h) => !i(h) : null; }\n    if (tokens[pos] === \"(\") { pos++; const i = or(); if (!i || tokens[pos] !== \")\") return null; pos++; return i; }\n    const n = Number(tokens[pos]);\n    if (!Number.isInteger(n) || n < 1 || n > count) return null;\n    pos++; return (h) => h[n - 1] ?? false;\n  };\n  const tree = tokens.length ? or() : null;\n  return tree && pos === tokens.length ? tree : null;\n}\n\nexport function ConditionBuilder({ fields, value, onApply }: {\n  fields: Field[]; value: Conditions; onApply: (c: Conditions) => void;\n}) {\n  const [draft, setDraft] = useState(value);\n  const nextId = useRef(Math.max(0, ...value.rows.map((r) => r.id)) + 1);\n  const valid = draft.match !== \"custom\" || compileLogic(draft.logic, draft.rows.length) !== null;\n  const patch = (id: number, p: Partial<Row>) => setDraft((c) => ({ ...c, rows: c.rows.map((r) => (r.id === id ? { ...r, ...p } : r)) }));\n  const kindOf = (f: string) => fields.find((x) => x.value === f)?.kind ?? \"text\";\n  /** kintone's ＋ adds a blank condition right after its own row; null appends one. */\n  const insertAfter = (rows: Row[], afterId: number | null): Row[] => {\n    const blank: Row = { id: nextId.current++, field: fields[0].value, operator: OPERATORS[fields[0].kind][0].value, value: \"\" };\n    const at = afterId === null ? rows.length : rows.findIndex((r) => r.id === afterId) + 1;\n    return [...rows.slice(0, at), blank, ...rows.slice(at)];\n  };\n\n  // role=\"search\" + a real submit: neither form-on-list-page nor hand-rolled-filter-bar flags it.\n  return (\n    <Form role=\"search\" aria-label=\"絞り込み条件\" controlWidth=\"100%\"\n      onSubmit={(e) => { e.preventDefault(); if (valid) onApply(draft); }}>\n      <Flex direction=\"col\" gap=\"md\">\n        <FormField label=\"条件の組み合わせ\">\n          <RadioGroup aria-label=\"条件の組み合わせ\" orientation=\"horizontal\" value={draft.match}\n            options={[{ value: \"all\", label: \"すべての条件を満たす\" }, { value: \"any\", label: \"いずれかの条件を満たす\" }, { value: \"custom\", label: \"条件式を指定\" }]}\n            onValueChange={(v) => setDraft((c) => ({ ...c, match: v as Conditions[\"match\"] }))} />\n        </FormField>\n        {draft.rows.map((row, i) => {\n          const field = fields.find((f) => f.value === row.field);\n          const n = i + 1;\n          return (\n            // ONE line per condition, kintone-style: [No.] [field] [operator] [value] [＋][−].\n            // Container grid: 24 tracks from lg (field 6, operator 5 — narrower but wide enough for\n            // 「次のキーワードを含まない」 — value 10), 12 at md (the operator gets the extra track so its\n            // longest label is not cut). Below md (a docked panel, a phone) it is 4 tracks and\n            // `dense` backfills the ＋/− cell beside the number: No. and buttons on the first line,\n            // field / operator / value stacked below. align=\"center\" = antd Row align=\"middle\".\n            <Flex key={row.id} direction=\"col\" role=\"group\" aria-label={\"条件 \" + n}>\n              <ResponsiveGrid columns={{ base: 4, md: 12, lg: 24 }} gap=\"sm\" align=\"center\" dense>\n                <ResponsiveGrid.Item span={{ base: 1, md: 1, lg: 1 }}>\n                  <Badge variant=\"outline\">{n}</Badge>\n                </ResponsiveGrid.Item>\n                <ResponsiveGrid.Item span={{ base: 4, md: 3, lg: 6 }}>\n                  <Select aria-label={\"条件 \" + n + \" · フィールド\"} options={fields} value={row.field}\n                    onValueChange={(f: string) => patch(row.id, { field: f, operator: OPERATORS[kindOf(f)][0].value, value: \"\" })} />\n                </ResponsiveGrid.Item>\n                <ResponsiveGrid.Item span={{ base: 4, md: 4, lg: 5 }}>\n                  <Select aria-label={\"条件 \" + n + \" · 演算子\"} options={OPERATORS[kindOf(row.field)]} value={row.operator}\n                    onValueChange={(op: string) => patch(row.id, { operator: op })} />\n                </ResponsiveGrid.Item>\n                <ResponsiveGrid.Item span={{ base: 4, md: 3, lg: 10 }}>\n                  {field?.kind === \"choice\" ? (\n                    <Select aria-label={\"条件 \" + n + \" · 値\"} options={field.options ?? []} value={row.value}\n                      onValueChange={(v: string) => patch(row.id, { value: v })} />\n                  ) : field?.kind === \"number\" ? (\n                    <NumberInput aria-label={\"条件 \" + n + \" · 値\"} value={row.value === \"\" ? null : Number(row.value)}\n                      onValueChange={(v) => patch(row.id, { value: v == null ? \"\" : String(v) })} />\n                  ) : (\n                    <Input aria-label={\"条件 \" + n + \" · 値\"} value={row.value} onChange={(e) => patch(row.id, { value: e.target.value })} />\n                  )}\n                </ResponsiveGrid.Item>\n                <ResponsiveGrid.Item span={{ base: 3, md: 1, lg: 2 }}>\n                  <Flex gap=\"xs\" justify=\"end\">\n                    <Button type=\"button\" variant=\"ghost\" size=\"icon-sm\" aria-label={\"条件 \" + n + \" の後に条件を追加\"}\n                      onClick={() => setDraft((c) => ({ ...c, rows: insertAfter(c.rows, row.id) }))}>\n                      <Plus aria-hidden=\"true\" />\n                    </Button>\n                    <Button type=\"button\" variant=\"ghost\" size=\"icon-sm\" aria-label={\"条件 \" + n + \" を削除\"}\n                      onClick={() => setDraft((c) => ({ ...c, rows: c.rows.filter((r) => r.id !== row.id) }))}>\n                      <Minus aria-hidden=\"true\" />\n                    </Button>\n                  </Flex>\n                </ResponsiveGrid.Item>\n              </ResponsiveGrid>\n            </Flex>\n          );\n        })}\n        {/* With every row removed there is no ＋ left to press: offer one. */}\n        {draft.rows.length === 0 && (\n          <Flex>\n            <Button type=\"button\" variant=\"outline\" size=\"sm\"\n              onClick={() => setDraft((c) => ({ ...c, rows: insertAfter(c.rows, null) }))}>\n              <Plus aria-hidden=\"true\" />条件を追加\n            </Button>\n          </Flex>\n        )}\n        {draft.match === \"custom\" && (\n          <FormField label=\"条件式\" id=\"cb-logic\" helper=\"番号付きの条件を AND・OR・NOT と括弧で組み合わせます（例: 1 AND (2 OR 3)）。\"\n            error={valid ? undefined : \"存在する条件の番号、AND・OR・NOT、対応の取れた括弧だけを使ってください。\"}>\n            <Input id=\"cb-logic\" value={draft.logic} onChange={(e) => setDraft((c) => ({ ...c, logic: e.target.value }))} />\n          </FormField>\n        )}\n        <Flex gap=\"sm\" justify=\"end\" wrap>\n          <Button type=\"button\" variant=\"ghost\" onClick={() => setDraft(value)}>変更を元に戻す</Button>\n          <Button type=\"submit\" disabled={!valid}>適用</Button>\n        </Flex>\n      </Flex>\n    </Form>\n  );\n}\n// Add 並び順 (Select + Segmented 昇順/降順) and \"save as view\" (Input + Button) as two more\n// FormFields when the screen keeps saved views. Docked beside the list (Salesforce list view):\n// <SplitPane asideWidth=\"lg\" aside={open ? <Card>…<ConditionBuilder/>…</Card> : null}> with a\n// counted Filters toggle in FilterBar's actions; below 1024px the same builder opens in a Sheet.",
    "name": "condition-builder",
    "tagline": "Condition builder — numbered rows of field / operator / value, 「すべての条件を満たす」 / 「いずれかの条件を満たす」 / custom logic `1 AND (2 OR 3)`, add/remove rows, 「並び順」, save as a named view; applied on SUBMIT inside a role=\"search\" Form. Composition, not a component. Docked beside the list it is the Salesforce list-view filter panel.",
    "tags": [
      "filter",
      "condition",
      "builder",
      "query",
      "logic",
      "and",
      "or",
      "kintone",
      "salesforce",
      "saved view",
      "絞り込み",
      "list"
    ]
  },
  {
    "aliases": [
      "organization-switcher",
      "workspace-switcher",
      "received-invitations",
      "memberships"
    ],
    "code": "// Research basis: GitHub organization membership + Slack workspace switching/invitations.\n// Each row = recognizable identity (Avatar/logo, NOT an identical decorative icon) + name +\n// a \"現在\" (Current) badge for the active org + role + a permission-aware action menu.\nimport { Card, CardContent, CardHeader, CardTitle, ListRow, Badge, Avatar, AvatarImage, AvatarFallback, EmptyState } from \"@godxjp/ui/data-display\";\nimport { Button } from \"@godxjp/ui/general\";\nimport { Flex } from \"@godxjp/ui/layout\";\nimport { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from \"@godxjp/ui/navigation\";\nimport { MoreHorizontal, Plus } from \"lucide-react\";\n\ntype Role = \"owner\" | \"admin\" | \"member\";\ntype Membership = { id: string; name: string; logoUrl?: string; role: Role; joinedAt: string; isCurrent: boolean; canManage: boolean };\ntype Invite = { id: string; orgName: string; invitedBy: string };\n\nconst ROLE_LABEL: Record<Role, string> = { owner: \"オーナー\", admin: \"管理者\", member: \"メンバー\" };\n\nexport function OrganizationMemberships({\n  memberships, invitations, onOpen, onSwitch, onManage, onLeave, onCreate, onAccept, onDecline,\n}: {\n  memberships: Membership[]; invitations: Invite[];\n  onOpen: (id: string) => void; onSwitch: (id: string) => void; onManage: (id: string) => void;\n  onLeave: (id: string) => void; onCreate: () => void; onAccept: (id: string) => void; onDecline: (id: string) => void;\n}) {\n  return (\n    <Flex direction=\"col\" gap=\"lg\">\n      <Card>\n        <CardHeader>\n          <Flex justify=\"between\" align=\"center\">\n            <CardTitle>組織</CardTitle>\n            {/* Create belongs at page/section level, not per row. */}\n            <Button variant=\"outline\" size=\"sm\" onClick={onCreate}><Plus className=\"size-4\" />組織を作成</Button>\n          </Flex>\n        </CardHeader>\n        {/* flush → ListRows draw their own edge-to-edge dividers. */}\n        <CardContent flush>\n          {memberships.length === 0 ? (\n            <EmptyState variant=\"section\" title=\"所属している組織がありません\" description=\"新しい組織を作成するか、招待を受け取ってください。\" />\n          ) : (\n            memberships.map((m) => (\n              <ListRow\n                key={m.id}\n                leading={\n                  <Avatar>\n                    {m.logoUrl ? <AvatarImage src={m.logoUrl} alt=\"\" /> : null}\n                    <AvatarFallback>{m.name.slice(0, 1)}</AvatarFallback>\n                  </Avatar>\n                }\n                title={\n                  <Flex gap=\"sm\" align=\"center\">\n                    <span className=\"font-medium\">{m.name}</span>\n                    {m.isCurrent && <Badge tone=\"success\" icon={null}>現在</Badge>}\n                  </Flex>\n                }\n                description={ROLE_LABEL[m.role]}\n                trailing={\n                  <Flex gap=\"sm\" align=\"center\">\n                    {m.isCurrent\n                      ? <Button size=\"sm\" onClick={() => onOpen(m.id)}>開く</Button>\n                      : <Button variant=\"outline\" size=\"sm\" onClick={() => onSwitch(m.id)}>切り替え</Button>}\n                    <DropdownMenu>\n                      <DropdownMenuTrigger asChild>\n                        <Button variant=\"ghost\" size=\"icon\" aria-label={`${m.name} の操作`}><MoreHorizontal className=\"size-4\" /></Button>\n                      </DropdownMenuTrigger>\n                      <DropdownMenuContent align=\"end\">\n                        <DropdownMenuItem onSelect={() => onOpen(m.id)}>開く</DropdownMenuItem>\n                        {m.canManage && <DropdownMenuItem onSelect={() => onManage(m.id)}>管理</DropdownMenuItem>}\n                        {m.role !== \"owner\" && <DropdownMenuItem onSelect={() => onLeave(m.id)}>退出</DropdownMenuItem>}\n                      </DropdownMenuContent>\n                    </DropdownMenu>\n                  </Flex>\n                }\n              />\n            ))\n          )}\n        </CardContent>\n      </Card>\n\n      {/* CONDITIONAL invitations — no pending invites ⇒ render NOTHING (no permanent empty Card).\n          Few pending ⇒ a compact actionable list. Many/history ⇒ link to a focused route instead. */}\n      {invitations.length > 0 && (\n        <Card>\n          <CardHeader><CardTitle>保留中の招待</CardTitle></CardHeader>\n          <CardContent flush>\n            {invitations.map((inv) => (\n              <ListRow\n                key={inv.id}\n                title={<span className=\"font-medium\">{inv.orgName}</span>}\n                description={`${inv.invitedBy} からの招待`}\n                trailing={\n                  <Flex gap=\"sm\">\n                    <Button size=\"sm\" onClick={() => onAccept(inv.id)}>参加</Button>\n                    <Button variant=\"ghost\" size=\"sm\" onClick={() => onDecline(inv.id)}>辞退</Button>\n                  </Flex>\n                }\n              />\n            ))}\n          </CardContent>\n        </Card>\n      )}\n    </Flex>\n  );\n}\n// DON'T: passive rows with no action · identical decorative icons · unlabeled timestamps\n// (label them: \"2024年に参加\") · a large empty invitation Card shown permanently.",
    "name": "organization-memberships",
    "tagline": "Workspace membership list — recognizable identity, current/active state, role, permission-aware Open/Switch/Manage/Leave row actions, page-level Create, and CONDITIONAL invitations (omit when none).",
    "tags": [
      "organization",
      "workspace",
      "membership",
      "invitation",
      "account",
      "switcher"
    ]
  },
  {
    "aliases": [
      "password-recovery",
      "forgot-password",
      "reset-password",
      "mfa-challenge",
      "two-factor-challenge",
      "2fa-challenge",
      "otp-challenge",
      "passkey-failure",
      "scr-008"
    ],
    "code": "// ⚠️ There is NO <PasswordRecoveryPanel state=…/> and NO <MfaChallengePanel state=…/> in\n// @godxjp/ui. What the package owns is the MEASURE:\n//    <CenteredShell variant=\"auth-canonical\" preset=\"account-recovery\">   ← 432px panel, 15px gutter at 390\n// Docs page: docs/layout/auth-recovery/\n//\n// PRESENTATION ONLY. No route, no reset semantics, no OTP verification, no recovery-code\n// consumption, no passkey authentication, no permissions. The consumer owns all logic and copy.\n\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from \"@godxjp/ui/data-display\";\nimport { FormField, Input, InputOTP, InputOTPGroup, InputOTPSlot, PasswordInput, PasswordStrength } from \"@godxjp/ui/data-entry\";\nimport { Alert, AlertDescription, AlertTitle } from \"@godxjp/ui/feedback\";\nimport { Button, Reveal } from \"@godxjp/ui/general\";\nimport { AuthFooter, CenteredShell, AuthStack, Flex } from \"@godxjp/ui/layout\";\n\n// ── THE CANONICAL PANEL ANATOMY — identical for ALL SEVEN states ───────────────────────\n//   Card > CardHeader(CardTitle + CardDescription)   ← title/description INSIDE the surface\n//        > CardContent > AuthStack\n//            [notice]   Alert tone=\"success|destructive\"   (only when the state has one)\n//            [fields]   FormField(s): email · password pair · ONE 6-slot InputOTP · one Input\n//            [primary]  Button fullWidth                    (spans the panel; exactly one)\n//            [fallback] Flex justify=\"between\" gap=\"sm\" wrap (two ghost actions share ONE row)\n//\n// DON'T put <AuthIdentity> above the panel here — it always renders the hosted mark, and the\n// SCR-008 hierarchy puts the heading INSIDE the bordered surface.\n// DON'T reuse <TwoFactorSetup> — that is the ENROLLMENT Dialog (QR / manual key / recovery\n// output). A sign-in challenge has no secret to reveal and no dialog to dismiss: it IS the page.\n\n// ── SIGN-IN MFA CHALLENGE (otp) ────────────────────────────────────────────────────────\n// Focus order IS DOM order: code field → verify → fallback A → fallback B. No tabindex.\n// The 6-slot row is ONE field: InputOTP owns paste/arrows/backspace/caret and is a single tab\n// stop. Six <Input>s would put six stops in the ring and break paste of a 6-digit code.\nexport function MfaChallengePage({ code, onCodeChange, error, pending, onVerify }) {\n  return (\n    <CenteredShell variant=\"auth-canonical\"\n      preset=\"account-recovery\"\n      brand={brand}\n      footer={<AuthFooter product=\"GoDX ID\" terms={termsLink} privacy={privacyLink} locale={localePicker} />}\n    >\n      <Reveal>\n        <Card>\n          <CardHeader>\n            <CardTitle level={1}>{t(\"mfa.title\")}</CardTitle>\n            <CardDescription>{t(\"mfa.description\")}</CardDescription>\n          </CardHeader>\n          <CardContent>\n            <AuthStack>\n              <FormField id=\"mfa-code\" label={t(\"mfa.codeLabel\")} error={error} required>\n                <InputOTP maxLength={6} pattern=\"^[0-9]+$\" value={code} onChange={onCodeChange}>\n                  <InputOTPGroup>\n                    {/* six <InputOTPSlot index={0..5} /> */}\n                  </InputOTPGroup>\n                </InputOTP>\n              </FormField>\n              <Button fullWidth loading={pending} loadingText={t(\"common.verifying\")} onClick={onVerify}>\n                {t(\"mfa.verify\")}\n              </Button>\n              <Flex justify=\"between\" gap=\"sm\" wrap>\n                <Button variant=\"ghost\" size=\"sm\">{t(\"mfa.useRecoveryCode\")}</Button>\n                <Button variant=\"ghost\" size=\"sm\">{t(\"mfa.retryPasskey\")}</Button>\n              </Flex>\n            </AuthStack>\n          </CardContent>\n        </Card>\n      </Reveal>\n    </CenteredShell>\n  );\n}\n\n// ── THE OTHER SIX STATES — same anatomy, only the notice/fields/copy change ─────────────\n// The STATE IS NOT A PROP: the server answers, and the route renders the matching body in the\n// SAME 432px panel. Nothing is reconstructed, because nothing was torn down.\n//\n//   password recovery · request        —  fields: email (autoComplete=\"username\")\n//   password recovery · sent           —  <Alert tone=\"success\"> + <Button fullWidth disabled> (cooldown)\n//   password recovery · new-password   —  two <PasswordInput autoComplete=\"new-password\"> + <PasswordStrength>\n//   password recovery · expired        —  <Alert tone=\"destructive\">, primary = request a new link\n//   MFA · recovery-code                —  ONE <Input autoComplete=\"one-time-code\">\n//   MFA · passkey-failure              —  <Alert tone=\"destructive\">, primary = retry passkey\n//\n// The masked address in `sent` (\"h*****@example.co.jp\") is produced by the SERVER — the panel\n// never reconstructs an identifier, it renders the string it is given.\n\n// ── STATES ─────────────────────────────────────────────────────────────────────────────\n//  error    FormField error ⇒ aria-invalid + aria-errormessage + role=\"alert\". Never colour-only.\n//  loading  Button loading + loadingText ⇒ aria-busy, the box does not resize, fallbacks disabled.\n//  disabled a resend cooldown is <Button disabled> — keep it VISIBLE so the affordance is known.\n\n// ── RESPONSIVE CONTRACT (1440 / 1024 / 390) ────────────────────────────────────────────\n//  1440 · 1024   panel 432px, centred, 16px page gutter; OTP one row (6 × 36px = 216px)\n//  390           panel 360px at x=15 (15px inline gutter, --auth-shell-recovery-main-padding-mobile),\n//                310px content column, OTP still ONE row, primary still full-width, the fallback\n//                row wraps to a stack only when the localized labels exceed the column.\n//  ⚠️ The 390 canonical reference is a desktop 2×2 COMPOSITE that overflows\n//  and crops horizontally. It is NOT a valid mobile source and was not traced; the row above is a\n//  DECIDED contract, documented in docs/layout/auth-recovery/ and pinned by tests.\n\n// ANTI-PATTERNS: a page-local --auth-shell-card-max-width override or a .recovery-panel{width:432px}\n// class · AuthIdentity above the panel · TwoFactorSetup as a challenge · six Inputs for the OTP row ·\n// re-scoping --control-height to widen the slots (use --otp-slot-size) · two primary actions.",
    "name": "auth-recovery-panels",
    "tagline": "Signed-OUT password recovery + sign-in MFA challenge panels (SCR-008). There is NO PasswordRecoveryPanel and NO MfaChallengePanel — compose Card + AuthStack + FormField/InputOTP + Button inside CenteredShell variant=\"auth-canonical\" preset=\"account-recovery\" (the 432px token-owned measure).",
    "tags": [
      "auth",
      "authshell",
      "recovery",
      "password",
      "mfa",
      "2fa",
      "otp",
      "inputotp",
      "passkey",
      "layout",
      "presentation"
    ]
  },
  {
    "aliases": [
      "recovery-email",
      "backup-codes",
      "security-settings",
      "recovery-settings"
    ],
    "code": "// Research basis: Google Account recovery (email/phone + backup codes), Microsoft Security info,\n// GitHub recovery codes. Signed-IN recovery SETTINGS (manage your methods) is a DIFFERENT surface\n// from the signed-OUT forgot-password JOURNEY — do not mix them on one page.\nimport { Card, CardContent, CardHeader, CardTitle, ListRow, Badge } from \"@godxjp/ui/data-display\";\nimport { Button } from \"@godxjp/ui/general\";\nimport { Flex } from \"@godxjp/ui/layout\";\nimport { Mail, Smartphone, KeyRound } from \"lucide-react\";\n\ntype MethodStatus = \"verified\" | \"unverified\" | \"unavailable\";\n\nconst STATUS: Record<MethodStatus, { tone: \"success\" | \"warning\" | \"neutral\"; label: string }> = {\n  verified: { tone: \"success\", label: \"確認済み\" },\n  unverified: { tone: \"warning\", label: \"未確認\" },\n  unavailable: { tone: \"neutral\", label: \"未設定\" },\n};\n\nfunction MethodRow({ icon: Icon, name, value, status, action }: {\n  icon: React.ComponentType<{ className?: string }>; name: string; value?: string;\n  status: MethodStatus; action: React.ReactNode;\n}) {\n  const s = STATUS[status];\n  return (\n    <ListRow\n      leading={<Icon className=\"text-muted-foreground size-5\" />}\n      title={<span className=\"font-medium\">{name}</span>}\n      description={value ?? \"未設定\"}\n      trailing={<Flex gap=\"sm\" align=\"center\"><Badge tone={s.tone} icon={null}>{s.label}</Badge>{action}</Flex>}\n    />\n  );\n}\n\n// Compact status/action rows — one row per method, capability-aware. Do NOT stack full-width\n// Info Alerts for ordinary status, and do NOT show unavailable features as permanent page content\n// beyond a single row that offers to set them up.\nexport function AccountRecoverySettings({ email, phone, backupCodesSupported, backupCodesRemaining, onChangeEmail, onChangePhone, onRegenerateCodes, onChangePassword }: {\n  email?: string; phone?: string; backupCodesSupported: boolean; backupCodesRemaining: number;\n  onChangeEmail: () => void; onChangePhone: () => void; onRegenerateCodes: () => void; onChangePassword: () => void;\n}) {\n  return (\n    <Flex direction=\"col\" gap=\"lg\">\n      <Card>\n        <CardHeader><CardTitle>アカウント復旧</CardTitle></CardHeader>\n        <CardContent flush>\n          <MethodRow icon={Mail} name=\"復旧用メール\" value={email} status={email ? \"verified\" : \"unavailable\"}\n            action={<Button variant=\"outline\" size=\"sm\" onClick={onChangeEmail}>{email ? \"変更\" : \"追加\"}</Button>} />\n          <MethodRow icon={Smartphone} name=\"復旧用電話番号\" value={phone} status={phone ? \"verified\" : \"unavailable\"}\n            action={<Button variant=\"outline\" size=\"sm\" onClick={onChangePhone}>{phone ? \"変更\" : \"追加\"}</Button>} />\n          {/* Capability-aware: only render backup codes when the account type supports them. */}\n          {backupCodesSupported && (\n            <MethodRow icon={KeyRound} name=\"バックアップコード\"\n              value={`残り ${backupCodesRemaining} 個`} status={backupCodesRemaining > 0 ? \"verified\" : \"unverified\"}\n              action={<Button variant=\"outline\" size=\"sm\" onClick={onRegenerateCodes}>再生成</Button>} />\n          )}\n        </CardContent>\n      </Card>\n\n      {/* Password change is its OWN destination (risk-appropriate confirmation lives there). */}\n      <Card>\n        <CardHeader><CardTitle>パスワード</CardTitle></CardHeader>\n        <CardContent>\n          <Flex justify=\"between\" align=\"center\">\n            <span className=\"text-muted-foreground text-sm\">最終更新: 90日前</span>\n            <Button variant=\"outline\" onClick={onChangePassword}>パスワードを変更</Button>\n          </Flex>\n        </CardContent>\n      </Card>\n    </Flex>\n  );\n}\n// The signed-OUT recovery journey (forgot password → email link → reset) is a separate flow on the\n// auth screens (CenteredShell variant=\"auth\"), NOT part of these signed-in settings. Do not surface it here.",
    "name": "account-recovery-settings",
    "tagline": "Signed-in recovery: compact method status/action rows (email · phone · backup codes) + a separate password-change destination — capability-aware, risk-appropriate, NO stacked Info Alerts.",
    "tags": [
      "account",
      "recovery",
      "security",
      "backup-codes",
      "password",
      "settings"
    ]
  },
  {
    "aliases": [
      "email-template",
      "email-tokens",
      "blade-email",
      "html-email"
    ],
    "code": "// ───────────────────────────────────────────────────────────────────────\n// WHY a separate export: an email is rendered by Blade/Twig/MJML and by clients that strip\n// <style>, ignore CSS custom properties and block remote images. So NEITHER the React components\n// NOR @godxjp/ui/tokens are reachable — every value must be a LITERAL inline style.\n// @godxjp/ui/email is that bridge. It is React-free and dependency-free, and every value is\n// DERIVED from src/tokens/ (HSL→hex at module load), so it can never drift from the web palette.\n//\n// ❌ style=\"background:#0077c7\"                 // hand-copied hex → drifts the first re-theme\n// ❌ style=\"padding: var(--space-8)\"            // no email client resolves var()\n// ❌ style=\"width: 30rem\"                       // rem is unreliable; use literal px\n// ❌ <img src=\"https://cdn…/logo.png\">          // blocked by default in most clients\n// ❌ an emerald pill with no internal glyph     // that is the INCOMPLETE mark\n// ✅ every value from the token export + the embedded brand mark below\n\nimport {\n  EMAIL_BRAND_MARK,\n  EMAIL_COLORS,\n  EMAIL_CTA,\n  EMAIL_FOOTER,\n  EMAIL_MOBILE,\n  EMAIL_SHELL,\n  EMAIL_TYPOGRAPHY,\n  emailBrandMarkSvg,\n  emailInlineStyle,\n} from \"@godxjp/ui/email\";\n\nconst c = EMAIL_COLORS;\n\n// 1) THE 480px SHELL — a table, not a div: Outlook's Word renderer has no flex/grid.\nconst page = emailInlineStyle({ padding: EMAIL_SHELL.pagePadding, backgroundColor: c.background });\nconst card = emailInlineStyle({\n  width: EMAIL_SHELL.width,               // \"480px\" — EMAIL_SHELL.widthPx (480) for width=\"480\"\n  maxWidth: \"100%\",\n  backgroundColor: c.surface,             // ← --card\n  border: `${EMAIL_SHELL.borderWidth} solid ${c.border}`,\n  borderRadius: EMAIL_SHELL.radius,\n});\nconst body = emailInlineStyle({\n  padding: EMAIL_SHELL.padding,           // content column = EMAIL_SHELL.contentWidth (416px)\n  fontFamily: EMAIL_TYPOGRAPHY.fontFamily,\n  fontSize: EMAIL_TYPOGRAPHY.bodyFontSize,\n  lineHeight: EMAIL_TYPOGRAPHY.bodyLineHeight,\n  color: c.foreground,\n});\n\n// 2) THE BRAND MARK — emerald capsule + internal glyph, embedded, zero network fetch.\n//    EMAIL_BRAND_MARK.svg        inline <svg>            (Apple Mail, iOS, Thunderbird)\n//    EMAIL_BRAND_MARK.dataUri    data: URL for <img src> (no request)\n//    EMAIL_BRAND_MARK.tableHtml  <table> fallback        (renders everywhere)\n//    The header is a LOCKUP: mark (22px) + EMAIL_BRAND_MARK.gap + the wordmark at\n//    EMAIL_BRAND_MARK.wordmarkFontSize/wordmarkFontWeight. The mark is DECORATIVE there —\n//    emailBrandMarkSvg({ label: \"\" }) — so the wordmark carries the name and it is announced once.\nconst mark = emailBrandMarkSvg({ label: \"\" });\n\n// 3) THE CTA — line-height EQUALS height, so the label centres with no flexbox.\nconst ctaCell = emailInlineStyle({\n  height: EMAIL_CTA.height,               // 36px — the canonical box (mirrors --control-height-lg)\n  borderRadius: EMAIL_CTA.radius,\n  backgroundColor: c.primary,\n});\nconst ctaLink = emailInlineStyle({\n  display: \"inline-block\",\n  padding: `0 ${EMAIL_CTA.paddingX}`,\n  lineHeight: EMAIL_CTA.lineHeight,\n  fontSize: EMAIL_CTA.fontSize,\n  fontWeight: EMAIL_CTA.fontWeight,\n  color: c.primaryForeground,\n  textDecoration: \"none\",\n});\n\n// 4) LEGAL FOOTER — quiet type over a hairline; links separated by a fixed-width spacer span\n//    (margins between inline links are dropped by several clients).\nconst footer = emailInlineStyle({\n  borderTop: `${EMAIL_FOOTER.borderWidth} solid ${c.border}`,\n  paddingTop: EMAIL_FOOTER.paddingTop,\n  fontSize: EMAIL_FOOTER.fontSize,\n  lineHeight: EMAIL_FOOTER.lineHeight,\n  color: c.mutedForeground,\n});\nconst linkSpacer = emailInlineStyle({ display: \"inline-block\", width: EMAIL_FOOTER.linkGap });\n\nexport const html = `\n<table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n  <tr><td align=\"center\" style=\"${page}\">\n    <table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"\n           width=\"${EMAIL_SHELL.widthPx}\" class=\"email-card\" style=\"${card}\">\n      <tr><td class=\"email-body\" style=\"${body}\">\n        ${mark}\n        <h1 class=\"email-heading\">Your subject line</h1>\n        <p>Your body copy.</p>\n        <table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\">\n          <tr><td class=\"email-cta\" align=\"center\" style=\"${ctaCell}\">\n            <a class=\"email-cta-link\" href=\"https://your.app/action\" style=\"${ctaLink}\">Continue</a>\n          </td></tr>\n        </table>\n        <table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n          <tr><td style=\"${footer}\">\n            <p>This mailbox is not monitored.</p>\n            <p><a href=\"#\">Help</a><span style=\"${linkSpacer}\">&#8203;</span><a href=\"#\">Privacy</a></p>\n          </td></tr>\n        </table>\n      </td></tr>\n    </table>\n  </td></tr>\n</table>`;\n\n// 5) MOBILE REFLOW — progressive enhancement ONLY. Every rule below has an inline equivalent\n//    above, so a client that strips <style> still renders the desktop layout, never a broken one.\nexport const mediaQuery = `\n@media (max-width: ${EMAIL_MOBILE.maxWidth}) {\n  .email-card { width: ${EMAIL_MOBILE.width} !important; }\n  .email-body { padding: ${EMAIL_MOBILE.padding} !important; }\n  .email-heading { font-size: ${EMAIL_MOBILE.headingFontSize} !important; }\n  .email-cta, .email-cta-link { width: ${EMAIL_MOBILE.ctaWidth} !important; }\n}`;\n\n// 6) BLADE / TWIG / any non-JS engine — dump the same values once in the build step:\n//    node --input-type=module -e 'import(\"@godxjp/ui/email\").then(m => process.stdout.write(m.EMAIL_TOKENS_JSON))' > resources/design/email-tokens.json\n//    then read the JSON from PHP. Same numbers, same source, no copy-paste.",
    "name": "transactional-email",
    "tagline": "Build a transactional HTML email (Blade/Twig/MJML/React) from @godxjp/ui/email — literal hex + px derived from the web tokens, the canonical GoDX brand mark, a bulletproof CTA, legal footer and mobile reflow. NEVER hand-copy hex or hand-draw the logo in a mail template.",
    "tags": [
      "email",
      "transactional",
      "blade",
      "laravel",
      "tokens",
      "brand",
      "mark",
      "mjml",
      "cta"
    ]
  },
  {
    "aliases": [
      "cards-in-page",
      "page-layout",
      "stack-cards"
    ],
    "code": "import { PageContainer, Flex, ResponsiveGrid } from \"@godxjp/ui/layout\";\nimport { Card, CardContent, DataTable, Descriptions, type ColumnDef } from \"@godxjp/ui/data-display\";\nimport { Button, Heading } from \"@godxjp/ui/general\";\n\ntype Shot = { id: string; name: string };\nconst columns: ColumnDef<Shot>[] = [{ key: \"name\", header: \"File\" }];\n\n// Two steps, never one uniform gap:\n//   section step — between direct children of PageContainer (--page-body-gap). Do NOT add gap-*/mt-*.\n//   item step    — inside a section, Flex gap=\"sm\" between its heading and its content.\n// A section is a Heading, not a Card. Card is for a real boundary: here, the table surface.\nexport function ReportPage({ shots }: { shots: Shot[] }) {\n  return (\n    <PageContainer title=\"Bug report\" extra={<Button>Send</Button>}>\n      <Flex direction=\"col\" gap=\"sm\">\n        <Heading level={2}>Details</Heading>\n        <Descriptions columns={2}>\n          <Descriptions.Item label=\"URL\">https://app.example.com/orders</Descriptions.Item>\n          <Descriptions.Item label=\"Browser\">Safari 18</Descriptions.Item>\n        </Descriptions>\n      </Flex>\n      <Flex direction=\"col\" gap=\"sm\">\n        <Heading level={2}>Screenshots</Heading>\n        <ResponsiveGrid columns={{ sm: 1, md: 2, lg: 3 }}>…</ResponsiveGrid>\n      </Flex>\n      <Flex direction=\"col\" gap=\"sm\">\n        <Heading level={2}>Attachments</Heading>\n        <Card>\n          <CardContent flush>\n            <DataTable data={shots} columns={columns} getRowId={(r) => r.id} />\n          </CardContent>\n        </Card>\n      </Flex>\n    </PageContainer>\n  );\n}\n",
    "name": "page-sections",
    "tagline": "A page of several sections: a Heading + section spacing marks a section, not a Card. PageContainer spaces its direct children (section step); Flex gap spaces items inside one (item step). Card only for a real boundary.",
    "tags": [
      "layout",
      "page",
      "card",
      "spacing",
      "flex",
      "grid",
      "section",
      "heading"
    ]
  },
  {
    "aliases": [
      "user-chip",
      "account-menu",
      "signed-in-user"
    ],
    "code": "import { Flex } from \"@godxjp/ui/layout\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"@godxjp/ui/data-display\";\nimport { Button, Text } from \"@godxjp/ui/general\";\nimport { LogOut } from \"lucide-react\";\n\n// ❌ <div className=\"inline-flex items-center gap-1.5 rounded-full border py-0.5\">…</div>\n//    → 38px tall next to 32px controls, and the audit flags every class on it.\n// ✅ a Flex row of real primitives: Avatar (control-sized), Text, an icon Button.\nexport function AccountChip({ user, onSignOut }: { user: { name: string; email: string; avatarUrl?: string }; onSignOut: () => void }) {\n  return (\n    <Flex gap=\"xs\" align=\"center\" title={user.email}>\n      <Avatar>\n        {user.avatarUrl && <AvatarImage src={user.avatarUrl} alt=\"\" />}\n        <AvatarFallback>{user.name.slice(0, 1)}</AvatarFallback>\n      </Avatar>\n      <Text>{user.name}</Text>\n      <Button variant=\"ghost\" size=\"icon\" aria-label=\"Sign out\" onClick={onSignOut}><LogOut /></Button>\n    </Flex>\n  );\n}\n\n// In the page header, a language Select takes width=\"auto\" so it does not swallow the row:\n// <PageContainer title=\"…\" extra={<><Select …><SelectTrigger width=\"auto\">…</SelectTrigger>…</Select><AccountChip … /><Button>Send</Button></>}>\n",
    "name": "topbar-account-chip",
    "tagline": "Signed-in user in a PageContainer `extra` slot: Avatar + name + sign-out as a Flex row of primitives — same control height as the buttons beside it. Never a hand-rolled rounded-full border div.",
    "tags": [
      "auth",
      "topbar",
      "avatar",
      "header",
      "account",
      "chip"
    ]
  },
  {
    "aliases": [
      "tenanttheme",
      "customer-brand-color",
      "runtime-theme",
      "brand-color"
    ],
    "code": "// ─────────────────────────────────────────────────────────────────────────\n// A CUSTOMER's brand colour, at runtime, on ONE region. gh#861 / gh#868.\n//\n// The colour arrives as DATA (a hex an admin picked in your settings screen),\n// not as a stylesheet you author. AppSettingPicker `brand` is an ENUM PRESET\n// (brand|crm|logistics|partner|slate|dxs) and cannot take an arbitrary hex.\n//\n// ❌ DON'T hand-roll the conversion or the label. Two consumer repos each wrote\n//    their own 18-line hexToHslChannels and their own contrast rule; a wrong\n//    label makes nothing red — the text is just hard to read, for end users only.\n// ❌ DON'T set --primary alone. It does not travel alone: --primary-foreground\n//    does NOT follow it, and --ring is bound at :root where it FREEZES.\n// ✅ DO ask the package for the declarations.\n\nimport { tenantTheme, contrastRatio, hexToHsl } from \"@godxjp/ui/app\";\n\nconst brand = tenantTheme(tenant.primary_color);      // \"#0071bd\" · 3- or 6-digit\n\n<div data-tenant={tenant.slug} style={brand.vars}>    // a REGION, not <html>\n  <Topbar … />\n  <Button>Open app</Button>                            {/* rest, hover AND pressed follow */}\n</div>\n\n// brand.vars is exactly eight declarations:\n//   --primary  --primary-foreground  --ring  --primary-hover  --primary-active\n//   --text-link  --text-brand  --text-primary        ← the brand as INK (gh#887)\n// An unusable hex yields EMPTY vars, so the region falls back to your own theme\n// instead of breaking the page (same contract ColorPicker already has).\n\n// THE LABEL IS CHOSEN BY CONTRAST, and the ratio is reported so you can check it.\n// WCAG 2.2 SC 1.4.3, normal text, 4.5:1. White/black pivot at relative luminance\n// 0.179; both candidates measure 4.58:1 there, so the winner ALWAYS clears AA.\n//   #FFD400 → #000000 at 14.67:1   (\"always white\" would have been 1.29:1)\n//   #0A1F44 → #ffffff at 16.25:1\nbrand.foreground  // \"#ffffff\"\nbrand.contrast    // 5.13\nbrand.meetsAA     // true\n\n// ALREADY COMPUTED THE PAIR SERVER-SIDE? Pass it. It is used AS GIVEN — never\n// silently replaced — and the result still reports what it achieves.\nconst supplied = tenantTheme(hex, { foreground: tenant.theme_tokens.foreground });\nif (!supplied.meetsAA) {\n  // supplied.contrast is the number. The API says NO instead of returning white.\n}\n\n// NESTED TENANTS on one page: call it per region. Each region carries its own\n// pair, so nothing freezes from the region above it.\n\n// THE BRAND AS INK IS ALSO FLOORED (gh#887) — it used not to be, and that is the\n// half of the contract a consumer used to have to write. --primary is guaranteed\n// against --primary-foreground: a FILL and its label. Nothing guaranteed the same\n// colour as TEXT on a page surface, and a sidebar active item, a NavList item, a\n// MegaMenu trigger, an Anchor link, `Text link` and `Button variant=\"link\"` all do\n// exactly that. Measured on /showcase/theme-lab: #FFD400 gave 1.18–2.06:1.\n// So the three inks come back as LITERALS, each walked away from the surface until\n// the painted pixel clears 4.5:1 and no further — a seed whose ramp step already\n// clears is untouched. No call site of yours changes; the roles were always read.\n//   #FFD400  link ink 1.73:1 → 4.56:1     #E2564A  3.80:1 → 4.53:1\n//   #7C3AED / #2563EB / #0A1F44           byte-identical, they already cleared\n\n// WHICH SURFACE? The DARKEST one the ink lands on — the default is --accent\n// (#ebe9e5), not --background, because an ink clamped on the canvas measures\n// 3.78:1 the moment the row under it is hovered. Pass your own when the region is\n// darker than the package light theme, and ALWAYS for a region inside a dark theme\n// (--accent there is #3c3a34) or the ink is walked the wrong way:\nconst darkRegion = tenantTheme(tenant.primary_color, { surface: \"#3c3a34\" });\n\n// Still your call whether the raw seed is usable as ink somewhere the library does\n// not paint — contrastRatio is the same arithmetic, exported:\nif ((contrastRatio(tenant.primary_color, \"#ffffff\") ?? 0) >= 4.5) {\n  // …the seed ITSELF, unclamped, on a white page\n}\n\n// The raw conversion, if you need it on its own (the inverse of hslToHex):\nhexToHsl(\"#0071bd\");   // \"204.13 100% 37.06%\"\n\n// Worked screen: /showcase/tenant-brand-color · docs/CUSTOMER-THEMING.md\n// Why there is no <TenantTheme> component: docs/COMPOSITION-VS-COMPONENT.md §3.2\n",
    "name": "tenant-brand-color",
    "tagline": "Wear a CUSTOMER's own hex at runtime, scoped to one region — tenantTheme(hex) returns the token declarations plus the WCAG 2.2 AA label, so you never hand-roll hex→HSL or guess a foreground. There is no <TenantTheme> component and there should not be one.",
    "tags": [
      "theme",
      "tenant",
      "multi-tenant",
      "brand",
      "color",
      "colour",
      "contrast",
      "wcag",
      "a11y",
      "runtime",
      "scoped",
      "hex",
      "hsl",
      "white-label",
      "launcher"
    ]
  },
  {
    "aliases": [
      "rebrand",
      "design-handoff",
      "brand-motion",
      "motion-tokens",
      "whitelabel",
      "white-label-theme",
      "theme-block",
      "external-brand"
    ],
    "code": "// ─────────────────────────────────────────────────────────────────────────\n// A WHOLE BRAND AS A THEME BLOCK.\n//\n// tenantTheme(hex) is for ONE customer colour arriving as data. This is the\n// other job: a design system handed to you (brand book, Figma, an HTML\n// prototype) that has its own palette, radii, shadows, type ramp AND motion\n// timing. All of it is token configuration. If you find yourself forking a\n// component or overriding a .ui-* selector from a page stylesheet, stop —\n// that is the signal to file an issue for the missing knob instead.\n//\n// Worked screens, three different brands: /showcase/acme-website (light/gold),\n// /showcase/futurelastic-web (dark/gold), /showcase/tcgm-website (light,\n// Japanese, and the one with a MOTION spec).\n\n<div data-tenant=\"acme\">              {/* a REGION, never <html> */}\n  <style>{THEME}</style>              {/* in an app this is your theme.css */}\n  <Navbar /> <Hero /> <Footer />\n</div>\n\nconst THEME = `\n[data-tenant=\"acme\"] {\n  /* 1 ── COLOUR. Map the brand's hexes onto the ROLES, as HSL triplets. */\n  --primary: 192 72% 21%; --primary-foreground: 0 0% 100%;\n  --ring: 199 89% 29%;          /* DECLARE IT. --ring is bound at :root and FREEZES;\n                                   many brands ring in a different blue than they fill with. */\n  --background: 210 40% 98%; --foreground: 195 57% 16%;\n  --card: 0 0% 100%; --muted-foreground: 196 17% 39%;\n  /* --border is decorative chrome. --input is the CONTROL BOUNDARY and is held to\n     3:1 (WCAG 2.2 SC 1.4.11) — brands usually ship a second, darker \"border-strong\"\n     for exactly this; use THAT one here, not the hairline. */\n  --border: 196 27% 89%; --input: 197 13% 51%;\n\n  /* 2 ── SHAPE + ELEVATION. Card hover is a TOKEN PAIR, not a hand-written rule:\n     \"lift 2px, shadow sm→md\" == --card-shadow + --card-hover-shadow. */\n  --radius-md: 12px; --radius-xl: 20px;\n  --card-radius: var(--radius-xl); --control-radius: var(--radius-md);\n  --card-shadow: 0 1px 2px rgb(15 76 92 / .06);\n  --card-hover-shadow: 0 12px 32px -14px rgb(15 76 92 / .20);\n\n  /* 3 ── TYPE. --font-size-display is the BASE KNOB of the display ramp: 3xl/4xl/5xl\n     all derive from it, so never restate --font-size-5xl. Use clamp() or the hero\n     paints its desktop size on a phone (measured: 56px at 390 where the brand scale\n     said 34px). */\n  --font-family-body: \"Noto Sans JP\", system-ui, sans-serif;\n  --font-family-sans: var(--font-family-body);\n  --font-family-display: var(--font-family-body);\n  --font-size-display: clamp(2.125rem, 4vw, 3.5rem);\n  /* ❌ --font-family-display: \"Montserrat\", \"Noto Sans JP\", …\n     A Latin-only face FIRST in a CJK product looks fine (the browser falls through\n     per glyph) while silently setting the Latin inside a Japanese headline in a\n     different face than the headline. Keep the CJK face as the display family and\n     opt into the Latin face per element instead: */\n  --brand-latin: \"Montserrat\", system-ui, sans-serif;\n}\n[data-tenant=\"acme\"] .brand-eyebrow,\n[data-tenant=\"acme\"] .brand-price { font-family: var(--brand-latin); }\n\n/* 4 ── MOTION. THIS IS THE PART PEOPLE FORK OVER, AND THEY DO NOT NEED TO.\n   A brand motion table — press 90ms · hover 140 · state 200 · enter 260 ·\n   hero-once 480ms at y8→0 · cubic-bezier(.2,.8,.2,1) — is EIGHT declarations.\n   <Reveal>, Card hoverable, Dialog/Sheet enter and every token-driven transition\n   retime themselves off these. Zero new @keyframes. */\n[data-tenant=\"acme\"] {\n  --duration-fast: 140ms;      /* hover / focus  */\n  --duration-base: 200ms;      /* state change   */\n  --duration-slow: 480ms;      /* hero once      */\n  --ease-standard: cubic-bezier(.2,.8,.2,1);\n  --ease-emphasized: cubic-bezier(.2,.8,.2,1);\n  --ease-decelerate: cubic-bezier(.2,.8,.2,1);\n  --reveal-distance: 8px;      /* the spec's \"y8→0\" */\n  --reveal-stagger-step: 70ms;\n  /* a11y numbers are tokens too — this brand's standard is a 44px target, above\n     the WCAG 2.2 SC 2.5.8 floor of 24px (gh#931): */\n  --carousel-arrow-size: 2.75rem;\n}\n`;\n\n// 5 ── SPEND THE MOTION, THEN STOP. Most brand books cap decorative motion\n// (\"one decorative region per viewport; no parallax, no count-up, no infinite\n// float\"). Read the spec before adding effects — adding more is not proving the\n// framework, it is breaking the brand.\n\n<Reveal on=\"mount\">                 {/* hero: illustration only, if the spec says\n                                       \"text appears immediately\" — then DON'T wrap\n                                       the headline, so it paints on frame 1 */}\n  <img src={heroArt} alt=\"…\" />\n</Reveal>\n\n<Reveal on=\"view\">                  {/* one observer per SECTION */}\n  <SectionHeading />\n</Reveal>\n<ResponsiveGrid columns={{ base: 2, sm: 3, lg: 4 }}>\n  {items.map((it, i) => (\n    <Reveal key={it.id} on=\"view\" delay={Math.min(i, 5) + 1}>   {/* delay caps at 6 */}\n      <Card hoverable>…</Card>\n    </Reveal>\n  ))}\n</ResponsiveGrid>\n\n// ❌ THE ONE THAT BITES: never put on=\"view\" on something a scroller CLIPS.\n//    <Carousel><CarouselItem><Reveal on=\"view\">…  ← 3 of 4 slides stayed at\n//    opacity 0 FOREVER: the observer's root is the viewport and a clipped box has\n//    an empty intersection rect, so a slide parked outside the rail never enters.\n// ✅ Reveal the RAIL:\n<Reveal on=\"view\">\n  <Carousel>\n    <CarouselContent>\n      {news.map((n) => (\n        <CarouselItem key={n.id} className=\"basis-4/5 sm:basis-1/2 lg:basis-1/3\">\n          <Card hoverable>…</Card>          {/* fractional basis, never basis-[82%] */}\n        </CarouselItem>\n      ))}\n    </CarouselContent>\n  </Carousel>\n</Reveal>\n\n// 6 ── VERIFY BY MEASURING, NOT BY SCREENSHOT. A full-page screenshot does not\n// fire viewport observers, so every scroll reveal photographs as invisible and\n// tells you nothing. Scroll the document, then count:\n//   [...document.querySelectorAll('.ui-reveal')]\n//     .filter(e => e.getAttribute('data-reveal-state') === 'out').length   // must be 0\n//   document.documentElement.scrollWidth - document.documentElement.clientWidth // must be 0\n// Check the hero's computed font-size at 390, and remember ResponsiveGrid steps\n// are CONTAINER widths: inside a 1280 shell with 32px gutters, a 768 tablet hands\n// the grid 704px, so `md` (48rem) never fires there — key that column to `sm`.\n",
    "name": "brand-theme-block",
    "tagline": "Wear a WHOLE external brand — its palette, shape, type AND its motion spec — as one scoped theme block of token declarations. No forked components, no page-local overrides of library selectors. Use when you are handed a design system (Figma/HTML prototype/brand book), not a single customer hex.",
    "tags": [
      "theme",
      "brand",
      "rebrand",
      "handoff",
      "motion",
      "animation",
      "reveal",
      "typography",
      "font",
      "cjk",
      "japanese",
      "tokens",
      "design-system",
      "white-label",
      "marketing",
      "landing"
    ]
  }
]
