{
  "name": "SettingsPanel",
  "summary": "Grouped settings that either apply immediately or are saved together — never both in one panel.",
  "useWhen": [
    "the user configures preferences, feature flags or account options",
    "the settings are grouped and the groups are worth naming"
  ],
  "composes": [
    "SettingRow",
    "Toggle",
    "Checkbox",
    "RadioGroup",
    "Radio",
    "Select",
    "Input",
    "Button",
    "Alert",
    "Divider",
    "Badge",
    "Tabs",
    "TabItem",
    "CodeSnippet",
    "InlineLoading",
    "ColorPicker",
    "SplitPane"
  ],
  "structure": [
    "Named groups, separated by Divider, each with a heading.",
    "One SettingRow per setting: label and description on the left, control on the right, wired together by the row.",
    "For immediate-apply panels, an InlineLoading beside each Toggle, mounted with the row: Saving… while the save runs, Saved until `onSuccess` sets it back to `inactive`, Not saved until the next attempt. The Toggle stays enabled mid-save and a second press is ignored; the words grow away from the Toggle, so it never moves.",
    "For save-on-submit panels, a save bar that appears only when something is dirty, with save and discard.",
    "Tabs for panels large enough that a single scroll is unreasonable — but never with unsaved changes hidden behind an inactive tab.",
    "A setting used from outside the app — an API key, a webhook, a CLI — shows how to use it as CodeSnippets under a sentence saying what each is for: a command as `single`, a request as `multi`, each with its own `label` and `copyLabel`, and a command named in a description as `inline`.",
    "A request shown with what it answers sits beside it, in a SplitPane: `start` the request's CodeSnippet, `end` the response's, `label` \"Resize the API request\", collapsing at the tablet breakpoint — both have long lines, and which one needs the room is the reader's to say.",
    "A colour the product shows to other people — a brand accent — is a ColorPicker with the brand's own colours as named `swatches`. When text sits on it, the panel measures the pair as it changes and refuses a colour under 4.5:1 with `isInvalid` and an error that gives the ratio, and a preview shows the colour where it will be used."
  ],
  "states": {
    "loading": {
      "must": "Show the groups and their labels with the controls disabled, rather than an empty panel.",
      "why": "the shape of the settings is known before their values are; showing it makes the wait legible"
    },
    "empty": {
      "must": "A group with nothing in it for this account states why — a plan limit, a permission — rather than being hidden.",
      "why": "a silently absent setting reads as a missing feature, and generates support tickets"
    },
    "error": {
      "must": "Revert the control to its previous value, show an Alert naming the setting that failed, and keep the rest of the panel usable.",
      "why": "a Toggle left in the new position after a failed save is a lie about the server's state"
    },
    "partial": {
      "must": "When a batched save partially fails, keep the successful values and mark only the failed rows dirty again.",
      "why": "reverting everything discards changes the server accepted"
    }
  },
  "propsUsed": {
    "Toggle": [
      "size",
      "intent",
      "isDisabled"
    ],
    "Checkbox": [
      "isDisabled",
      "isIndeterminate"
    ],
    "RadioGroup": [
      "label",
      "value",
      "onChange",
      "isDisabled"
    ],
    "Select": [
      "label",
      "description",
      "isInvalid",
      "errorMessage"
    ],
    "Button": [
      "variant",
      "isDisabled"
    ],
    "Alert": [
      "intent",
      "title",
      "actions"
    ],
    "Tabs": [
      "type",
      "size"
    ],
    "CodeSnippet": [
      "type",
      "label",
      "copyLabel",
      "language"
    ],
    "InlineLoading": [
      "status",
      "onSuccess"
    ],
    "ColorPicker": [
      "label",
      "description",
      "value",
      "onChange",
      "swatches",
      "isInvalid",
      "errorMessage"
    ],
    "SplitPane": [
      "start",
      "end",
      "label",
      "defaultSize",
      "minSize",
      "maxSize",
      "collapse"
    ]
  },
  "variantsUsed": {
    "Toggle": {
      "intent": [
        "brand",
        "danger"
      ]
    },
    "Alert": {
      "intent": [
        "error",
        "warning"
      ]
    },
    "Button": {
      "variant": [
        "primary-brand",
        "tertiary"
      ]
    },
    "Tabs": {
      "type": [
        "underline"
      ]
    },
    "CodeSnippet": {
      "type": [
        "single",
        "multi",
        "inline"
      ]
    },
    "InlineLoading": {
      "status": [
        "inactive",
        "active",
        "finished",
        "error"
      ]
    }
  },
  "a11y": {
    "requires": [
      "a label on every control — the row's visible text, wired up, not just placed beside it",
      "the description associated with the control rather than floating next to it, so it is announced with the setting",
      "an announcement when an immediate-apply setting saves; the visual state change is not enough — the row's InlineLoading, already mounted, is that announcement"
    ],
    "notes": [
      "Toggle for immediate effect, Checkbox for values saved with a submit. Mixing both in one panel means the user cannot tell which is which."
    ]
  },
  "antiPatterns": [
    {
      "dont": "Toggles that apply immediately alongside a Save button",
      "why": "the switch says the change already happened; the button says it has not"
    },
    {
      "dont": "a destructive setting with no confirmation",
      "do": "the DestructiveConfirm pattern",
      "why": "\"delete this workspace\" as a Toggle row is one mis-click"
    },
    {
      "dont": "<Toggle isDisabled={saving} /> on an immediate-apply row",
      "do": "an enabled Toggle that ignores presses while its InlineLoading is active",
      "why": "disabling the switch a keyboard user just pressed throws their focus to the top of the page"
    },
    {
      "dont": "unsaved changes on an inactive tab",
      "why": "the user is told the form is dirty with nothing on screen that is"
    },
    {
      "dont": "hiding settings the account cannot use",
      "why": "an absent control is indistinguishable from a missing feature"
    },
    {
      "dont": "a colour setting saved with no check, when text will sit on the colour",
      "do": "`<ColorPicker isInvalid={ratio < 4.5} errorMessage={`Too light for white text: ${ratio}:1. Needs 4.5:1.`} … />`",
      "why": "the picker offers every colour; the one the admin likes best may leave every email's button unreadable"
    }
  ]
}
