{"$schema":"https:\/\/ui.particle.academy\/schema\/registry-item.json","name":"robots-editor","type":"registry:ui","title":"RobotsEditor","description":"Controlled robots.txt rule builder \u2014 per-group Allow\/Disallow, sitemaps, and protected paths pinned Disallow everywhere (the protect() safety rail).","package":"fancy-x-files-ui","dependencies":["@particle-academy\/react-fancy"],"registryDependencies":["model.js","validate.js"],"files":[{"path":"components\/fancy\/robots-editor\/RobotsEditor.tsx","content":"import { Button, Callout, Input } from \"@particle-academy\/react-fancy\";\r\nimport type { RobotsGroup, RobotsModel } from \"..\/model.js\";\r\nimport { validateRobots } from \"..\/validate.js\";\r\nimport { IssueList, Section, StringList } from \".\/internal.js\";\r\n\r\nexport interface RobotsEditorProps {\r\n  \/** Controlled robots.txt model. *\/\r\n  value: RobotsModel;\r\n  \/** Fires with the next model on every edit. *\/\r\n  onChange: (next: RobotsModel) => void;\r\n  \/** Hide the inline validation issue list. Default `false`. *\/\r\n  hideIssues?: boolean;\r\n  className?: string;\r\n}\r\n\r\n\/**\r\n * Controlled robots.txt rule builder.\r\n *\r\n * Protected-path safety (mirrors the PHP `protect()` rail): paths listed under\r\n * **Protected paths** are pinned Disallowed for every group and can NEVER be\r\n * Allowed. The editor:\r\n *  - renders protected chips in red, labelled \"Disallowed everywhere\";\r\n *  - strips a protected path out of any group's Allow list as you protect it;\r\n *  - flags (via {@link validateRobots}) any protected path still sitting in an\r\n *    Allow list, so the issue surfaces inline.\r\n *\/\r\nexport function RobotsEditor({\r\n  value,\r\n  onChange,\r\n  hideIssues,\r\n  className,\r\n}: RobotsEditorProps) {\r\n  const issues = validateRobots(value);\r\n\r\n  const patch = (next: Partial<RobotsModel>) => onChange({ ...value, ...next });\r\n\r\n  const updateGroup = (index: number, next: Partial<RobotsGroup>) => {\r\n    const groups = value.groups.map((g, i) =>\r\n      i === index ? { ...g, ...next } : g,\r\n    );\r\n    onChange({ ...value, groups });\r\n  };\r\n\r\n  const addGroup = () =>\r\n    patch({\r\n      groups: [\r\n        ...value.groups,\r\n        { userAgents: [\"*\"], allow: [], disallow: [] },\r\n      ],\r\n    });\r\n\r\n  const removeGroup = (index: number) =>\r\n    patch({ groups: value.groups.filter((_, i) => i !== index) });\r\n\r\n  \/** Setting Allow for a group: protected paths are filtered out entirely. *\/\r\n  const setAllow = (index: number, next: string[]) => {\r\n    const filtered = next.filter((p) => !value.protectedPaths.includes(p));\r\n    updateGroup(index, { allow: filtered });\r\n  };\r\n\r\n  \/** Protecting a path also removes it from every group's Allow list. *\/\r\n  const setProtected = (next: string[]) => {\r\n    const protectedSet = new Set(next);\r\n    const groups = value.groups.map((g) => ({\r\n      ...g,\r\n      allow: g.allow.filter((p) => !protectedSet.has(p)),\r\n    }));\r\n    onChange({ ...value, protectedPaths: next, groups });\r\n  };\r\n\r\n  return (\r\n    <div className={className} data-x-editor=\"robots\">\r\n      <Section\r\n        title=\"Protected paths\"\r\n        handle=\"robots-protected\"\r\n        action={\r\n          value.protectedPaths.length ? (\r\n            <span style={{ fontSize: \"0.75rem\", opacity: 0.7 }}>\r\n              Disallowed for every agent \u00b7 cannot be Allowed\r\n            <\/span>\r\n          ) : null\r\n        }\r\n      >\r\n        <Callout color=\"red\">\r\n          Paths added here are pinned <strong>Disallow<\/strong> on every\r\n          user-agent group and can never be marked Allow. This mirrors the\r\n          server-side <code>protect()<\/code> safety rail.\r\n        <\/Callout>\r\n        <StringList\r\n          handle=\"robots-protected\"\r\n          values={value.protectedPaths}\r\n          onChange={setProtected}\r\n          placeholder=\"\/admin\"\r\n        \/>\r\n      <\/Section>\r\n\r\n      <Section\r\n        title=\"Sitemaps\"\r\n        handle=\"robots-sitemaps\"\r\n      >\r\n        <StringList\r\n          handle=\"robots-sitemaps\"\r\n          values={value.sitemaps}\r\n          onChange={(sitemaps) => patch({ sitemaps })}\r\n          placeholder=\"https:\/\/example.com\/sitemap.xml\"\r\n        \/>\r\n      <\/Section>\r\n\r\n      <Section title=\"Host\" handle=\"robots-host\">\r\n        <Input\r\n          value={value.host ?? \"\"}\r\n          onValueChange={(host) => patch({ host: host || undefined })}\r\n          placeholder=\"example.com\"\r\n          data-x-input=\"robots-host\"\r\n        \/>\r\n      <\/Section>\r\n\r\n      <Section\r\n        title=\"User-agent groups\"\r\n        handle=\"robots-groups\"\r\n        action={\r\n          <Button\r\n            type=\"button\"\r\n            variant=\"ghost\"\r\n            onClick={addGroup}\r\n            data-x-add=\"robots-group\"\r\n          >\r\n            Add group\r\n          <\/Button>\r\n        }\r\n      >\r\n        {value.groups.map((group, i) => (\r\n          <div\r\n            key={i}\r\n            data-x-group={i}\r\n            style={{\r\n              border: \"1px solid var(--rf-border, #e5e7eb)\",\r\n              borderRadius: \"0.5rem\",\r\n              padding: \"0.75rem\",\r\n              display: \"flex\",\r\n              flexDirection: \"column\",\r\n              gap: \"0.6rem\",\r\n            }}\r\n          >\r\n            <div\r\n              style={{\r\n                display: \"flex\",\r\n                justifyContent: \"space-between\",\r\n                alignItems: \"center\",\r\n              }}\r\n            >\r\n              <strong style={{ fontSize: \"0.8rem\" }}>Group {i + 1}<\/strong>\r\n              <Button\r\n                type=\"button\"\r\n                variant=\"ghost\"\r\n                color=\"red\"\r\n                onClick={() => removeGroup(i)}\r\n                data-x-remove={`robots-group:${i}`}\r\n                disabled={value.groups.length <= 1}\r\n              >\r\n                Remove\r\n              <\/Button>\r\n            <\/div>\r\n            <StringList\r\n              label=\"User-agents\"\r\n              handle={`robots-ua:${i}`}\r\n              values={group.userAgents}\r\n              onChange={(userAgents) => updateGroup(i, { userAgents })}\r\n              placeholder=\"*\"\r\n            \/>\r\n            <StringList\r\n              label=\"Disallow\"\r\n              handle={`robots-disallow:${i}`}\r\n              values={group.disallow}\r\n              onChange={(disallow) => updateGroup(i, { disallow })}\r\n              placeholder=\"\/private\"\r\n            \/>\r\n            <StringList\r\n              label=\"Allow\"\r\n              handle={`robots-allow:${i}`}\r\n              values={group.allow}\r\n              onChange={(allow) => setAllow(i, allow)}\r\n              placeholder=\"\/public\"\r\n              disallowedValues={value.protectedPaths}\r\n              disallowedReason={(p) =>\r\n                `\"${p}\" is protected and cannot be Allowed.`\r\n              }\r\n            \/>\r\n            <div style={{ maxWidth: \"12rem\" }}>\r\n              <Input\r\n                type=\"number\"\r\n                label=\"Crawl-delay (s)\"\r\n                value={group.crawlDelay != null ? String(group.crawlDelay) : \"\"}\r\n                onValueChange={(v) =>\r\n                  updateGroup(i, {\r\n                    crawlDelay: v === \"\" ? undefined : Number(v),\r\n                  })\r\n                }\r\n                data-x-input={`robots-crawldelay:${i}`}\r\n                placeholder=\"0\"\r\n              \/>\r\n            <\/div>\r\n          <\/div>\r\n        ))}\r\n      <\/Section>\r\n\r\n      {hideIssues ? null : <IssueList issues={issues} testid=\"robots\" \/>}\r\n    <\/div>\r\n  );\r\n}\r\n","type":"registry:ui","target":"components\/fancy\/robots-editor\/RobotsEditor.tsx"}]}