@godxjp/ui 33.2.1 → 33.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +5 -5
  2. package/agent/START-HERE.md +6 -6
  3. package/agent/components/ResponsiveGrid.json +2 -2
  4. package/agent/components/SearchForm.json +115 -0
  5. package/agent/components/Toolbar.json +6 -3
  6. package/agent/components/TopbarItem.json +1 -1
  7. package/agent/components-index.json +10 -1
  8. package/agent/components.json +124 -6
  9. package/agent/index.json +7 -7
  10. package/agent/llms.txt +7 -7
  11. package/agent/patterns/condition-builder.json +31 -0
  12. package/agent/patterns/data-table-page.json +1 -1
  13. package/agent/patterns/filter-bar-layouts.json +30 -0
  14. package/agent/patterns/inertia-list-page.json +1 -1
  15. package/agent/patterns/list-filters.json +1 -1
  16. package/agent/patterns/search-form.json +27 -0
  17. package/agent/patterns-index.json +85 -0
  18. package/agent/patterns.json +91 -3
  19. package/agent/rules.json +1 -1
  20. package/agent/tokens.json +16 -4
  21. package/dist/components/data-display/popover.js +8 -6
  22. package/dist/components/feedback/overlay-close-focus.js +3 -2
  23. package/dist/components/feedback/tooltip.js +2 -1
  24. package/dist/components/layout/topbar-item.js +1 -1
  25. package/dist/components/navigation/index.d.ts +2 -0
  26. package/dist/components/navigation/index.js +2 -0
  27. package/dist/components/navigation/search-form.d.ts +31 -0
  28. package/dist/components/navigation/search-form.js +112 -0
  29. package/dist/contracts/measurement.json +1 -1
  30. package/dist/i18n/messages/en.json +9 -0
  31. package/dist/i18n/messages/ja.json +9 -0
  32. package/dist/i18n/messages/vi.json +9 -0
  33. package/dist/lab/mega-menu.js +3 -2
  34. package/dist/lib/active-element.d.ts +12 -0
  35. package/dist/lib/active-element.js +10 -0
  36. package/dist/lib/shortcut.js +3 -2
  37. package/dist/props/components/layout.prop.d.ts +6 -1
  38. package/dist/props/components/navigation.prop.d.ts +43 -3
  39. package/dist/props/registry.d.ts +5 -0
  40. package/dist/props/registry.js +12 -0
  41. package/dist/styles/layers.json +11 -1
  42. package/dist/styles/layout.css +41 -0
  43. package/dist/styles/shell-layout.css +19 -3
  44. package/dist/styles.css +1 -1
  45. package/dist/tokens/components/shell.css +4 -2
  46. package/docs/CONSUMER-RULES.md +1 -1
  47. package/package.json +2 -2
  48. package/scripts/ui-audit.mjs +148 -3
@@ -4,7 +4,7 @@
4
4
  "table-state",
5
5
  "list-page"
6
6
  ],
7
- "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. */}\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.",
7
+ "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.",
8
8
  "name": "data-table-page",
9
9
  "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.",
