{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://ebi-webcomponents.github.io/protvista/schema/v1/config.schema.json",
  "title": "ProtvistaViewerConfig",
  "description": "Declarative configuration for a ProtVista viewer instance (v1). Source-of-truth JSON Schema, authored in lockstep with src/schema/types.ts.",

  "type": "object",
  "required": ["rows"],
  "additionalProperties": false,
  "properties": {
    "$schema": { "type": "string" },
    "version": {
      "description": "Schema protocol version. Optional — defaults to \"1.0\" when omitted.",
      "const": "1.0"
    },
    "extends": {
      "description": "One or more base configs to merge under this one. Each entry is a URL, a file path, or a registered preset name resolved via an embedder-supplied resolver.",
      "oneOf": [
        { "type": "string", "minLength": 1 },
        {
          "type": "array",
          "items": { "type": "string", "minLength": 1 },
          "minItems": 1
        }
      ]
    },
    "accession": {
      "description": "Primary accession (e.g. \"P05067\"). Used to interpolate {accession} placeholders.",
      "type": "string",
      "minLength": 1
    },
    "sources": {
      "description": "Map of named URL templates. Tracks reference these by key.",
      "type": "object",
      "additionalProperties": { "type": "string", "minLength": 1 }
    },
    "defaults": { "$ref": "#/$defs/ConfigDefaults" },
    "strict": {
      "description": "Promote every non-fatal warning (empty sequence, per-track fetch failure, setTrackData misuse) to a mount-level failure shown in the viewer's error panel. Default false.",
      "type": "boolean"
    },
    "theme": { "$ref": "#/$defs/ThemeConfig" },
    "rows": {
      "description": "Ordered list of rows: each is either a group of tracks (has `tracks`) or a standalone track (has `data`, no `tracks`). Mixed freely; declaration order preserved.",
      "type": "array",
      "items": { "$ref": "#/$defs/TopLevelEntry" }
    }
  },

  "$defs": {
    "TopLevelEntry": {
      "description": "A group of tracks (has `tracks`) or a standalone track (has `data`, no `tracks`).",
      "oneOf": [
        { "$ref": "#/$defs/GroupConfig" },
        { "$ref": "#/$defs/TrackConfig" }
      ]
    },

    "ConfigDefaults": {
      "description": "Viewer-wide defaults inherited by every group/track.",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "rendering": { "$ref": "#/$defs/RenderingOptions" }
      }
    },

    "ThemeConfig": {
      "description": "Viewer-wide chrome colours, applied as --protvista-* custom properties on the host. A no-code way to recolour the viewer from the config (see docs/theming.md for the underlying tokens).",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "labelColor": {
          "description": "Background of the row-label side panel (group + track labels).",
          "type": "string",
          "minLength": 1
        },
        "accentColor": {
          "description": "Primary accent — focus rings and the datatable active-row marker.",
          "type": "string",
          "minLength": 1
        }
      }
    },

    "GroupConfig": {
      "type": "object",
      "required": ["id", "tracks"],
      "additionalProperties": false,
      "properties": {
        "id": { "type": "string", "minLength": 1 },
        "label": {
          "type": "string",
          "description": "Human-readable label. A Markdoc inline source string — Markdown (emphasis, code, links) plus the `{% help slug=\"…\" %}…{% /help %}` tag; `{accession}` is interpolated before rendering. Block-level markup is not supported. Falls back to a title-cased form of `id` if omitted."
        },
        "description": {
          "type": "string",
          "description": "Short plain-text description shown as a native tooltip (HTML `title` attribute) when hovering the group label. No Markdown or HTML interpretation."
        },
        "component": { "$ref": "#/$defs/ComponentName" },
        "tracks": {
          "type": "array",
          "items": { "$ref": "#/$defs/TrackConfig" }
        },
        "hidden": {
          "type": "boolean",
          "description": "Ship this row hidden. The row is absent from the render but is listed as a stub in customize mode so a user can reveal it. This is the same field the show/hide control writes, so an author's choice and a user's are the same state and getConfig() exports whichever is current. Defaults to false."
        },
        "rendering": { "$ref": "#/$defs/RenderingOptions" }
      }
    },

    "TrackConfig": {
      "type": "object",
      "required": ["id", "data"],
      "additionalProperties": false,
      "properties": {
        "id": { "type": "string", "minLength": 1 },
        "label": {
          "type": "string",
          "description": "Human-readable label. A Markdoc inline source string — Markdown (emphasis, code, links) plus the `{% help slug=\"…\" %}…{% /help %}` tag; `{accession}` is interpolated before rendering. Block-level markup is not supported. Falls back to a title-cased form of `id` if omitted."
        },
        "kind": { "$ref": "#/$defs/SemanticKind" },
        "component": { "$ref": "#/$defs/ComponentName" },
        "data": {
          "description": "String shorthand, single descriptor, or array of descriptors.",
          "oneOf": [
            { "type": "string", "minLength": 1 },
            { "$ref": "#/$defs/DataSourceDescriptor" },
            {
              "type": "array",
              "items": { "$ref": "#/$defs/DataSourceDescriptor" },
              "minItems": 1
            }
          ]
        },
        "description": {
          "type": "string",
          "description": "Short plain-text description shown as a native tooltip (HTML `title` attribute) when hovering the track label. No Markdown or HTML interpretation."
        },
        "dataTooltip": { "$ref": "#/$defs/DataTooltip" },
        "filter": { "type": "string", "minLength": 1 },
        "filterUI": { "const": "nightingale-filter" },
        "hidden": {
          "type": "boolean",
          "description": "Ship this track hidden. The track is absent from the render but is listed as a stub in customize mode so a user can reveal it. This is the same field the show/hide control writes, so an author's choice and a user's are the same state and getConfig() exports whichever is current. Defaults to false."
        },
        "rendering": { "$ref": "#/$defs/RenderingOptions" }
      }
    },

    "DataTooltip": {
      "description": "Per-item tooltip. Three authoring forms: (a) a bare string — shorthand for a Markdoc template; (b) a fields object — declarative label/value rows; (c) a template object — explicit Markdoc template with optional extra variables. Rich, interactive, or stateful tooltips are not a config concern — listen for the Nightingale `change` event and mount your own UI, setting `notooltip` on the element to suppress the built-in popover.",
      "oneOf": [
        { "type": "string" },
        { "$ref": "#/$defs/TooltipFieldsSpec" },
        { "$ref": "#/$defs/TooltipMarkdownSpec" }
      ]
    },

    "TooltipFieldsSpec": {
      "description": "Declarative label/value rows. Each entry becomes `<h5>label</h5>` followed by the HTML-escaped value at `path`. Rich / interactive / stateful tooltips are not a config concern — consumers listen for the Nightingale `change` event, mount their own UI, and set `notooltip` on the element to suppress the built-in popover.",
      "type": "object",
      "required": ["kind", "fields"],
      "additionalProperties": false,
      "properties": {
        "kind": { "const": "fields" },
        "fields": {
          "type": "array",
          "items": { "$ref": "#/$defs/TooltipFieldSpec" },
          "minItems": 1
        }
      }
    },

    "TooltipFieldSpec": {
      "description": "One row in a `fields`-form tooltip. `path` is a dotted property path against the item (e.g. `association.0.name`). The value at `path` is coerced to string, HTML-escaped, and wrapped in `<p>` at the leaf.",
      "type": "object",
      "required": ["path", "label"],
      "additionalProperties": false,
      "properties": {
        "path": { "type": "string", "minLength": 1 },
        "label": { "type": "string" }
      }
    },

    "TooltipMarkdownSpec": {
      "description": "Markdoc template rendered against the item's fields. `{% $field %}` interpolates scalars.",
      "type": "object",
      "required": ["kind", "template"],
      "additionalProperties": false,
      "properties": {
        "kind": { "const": "markdown" },
        "template": { "type": "string", "minLength": 1 },
        "variables": {
          "description": "Extra variables merged into the Markdoc scope alongside the item's fields. Useful for threading track-level context (accession, trackId) into per-item templates.",
          "type": "object"
        }
      }
    },

    "DataSourceDescriptor": {
      "description": "Bridge between Intent and Representation. `from` defaults to \"url\".",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "from": { "enum": ["url", "inline", "file", "custom"] },
        "source": {
          "oneOf": [
            { "type": "string", "minLength": 1 },
            {
              "type": "array",
              "items": { "type": "string", "minLength": 1 },
              "minItems": 1
            }
          ]
        },
        "url": {
          "oneOf": [
            { "type": "string", "minLength": 1 },
            {
              "type": "array",
              "items": { "type": "string", "minLength": 1 },
              "minItems": 1
            }
          ]
        },
        "inlineData": {},
        "adapter": { "$ref": "#/$defs/AdapterName" }
      },
      "allOf": [
        {
          "if": {
            "type": "object",
            "required": ["from"],
            "properties": { "from": { "const": "inline" } }
          },
          "then": { "required": ["inlineData"] }
        }
      ]
    },

    "RenderingOptions": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "color": { "type": "string", "minLength": 1 },
        "shape": { "type": "string", "minLength": 1 },
        "height": { "type": "number", "exclusiveMinimum": 0 },
        "layout": { "enum": ["non-overlapping", "default"] },
        "colorScale": { "$ref": "#/$defs/ColorScaleConfig" }
      }
    },

    "ColorScaleConfig": {
      "description": "Either a named theme OR explicit stops (at least one required). When both are present, explicit stops take precedence at render time.",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "theme": { "type": "string", "minLength": 1 },
        "stops": {
          "type": "array",
          "items": { "$ref": "#/$defs/ColorStop" },
          "minItems": 2
        }
      },
      "anyOf": [
        { "required": ["theme"] },
        { "required": ["stops"] }
      ]
    },

    "ColorStop": {
      "type": "object",
      "required": ["value", "color"],
      "additionalProperties": false,
      "properties": {
        "value": { "type": "number" },
        "color": { "type": "string", "minLength": 1 },
        "label": { "type": "string" }
      }
    },

    "SemanticKind": {
      "description": "Open-string vocabulary. Closed-set validation (built-ins + runtime-registered kinds) is applied by the runtime validator, not this static schema.",
      "type": "string",
      "minLength": 1
    },

    "ComponentName": {
      "description": "Open-string vocabulary. Closed-set validation is applied by the runtime validator.",
      "type": "string",
      "minLength": 1
    },

    "AdapterName": {
      "description": "Open-string vocabulary. Closed-set validation is applied by the runtime validator.",
      "type": "string",
      "minLength": 1
    }
  }
}
