{
  "$comment": "What each platform can render. The builder, validator and agents (get_capabilities) only offer what is listed here. Wear OS only for now. filterSpecs drives the expression parser on every platform (docs/expressions.md).",
  "schemaVersion": 2,
  "components": {
    "text":      { "props": ["value", "style", "maxLines"], "styles": ["title", "body", "caption", "display"] },
    "stat":      { "props": ["value", "unit", "label", "trend", "icon"], "trends": ["up", "down", "flat"] },
    "list":      { "props": ["items", "item", "maxRows", "header", "emptyText"], "itemSlots": ["title", "subtitle", "trailing", "badge", "icon", "action"] },
    "status":    { "props": ["state", "label", "detail"], "states": ["ok", "warn", "error", "unknown"] },
    "progress":  { "props": ["value", "min", "max", "style", "label"], "styles": ["ring", "bar"] },
    "countdown": { "props": ["target", "format", "label"], "formats": ["relative", "clock"] },
    "button":    { "props": ["label", "icon", "action", "confirm", "style"], "styles": ["primary", "secondary"] },
    "toggle":    { "props": ["label", "state", "onAction", "offAction", "icon"] },
    "icon":      { "props": ["icon", "url", "size"], "sizes": ["small", "medium", "large"] },
    "stack":     { "props": ["direction", "header", "children"], "directions": ["vertical", "horizontal"] }
  },
  "commonProps": ["type", "id"],
  "actionTypes": {
    "http":          { "props": ["url", "method", "headers", "body", "confirm", "toast", "mode", "refresh"], "required": ["url"] },
    "refreshSource": { "props": ["source", "confirm", "toast"], "required": ["source"] },
    "openScreen":    { "props": ["screen"], "required": ["screen"] },
    "connector":     { "props": ["connector", "params", "confirm", "toast", "refresh"], "required": ["connector"] }
  },
  "sourceTypes": ["template", "http", "feed"],
  "sourceProps": ["type", "mode", "template", "params", "url", "method", "headers", "body", "map", "push", "refresh"],
  "sourceModes": ["proxied", "direct"],
  "pushLevels": ["values", "signal", "sealed", "none"],
  "experienceProps": ["title", "subtitle", "icon", "iconUrl", "description", "theme", "main", "pages", "tile", "complication"],
  "themes": {
    "builtIn": ["dark", "light", "high-contrast", "ember", "mono", "slate"],
    "default": "dark",
    "corners": ["sharp", "rounded", "pill"],
    "colorRoles": [
      "background", "surface", "onSurface", "onSurfaceVariant", "outline", "accent", "onAccent", "timeText", "scrollThumb",
      "scrollTrack", "warningText", "statusOk", "statusWarn", "statusError", "statusUnknown"
    ],
    "minTextContrast": 4.5
  },
  "mappingDirectives": ["$each", "$map", "$if", "$value", "$let", "$literal"],
  "numberFormats": ["compact", "N0-N10", "F0-F10", "P0-P10"],
  "filters": [
    "number", "date", "time", "relative", "upper", "lower", "default", "take", "status", "round", "prefix", "suffix",
    "lookup", "where", "map", "sortBy", "find", "count", "sum", "indexBy", "first", "last", "join", "toNumber", "toText", "toTime"
  ],
  "filterSpecs": {
    "number":   { "args": [{ "kind": "text", "optional": true }] },
    "date":     { "args": [{ "kind": "text", "optional": true }] },
    "time":     { "args": [{ "kind": "text", "optional": true }] },
    "relative": { "args": [{ "kind": "text", "optional": true }] },
    "upper":    { "args": [] },
    "lower":    { "args": [] },
    "default":  { "args": [{ "kind": "text", "optional": true }] },
    "take":     { "args": [{ "kind": "text", "optional": false }] },
    "status":   { "args": [{ "kind": "text", "optional": true }] },
    "round":    { "args": [{ "kind": "text", "optional": true }] },
    "prefix":   { "args": [{ "kind": "text", "optional": false }] },
    "suffix":   { "args": [{ "kind": "text", "optional": false }] },
    "lookup":   { "args": [{ "kind": "text", "optional": false }] },
    "where":    { "args": [{ "kind": "lambda", "optional": false }] },
    "map":      { "args": [{ "kind": "lambda", "optional": false }] },
    "sortBy":   { "args": [{ "kind": "lambda", "optional": false }, { "kind": "expr", "optional": true }] },
    "find":     { "args": [{ "kind": "lambda", "optional": false }] },
    "count":    { "args": [{ "kind": "lambda", "optional": true }] },
    "sum":      { "args": [{ "kind": "lambda", "optional": true }] },
    "indexBy":  { "args": [{ "kind": "lambda", "optional": false }] },
    "first":    { "args": [] },
    "last":     { "args": [] },
    "join":     { "args": [{ "kind": "expr", "optional": true }] },
    "toNumber": { "args": [] },
    "toText":   { "args": [] },
    "toTime":   { "args": [] }
  },
  "filterDocs": {
    "number":   { "argument": "compact, N0-N10, F0-F10 or P0-P10 (optional)", "description": "Formats a number (invariant culture). Default: thousands separators and at most 2 decimals (1,234.5). compact gives 1.2k / 3.4M; N2 = 1,234.50; F2 = 1234.50; P0 = 50 %. Custom patterns are not supported.", "example": "{{ stats.visitors | number:compact }}" },
    "round":    { "argument": "decimals 0-10 (optional)", "description": "Rounds a number (half away from zero). Default 0 decimals.", "example": "{{ weather.temp | round:1 }}" },
    "date":     { "argument": "date pattern (tokens yyyy yy MMMM MMM MM M dddd ddd dd d HH H hh h mm m ss s tt) or iso/short/long/weekday/day/datetime (optional)", "description": "Formats a timestamp in the project time zone (default Europe/Stockholm). Default 'yyyy-MM-dd'.", "example": "{{ event.start | date:'d MMM' }}" },
    "time":     { "argument": "time pattern or clock/seconds (optional)", "description": "Formats a timestamp's time of day. Default 'HH:mm'.", "example": "{{ train.expectedAt | time }}" },
    "relative": { "argument": "'compact' (optional)", "description": "Time from now: 'in 5 min', '3 min ago', 'now'. 'compact' gives '5 min' / '-3 min'.", "example": "{{ build.finishedAt | relative }}" },
    "upper":    { "description": "Upper-cases text.", "example": "{{ stop.name | upper }}" },
    "lower":    { "description": "Lower-cases text.", "example": "{{ state | lower }}" },
    "default":  { "argument": "fallback text", "description": "Used when the value is missing, null, empty text or an empty list.", "example": "{{ weather.temp | default:'--' }}" },
    "take":     { "argument": "count (required)", "description": "First n items of a list, or first n characters of text.", "example": "{{ odenplan.departures | take:3 }}" },
    "status":   { "argument": "rules (optional): state=value[/value], state<n, state>=n, bare state as catch-all", "description": "Maps a value to ok / warn / error / unknown. Without rules, common words (success, failed, running, green, red...) are recognised.", "example": "{{ ci.conclusion | status:ok=success,warn=running,error }}" },
    "prefix":   { "argument": "text (required)", "description": "Adds text before a non-empty value.", "example": "{{ train.platform | prefix:'Platform ' }}" },
    "suffix":   { "argument": "text (required)", "description": "Adds text after a non-empty value.", "example": "{{ weather.temp | suffix:'°' }}" },
    "lookup":   { "argument": "rules KEY=value,...,*=fallback (required)", "description": "Maps a value by exact match; * is the fallback. Write a literal = or , as \\= or \\,.", "example": "{{ line.mode | lookup:METRO=train,BUS=bus,*=train }}" },
    "where":    { "argument": "condition per item (required)", "description": "Items of a list for which the condition is true. Names inside are relative to the item.", "example": "{{ departures | where: line.mode == 'METRO' }}" },
    "map":      { "argument": "expression per item (required)", "description": "Replaces each item of a list with the expression's value.", "example": "{{ runs | map: name }}" },
    "sortBy":   { "argument": "key per item (required), then 'desc' (optional)", "description": "Stable sort by a key: numbers numerically, text ordinally, missing keys last.", "example": "{{ departures | sortBy: expected }}" },
    "find":     { "argument": "condition per item (required)", "description": "The first item for which the condition is true, else nothing.", "example": "{{ states | find: entity_id == 'lock.front_door' }}" },
    "count":    { "argument": "condition per item (optional)", "description": "Number of items (matching the condition), characters of text, or keys of an object.", "example": "{{ builds | count: status == 'failed' }}" },
    "sum":      { "argument": "value per item (optional)", "description": "Sum of the numeric items (or of the expression per item). Empty lists give 0.", "example": "{{ orders | sum: total }}" },
    "indexBy":  { "argument": "key per item (required)", "description": "An object keyed by the expression; the first item wins for duplicate keys.", "example": "{{ $root | indexBy: entity_id }}" },
    "first":    { "description": "The first item of a list.", "example": "{{ departures | first }}" },
    "last":     { "description": "The last item of a list.", "example": "{{ readings | last }}" },
    "join":     { "argument": "separator expression (optional, default ', ')", "description": "Joins the items of a list as text.", "example": "{{ tags | join: ' · ' }}" },
    "toNumber": { "description": "Converts text such as \"12.5\" to a number; anything else gives nothing.", "example": "{{ sensor.state | toNumber }}" },
    "toText":   { "description": "Converts a value to its display text.", "example": "{{ id | toText }}" },
    "toTime":   { "description": "Parses a timestamp (ISO 8601, epoch seconds/ms, relative '+5m') to ISO 8601 UTC.", "example": "{{ expected | toTime }}" }
  },
  "dataStates": ["sample", "live", "loading", "error", "empty", "stale"],
  "icons": [
    "add", "alarm", "arrow-down", "arrow-up", "battery", "bell", "bike", "bolt", "build", "bus", "calendar", "car",
    "check", "chevron-right", "clock", "close", "cloud", "code", "door", "error", "fan", "flag", "git", "heart",
    "home", "info", "light", "lock", "mail", "map-pin", "music", "pause", "phone", "play", "plug", "power",
    "rain", "refresh", "remove", "robot", "rocket", "settings", "ship", "snow", "star", "stop", "sun", "temp",
    "thermometer", "timer", "train", "tram", "unlock", "user", "walk", "warning", "wifi", "wind"
  ],
  "platforms": {
    "wearos": {
      "name": "Wear OS (Galaxy Watch)",
      "surfaces": {
        "screen": {
          "components": ["text", "stat", "list", "status", "progress", "countdown", "button", "toggle", "icon", "stack"],
          "limits": { "listMaxRows": 10, "textMaxLines": 3 }
        },
        "tile": {
          "instances": 10,
          "minRefreshSeconds": 60,
          "limits": { "listMaxRows": 3, "compactListMaxRows": 8, "maxButtons": 2, "maxToggles": 1 },
          "templates": {
            "text":          { "slots": ["title", "icon", "text"], "required": ["text"] },
            "single-stat":   { "slots": ["title", "icon", "value", "unit", "label", "trend"], "required": ["value"] },
            "list3":         { "slots": ["title", "icon", "items", "item", "emptyText"], "required": ["items", "item"] },
            "compact-list":  { "slots": ["title", "icon", "items", "item", "emptyText"], "required": ["items", "item"] },
            "status":        { "slots": ["title", "icon", "state", "label", "detail"], "required": ["state"] },
            "progress-ring": { "slots": ["title", "icon", "value", "min", "max", "label"], "required": ["value"] },
            "buttons2":      { "slots": ["title", "icon", "buttons"], "required": ["buttons"] },
            "toggle":        { "slots": ["title", "icon", "toggle"], "required": ["toggle"] }
          }
        },
        "complication": {
          "instances": 1,
          "dataSources": 1,
          "minRefreshSeconds": 300,
          "kinds": {
            "shortText":   { "slots": ["text", "title", "icon"], "required": ["text"], "maxTextChars": 7 },
            "longText":    { "slots": ["text", "title", "icon"], "required": ["text"], "maxTextChars": 24 },
            "rangedValue": { "slots": ["value", "min", "max", "text", "title", "icon"], "required": ["value"] },
            "image":       { "slots": ["icon", "url"], "required": [] }
          }
        }
      }
    }
  }
}
