{
  "aliases": [
    "絞り込み条件",
    "条件ビルダー",
    "filter-builder",
    "query-builder",
    "kintone filter",
    "kintone-filter",
    "salesforce list view filter",
    "salesforce-filter",
    "filter logic",
    "list-view-filter"
  ],
  "code": "// Live examples: docs page navigation/search-form (m = builder, o = list view with the builder in a\n// docked panel). Research: kintone's filter rows are field / operator / value (「次のいずれかを\n// 含む」, ≦ …) combined with 「すべての条件を満たす」, conditions added with 「絞り込み条件を\n// 追加」; the kintone query language joins conditions with and / or and groups them with\n// parentheses, sorted by order by. Salesforce numbers each filter (Filter 1, 2, …) and combines\n// them with AND / OR / NOT, e.g. (1 AND 2) OR 3; list views are switched from a picker, pinned\n// as the default, and edited in a Filters panel (Add Filter → field / operator / value).\n//\n// A COMPOSITION, not a component: only one of the design systems the library follows names a\n// condition builder, the operators are domain policy, and every piece is an existing primitive.\nimport { useRef, useState } from \"react\";\nimport { Minus, Plus } from \"lucide-react\";\nimport { Badge } from \"@godxjp/ui/data-display\";\nimport { Form, FormField, Input, NumberInput, RadioGroup, Select } from \"@godxjp/ui/data-entry\";\nimport { Button } from \"@godxjp/ui/general\";\nimport { Flex, ResponsiveGrid } from \"@godxjp/ui/layout\";\n\ntype Kind = \"text\" | \"choice\" | \"number\";\ntype Field = { value: string; label: string; kind: Kind; options?: { value: string; label: string }[] };\ntype Row = { id: number; field: string; operator: string; value: string };\nexport type Conditions = { match: \"all\" | \"any\" | \"custom\"; logic: string; rows: Row[] };\n\nconst OPERATORS: Record<Kind, { value: string; label: string }[]> = {\n  text: [{ value: \"contains\", label: \"次のキーワードを含む\" }, { value: \"notContains\", label: \"次のキーワードを含まない\" }, { value: \"eq\", label: \"＝\" }, { value: \"neq\", label: \"≠\" }],\n  choice: [{ value: \"in\", label: \"次のいずれかを含む\" }, { value: \"notIn\", label: \"次のいずれも含まない\" }],\n  number: [{ value: \"eq\", label: \"＝\" }, { value: \"neq\", label: \"≠\" }, { value: \"gte\", label: \"≥（以上）\" }, { value: \"lte\", label: \"≤（以下）\" }],\n};\n\n/** Numbered-row logic: \"1 AND (2 OR 3)\", \"NOT 2\". null = malformed → show the field error. */\nexport function compileLogic(expr: string, count: number): ((hits: boolean[]) => boolean) | null {\n  const tokens = expr.toUpperCase().match(/\\d+|AND|OR|NOT|\\(|\\)|\\S/g) ?? [];\n  let pos = 0;\n  type Node = (h: boolean[]) => boolean;\n  const or = (): Node | null => { let l = and(); while (l && tokens[pos] === \"OR\") { pos++; const r = and(); if (!r) return null; const a = l; l = (h) => a(h) || r(h); } return l; };\n  const and = (): Node | null => { let l = not(); while (l && tokens[pos] === \"AND\") { pos++; const r = not(); if (!r) return null; const a = l; l = (h) => a(h) && r(h); } return l; };\n  const not = (): Node | null => {\n    if (tokens[pos] === \"NOT\") { pos++; const i = not(); return i ? (h) => !i(h) : null; }\n    if (tokens[pos] === \"(\") { pos++; const i = or(); if (!i || tokens[pos] !== \")\") return null; pos++; return i; }\n    const n = Number(tokens[pos]);\n    if (!Number.isInteger(n) || n < 1 || n > count) return null;\n    pos++; return (h) => h[n - 1] ?? false;\n  };\n  const tree = tokens.length ? or() : null;\n  return tree && pos === tokens.length ? tree : null;\n}\n\nexport function ConditionBuilder({ fields, value, onApply }: {\n  fields: Field[]; value: Conditions; onApply: (c: Conditions) => void;\n}) {\n  const [draft, setDraft] = useState(value);\n  const nextId = useRef(Math.max(0, ...value.rows.map((r) => r.id)) + 1);\n  const valid = draft.match !== \"custom\" || compileLogic(draft.logic, draft.rows.length) !== null;\n  const patch = (id: number, p: Partial<Row>) => setDraft((c) => ({ ...c, rows: c.rows.map((r) => (r.id === id ? { ...r, ...p } : r)) }));\n  const kindOf = (f: string) => fields.find((x) => x.value === f)?.kind ?? \"text\";\n  /** kintone's ＋ adds a blank condition right after its own row; null appends one. */\n  const insertAfter = (rows: Row[], afterId: number | null): Row[] => {\n    const blank: Row = { id: nextId.current++, field: fields[0].value, operator: OPERATORS[fields[0].kind][0].value, value: \"\" };\n    const at = afterId === null ? rows.length : rows.findIndex((r) => r.id === afterId) + 1;\n    return [...rows.slice(0, at), blank, ...rows.slice(at)];\n  };\n\n  // role=\"search\" + a real submit: neither form-on-list-page nor hand-rolled-filter-bar flags it.\n  return (\n    <Form role=\"search\" aria-label=\"絞り込み条件\" controlWidth=\"100%\"\n      onSubmit={(e) => { e.preventDefault(); if (valid) onApply(draft); }}>\n      <Flex direction=\"col\" gap=\"md\">\n        <FormField label=\"条件の組み合わせ\">\n          <RadioGroup aria-label=\"条件の組み合わせ\" orientation=\"horizontal\" value={draft.match}\n            options={[{ value: \"all\", label: \"すべての条件を満たす\" }, { value: \"any\", label: \"いずれかの条件を満たす\" }, { value: \"custom\", label: \"条件式を指定\" }]}\n            onValueChange={(v) => setDraft((c) => ({ ...c, match: v as Conditions[\"match\"] }))} />\n        </FormField>\n        {draft.rows.map((row, i) => {\n          const field = fields.find((f) => f.value === row.field);\n          const n = i + 1;\n          return (\n            // ONE line per condition, kintone-style: [No.] [field] [operator] [value] [＋][−].\n            // Container grid: 24 tracks from lg (field 6, operator 5 — narrower but wide enough for\n            // 「次のキーワードを含まない」 — value 10), 12 at md (the operator gets the extra track so its\n            // longest label is not cut). Below md (a docked panel, a phone) it is 4 tracks and\n            // `dense` backfills the ＋/− cell beside the number: No. and buttons on the first line,\n            // field / operator / value stacked below. align=\"center\" = antd Row align=\"middle\".\n            <Flex key={row.id} direction=\"col\" role=\"group\" aria-label={\"条件 \" + n}>\n              <ResponsiveGrid columns={{ base: 4, md: 12, lg: 24 }} gap=\"sm\" align=\"center\" dense>\n                <ResponsiveGrid.Item span={{ base: 1, md: 1, lg: 1 }}>\n                  <Badge variant=\"outline\">{n}</Badge>\n                </ResponsiveGrid.Item>\n                <ResponsiveGrid.Item span={{ base: 4, md: 3, lg: 6 }}>\n                  <Select aria-label={\"条件 \" + n + \" · フィールド\"} options={fields} value={row.field}\n                    onValueChange={(f: string) => patch(row.id, { field: f, operator: OPERATORS[kindOf(f)][0].value, value: \"\" })} />\n                </ResponsiveGrid.Item>\n                <ResponsiveGrid.Item span={{ base: 4, md: 4, lg: 5 }}>\n                  <Select aria-label={\"条件 \" + n + \" · 演算子\"} options={OPERATORS[kindOf(row.field)]} value={row.operator}\n                    onValueChange={(op: string) => patch(row.id, { operator: op })} />\n                </ResponsiveGrid.Item>\n                <ResponsiveGrid.Item span={{ base: 4, md: 3, lg: 10 }}>\n                  {field?.kind === \"choice\" ? (\n                    <Select aria-label={\"条件 \" + n + \" · 値\"} options={field.options ?? []} value={row.value}\n                      onValueChange={(v: string) => patch(row.id, { value: v })} />\n                  ) : field?.kind === \"number\" ? (\n                    <NumberInput aria-label={\"条件 \" + n + \" · 値\"} value={row.value === \"\" ? null : Number(row.value)}\n                      onValueChange={(v) => patch(row.id, { value: v == null ? \"\" : String(v) })} />\n                  ) : (\n                    <Input aria-label={\"条件 \" + n + \" · 値\"} value={row.value} onChange={(e) => patch(row.id, { value: e.target.value })} />\n                  )}\n                </ResponsiveGrid.Item>\n                <ResponsiveGrid.Item span={{ base: 3, md: 1, lg: 2 }}>\n                  <Flex gap=\"xs\" justify=\"end\">\n                    <Button type=\"button\" variant=\"ghost\" size=\"icon-sm\" aria-label={\"条件 \" + n + \" の後に条件を追加\"}\n                      onClick={() => setDraft((c) => ({ ...c, rows: insertAfter(c.rows, row.id) }))}>\n                      <Plus aria-hidden=\"true\" />\n                    </Button>\n                    <Button type=\"button\" variant=\"ghost\" size=\"icon-sm\" aria-label={\"条件 \" + n + \" を削除\"}\n                      onClick={() => setDraft((c) => ({ ...c, rows: c.rows.filter((r) => r.id !== row.id) }))}>\n                      <Minus aria-hidden=\"true\" />\n                    </Button>\n                  </Flex>\n                </ResponsiveGrid.Item>\n              </ResponsiveGrid>\n            </Flex>\n          );\n        })}\n        {/* With every row removed there is no ＋ left to press: offer one. */}\n        {draft.rows.length === 0 && (\n          <Flex>\n            <Button type=\"button\" variant=\"outline\" size=\"sm\"\n              onClick={() => setDraft((c) => ({ ...c, rows: insertAfter(c.rows, null) }))}>\n              <Plus aria-hidden=\"true\" />条件を追加\n            </Button>\n          </Flex>\n        )}\n        {draft.match === \"custom\" && (\n          <FormField label=\"条件式\" id=\"cb-logic\" helper=\"番号付きの条件を AND・OR・NOT と括弧で組み合わせます（例: 1 AND (2 OR 3)）。\"\n            error={valid ? undefined : \"存在する条件の番号、AND・OR・NOT、対応の取れた括弧だけを使ってください。\"}>\n            <Input id=\"cb-logic\" value={draft.logic} onChange={(e) => setDraft((c) => ({ ...c, logic: e.target.value }))} />\n          </FormField>\n        )}\n        <Flex gap=\"sm\" justify=\"end\" wrap>\n          <Button type=\"button\" variant=\"ghost\" onClick={() => setDraft(value)}>変更を元に戻す</Button>\n          <Button type=\"submit\" disabled={!valid}>適用</Button>\n        </Flex>\n      </Flex>\n    </Form>\n  );\n}\n// Add 並び順 (Select + Segmented 昇順/降順) and \"save as view\" (Input + Button) as two more\n// FormFields when the screen keeps saved views. Docked beside the list (Salesforce list view):\n// <SplitPane asideWidth=\"lg\" aside={open ? <Card>…<ConditionBuilder/>…</Card> : null}> with a\n// counted Filters toggle in FilterBar's actions; below 1024px the same builder opens in a Sheet.",
  "name": "condition-builder",
  "tagline": "Condition builder — numbered rows of field / operator / value, 「すべての条件を満たす」 / 「いずれかの条件を満たす」 / custom logic `1 AND (2 OR 3)`, add/remove rows, 「並び順」, save as a named view; applied on SUBMIT inside a role=\"search\" Form. Composition, not a component. Docked beside the list it is the Salesforce list-view filter panel.",
  "tags": [
    "filter",
    "condition",
    "builder",
    "query",
    "logic",
    "and",
    "or",
    "kintone",
    "salesforce",
    "saved view",
    "絞り込み",
    "list"
  ]
}