10
10
  "tags": [
@@ -0,0 +1,30 @@
1
+ {
2
+ "aliases": [
3
+ "filter-layouts",
4
+ "faceted-filter",
5
+ "faceted-sidebar",
6
+ "saved-views-filter",
7
+ "docked-filter-panel",
8
+ "filter-panel",
9
+ "filter-menu"
10
+ ],
11
+ "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\".",
12
+ "name": "filter-bar-layouts",
13
+ "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.",
14
+ "tags": [
15
+ "filter",
16
+ "filters",
17
+ "layout",
18
+ "faceted",
19
+ "facets",
20
+ "saved views",
21
+ "docked",
22
+ "panel",
23
+ "sidebar",
24
+ "chips",
25
+ "date range",
26
+ "mobile",
27
+ "list",
28
+ "datatable"
29
+ ]
30
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
- "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. */}\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",
2
+ "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",
3
3
  "name": "inertia-list-page",
4
4
  "tagline": "Inertia + @godxjp/ui list page — PageContainer + Toolbar (above the table) + DataTable + Badge + Pagination. No KPI row, no restating subtitle.",
5
5
  "tags": [
@@ -6,7 +6,7 @@
6
6
  "advanced-filters",
7
7
  "add-filter"
8
8
  ],
9
- "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// ✅ 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.",
9
+ "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.",
10
10
  "name": "list-filters",
11
11
  "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.",
12
12
  "tags": [
@@ -0,0 +1,27 @@
1
+ {
2
+ "aliases": [
3
+ "検索条件",
4
+ "検索フォーム",
5
+ "query-filter",
6
+ "query filter",
7
+ "advanced search",
8
+ "advanced-search",
9
+ "search-panel"
10
+ ],
11
+ "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.",
12
+ "name": "search-form",
13
+ "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).",
14
+ "tags": [
15
+ "search",
16
+ "search form",
17
+ "query",
18
+ "filter",
19
+ "検索",
20
+ "検索条件",
21
+ "japanese",
22
+ "business",
23
+ "advanced search",
24
+ "list",
25
+ "datatable"
26
+ ]
27
+ }
@@ -279,6 +279,91 @@
279
279
  "datatable"
280
280
  ]
281
281
  },
282
+ {
283
+ "aliases": [
284
+ "filter-layouts",
285
+ "faceted-filter",
286
+ "faceted-sidebar",
287
+ "saved-views-filter",
288
+ "docked-filter-panel",
289
+ "filter-panel",
290
+ "filter-menu"
291
+ ],
292
+ "name": "filter-bar-layouts",
293
+ "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.",
294
+ "tags": [
295
+ "filter",
296
+ "filters",
297
+ "layout",
298
+ "faceted",
299
+ "facets",
300
+ "saved views",
301
+ "docked",
302
+ "panel",
303
+ "sidebar",
304
+ "chips",
305
+ "date range",
306
+ "mobile",
307
+ "list",
308
+ "datatable"
309
+ ]
310
+ },
311
+ {
312
+ "aliases": [
313
+ "検索条件",
314
+ "検索フォーム",
315
+ "query-filter",
316
+ "query filter",
317
+ "advanced search",
318
+ "advanced-search",
319
+ "search-panel"
320
+ ],
321
+ "name": "search-form",
322
+ "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).",
323
+ "tags": [
324
+ "search",
325
+ "search form",
326
+ "query",
327
+ "filter",
328
+ "検索",
329
+ "検索条件",
330
+ "japanese",
331
+ "business",
332
+ "advanced search",
333
+ "list",
334
+ "datatable"
335
+ ]
336
+ },
337
+ {
338
+ "aliases": [
339
+ "絞り込み条件",
340
+ "条件ビルダー",
341
+ "filter-builder",
342
+ "query-builder",
343
+ "kintone filter",
344
+ "kintone-filter",
345
+ "salesforce list view filter",
346
+ "salesforce-filter",
347
+ "filter logic",
348
+ "list-view-filter"
349
+ ],
350
+ "name": "condition-builder",
351
+ "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.",
352
+ "tags": [
353
+ "filter",
354
+ "condition",
355
+ "builder",
356
+ "query",
357
+ "logic",
358
+ "and",
359
+ "or",
360
+ "kintone",
361
+ "salesforce",
362
+ "saved view",
363
+ "絞り込み",
364
+ "list"
365
+ ]
366
+ },
282
367
  {
283
368
  "aliases": [
284
369
  "organization-switcher",
@@ -151,7 +151,7 @@
151
151
  ]
152
152
  },
153
153
  {
154
- "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. */}\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",
154
+ "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",
155
155
  "name": "inertia-list-page",
156
156
  "tagline": "Inertia + @godxjp/ui list page — PageContainer + Toolbar (above the table) + DataTable + Badge + Pagination. No KPI row, no restating subtitle.",
157
157
  "tags": [
@@ -260,7 +260,7 @@
260
260
  "table-state",
261
261
  "list-page"
262
262
  ],
263
- "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. */}\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.",
263
+ "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.",
264
264
  "name": "data-table-page",
265
265
  "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.",
266
266
  "tags": [
@@ -280,7 +280,7 @@
280
280
  "advanced-filters",
281
281
  "add-filter"
282
282
  ],
283
- "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// ✅ 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.",
283
+ "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.",
284
284
  "name": "list-filters",
285
285
  "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.",
286
286
  "tags": [
@@ -296,6 +296,94 @@
296
296
  "datatable"
297
297
  ]
298
298
  },
299
+ {
300
+ "aliases": [
301
+ "filter-layouts",
302
+ "faceted-filter",
303
+ "faceted-sidebar",
304
+ "saved-views-filter",
305
+ "docked-filter-panel",
306
+ "filter-panel",
307
+ "filter-menu"
308
+ ],
309
+ "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\".",
310
+ "name": "filter-bar-layouts",
311
+ "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.",
312
+ "tags": [
313
+ "filter",
314
+ "filters",
315
+ "layout",
316
+ "faceted",
317
+ "facets",
318
+ "saved views",
319
+ "docked",
320
+ "panel",
321
+ "sidebar",
322
+ "chips",
323
+ "date range",
324
+ "mobile",
325
+ "list",
326
+ "datatable"
327
+ ]
328
+ },
329
+ {
330
+ "aliases": [
331
+ "検索条件",
332
+ "検索フォーム",
333
+ "query-filter",
334
+ "query filter",
335
+ "advanced search",
336
+ "advanced-search",
337
+ "search-panel"
338
+ ],
339
+ "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.",
340
+ "name": "search-form",
341
+ "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).",
342
+ "tags": [
343
+ "search",
344
+ "search form",
345
+ "query",
346
+ "filter",
347
+ "検索",
348
+ "検索条件",
349
+ "japanese",
350
+ "business",
351
+ "advanced search",
352
+ "list",
353
+ "datatable"
354
+ ]
355
+ },
356
+ {
357
+ "aliases": [
358
+ "絞り込み条件",
359
+ "条件ビルダー",
360
+ "filter-builder",
361
+ "query-builder",
362
+ "kintone filter",
363
+ "kintone-filter",
364
+ "salesforce list view filter",
365
+ "salesforce-filter",
366
+ "filter logic",
367
+ "list-view-filter"
368
+ ],
369
+ "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.",
370
+ "name": "condition-builder",
371
+ "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.",
372
+ "tags": [
373
+ "filter",
374
+ "condition",
375
+ "builder",
376
+ "query",
377
+ "logic",
378
+ "and",
379
+ "or",
380
+ "kintone",
381
+ "salesforce",
382
+ "saved view",
383
+ "絞り込み",
384
+ "list"
385
+ ]
386
+ },
299
387
  {
300
388
  "aliases": [
301
389
  "organization-switcher",
package/agent/rules.json CHANGED
@@ -215,7 +215,7 @@
215
215
  "title": "A scoped theme must state its own `color`"
216
216
  },
217
217
  {
218
- "body": "A list page's filters live in `FilterBar` (alias `Toolbar`), and the bar WRAPS: one stacked column below 640px, extra rows above. Nothing in it scrolls sideways at any width — no horizontally scrolling strip of filters, search fields or labelled controls, whether built with a prop, a `ScrollArea`, or an `overflow-x-*` box (WCAG 2.2 SC 1.4.10 Reflow: search fields and controls are not in the two-dimensional exception; a sideways strip hides filters off-screen and clips the first label). Keep the 2–4 most-used filters inline. Put every other filter behind ONE 'More filters' `Button` — its `count` = how many of those hidden filters are applied — that opens a `Sheet` holding them as `FilterBarGroup` + control. Every applied filter, inline or hidden, shows as a removable chip in the bar's `chips` row, and that row wraps too. A row of filter chips (`ToggleGroup`) takes `wrap`; it does not scroll either. Copy-paste composition: pattern `list-filters`. Enforced by ui-audit `filter-bar-no-overflow` and `filter-strip-no-scroll`. Prior art: Shopify Polaris Filters (2–3 promoted filters, a More button opening a sheet), Linear (one Filter menu), IBM Carbon (hidden filters show an applied count).",
218
+ "body": "A list page's filters live in `FilterBar` (alias `Toolbar`), and the bar WRAPS: one stacked column below 640px, extra rows above. Nothing in it scrolls sideways at any width — no horizontally scrolling strip of filters, search fields or labelled controls, whether built with a prop, a `ScrollArea`, or an `overflow-x-*` box (WCAG 2.2 SC 1.4.10 Reflow: search fields and controls are not in the two-dimensional exception; a sideways strip hides filters off-screen and clips the first label). Keep the 2–4 most-used filters inline. Put every other filter behind ONE 'More filters' `Button` — its `count` = how many of those hidden filters are applied — that opens a `Sheet` holding them as `FilterBarGroup` + control. Every applied filter, inline or hidden, shows as a removable chip in the bar's `chips` row, and that row wraps too. A row of filter chips (`ToggleGroup`) takes `wrap`; it does not scroll either. Copy-paste composition: pattern `list-filters`. WHICH SHAPE — a decision, not one answer: (1) a few filters applied the moment they change (SaaS lists) → this FilterBar; (2) many conditions, a heavy query, or users who expect an explicit 検索 button (Japanese business systems) → `SearchForm` (検索条件フォーム: aligned columns, 検索 + クリア, 詳細条件を開く, a 検索条件 summary; pattern `search-form`); (3) power users combining arbitrary field × operator conditions with all / any / numbered logic and saving them as views → a condition builder (pattern `condition-builder`, docked beside the list as a Salesforce-style filter panel). Shapes (2) and (3) apply on submit inside a `role=\"search\"` form; the 'wraps, never scrolls sideways, packed at the start' half of this rule applies to all three. Enforced by ui-audit `filter-bar-no-overflow`, `filter-strip-no-scroll` and `hand-rolled-filter-bar` (2+ filters laid out in your own Flex/Card above a list instead of FilterBar; filters are packed at the start — never `justify=\"between\"`, the only far-end element is the `actions` slot). Prior art: Shopify Polaris Filters (2–3 promoted filters, a More button opening a sheet), Linear (one Filter menu), IBM Carbon (hidden filters show an applied count).",
219
219
  "number": 51,
220
220
  "title": "A filter bar wraps — many filters go behind ONE 'More filters' control"
221
221
  }