{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://chw.aemwip.com/packages/ui/screens/screen.schema.json",
  "title": "Club Hub screen",
  "description": "A screen defined as data: metadata, data sources, a tree of layout and component nodes, and declared actions. Rendered by packages/ui/js/screen.js. Component data is validated against each component's own contract (packages/ui/components/<id>.js). Two kinds: \"screen\" (default; a body of nodes) and \"flow\" (a task with steps, shown guided as a wizard or compact as sections — see SCREENS.md).",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "id",
    "app",
    "route",
    "title"
  ],
  "properties": {
    "$schema": {
      "type": "string",
      "description": "Path to this schema, for editor support."
    },
    "id": {
      "type": "string",
      "pattern": "^[a-z][a-z0-9-]*(\\.[a-z][a-z0-9-]*)+$",
      "description": "Unique id: <feature>.<screen>, e.g. \"members.registrations\". The feature prefix decides which distributions include it."
    },
    "version": {
      "type": "integer",
      "minimum": 1,
      "description": "Format version of this file."
    },
    "app": {
      "type": "string",
      "enum": [
        "admin",
        "trainer",
        "member",
        "public",
        "screens",
        "operator"
      ],
      "description": "App (shell) the screen belongs to."
    },
    "route": {
      "type": "string",
      "pattern": "^/",
      "description": "URL path inside the app; `:param` segments become ctx.params, e.g. \"/mitglieder/anmeldungen/:id\"."
    },
    "title": {
      "description": "Page title (document.title and default heading). Literal or expression ($t, $text)."
    },
    "nav": {
      "type": "object",
      "additionalProperties": false,
      "description": "How the screen appears in the app navigation.",
      "properties": {
        "label": {
          "description": "Navigation label (literal or $t)."
        },
        "icon": {
          "type": "string",
          "description": "Icon id from the sprite."
        },
        "order": {
          "type": "integer",
          "description": "Position in the navigation."
        },
        "tab": {
          "type": "boolean",
          "description": "Show in the mobile tab bar (max 5 per app)."
        }
      }
    },
    "requires": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Capabilities (entitlement ∧ setting ∧ flag ∧ permission) needed to see the screen at all."
    },
    "data": {
      "type": "object",
      "description": "Named data sources the app loads before rendering; results are available to expressions under their name.",
      "additionalProperties": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "source"
        ],
        "properties": {
          "source": {
            "type": "string",
            "description": "SDK operation id from the API contracts, e.g. \"members.registrations.list\"."
          },
          "params": {
            "type": "object",
            "description": "Parameters; values may be expressions ({\"$bind\": \"params.id\"})."
          },
          "offline": {
            "type": "boolean",
            "description": "Cache for offline use (Trainer/Member apps)."
          }
        }
      }
    },
    "actions": {
      "type": "object",
      "description": "Actions components can trigger via data-action=\"<name>\". The risk class decides the safeguard (UX guide §5).",
      "additionalProperties": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "handler",
          "risk"
        ],
        "properties": {
          "handler": {
            "type": "string",
            "description": "Name of a handler registered by the feature's code, e.g. \"members.approveRegistration\"."
          },
          "risk": {
            "enum": [
              "safe",
              "reversible",
              "irreversible"
            ],
            "description": "safe: just do it; reversible: do it and offer undo; irreversible: confirm first, naming the consequences."
          },
          "confirm": {
            "type": "object",
            "description": "Confirm dialog options (title, body, consequences[], confirmLabel, tone); values may be expressions. Required in practice for irreversible actions."
          },
          "success": {
            "description": "Toast text after success (literal or expression)."
          },
          "undoLabel": {
            "type": "string",
            "description": "Label of the undo button for reversible actions (default \"Rückgängig\")."
          }
        }
      }
    },
    "body": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/node"
      },
      "description": "Content of the app shell's main area, top to bottom."
    },
    "kind": {
      "enum": [
        "screen",
        "flow"
      ],
      "default": "screen",
      "description": "\"screen\": shows things (body). \"flow\": does a task (steps → review → submit), rendered guided or compact by js/flow.js."
    },
    "intro": {
      "description": "Flow: one or two sentences shown at the top of the first step in guided mode (what this task does and what it changes)."
    },
    "steps": {
      "type": "array",
      "minItems": 1,
      "maxItems": 6,
      "description": "Flow: the steps. Guided shows one per screen; compact shows them as sections.",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "title",
          "body"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9-]*$",
            "description": "Step id; also the ?step= value in guided mode. \"review\" is reserved."
          },
          "title": {
            "description": "Step name (stepper label and section heading). Literal or expression."
          },
          "help": {
            "description": "Explanation shown in guided mode above the step, and behind a \"?\" in compact mode. Required except for Dojang screens."
          },
          "if": {
            "description": "Show the step only when true (e.g. guardian step when under 16). May use values.* (form values so far)."
          },
          "body": {
            "type": "array",
            "items": {
              "$ref": "#/$defs/node"
            },
            "description": "Nodes of this step; form controls must have a name (it becomes values.<name>)."
          }
        }
      }
    },
    "review": {
      "type": "object",
      "additionalProperties": false,
      "description": "Flow: the review — its own last step in guided mode, a summary above the submit button in compact mode.",
      "properties": {
        "title": {
          "description": "Step/section name (default \"Prüfen\" / \"Übersicht\")."
        },
        "intro": {
          "description": "Text above the review in guided mode."
        },
        "source": {
          "type": "string",
          "description": "SDK dry-run operation called with the values when the review opens; its result is available as review.* (e.g. review.effects)."
        },
        "summary": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/node"
          },
          "description": "Nodes summarising what was entered (e.g. a key-value list of values)."
        },
        "effectsTitle": {
          "description": "Heading of the effects list."
        },
        "effects": {
          "description": "Expression resolving to a list of strings: what will happen on submit (from the dry-run)."
        }
      }
    },
    "submit": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "action"
      ],
      "description": "Flow: the final action.",
      "properties": {
        "action": {
          "type": "string",
          "description": "Name of the declared action that performs the task. Its risk class applies; the review counts as the confirmation."
        },
        "label": {
          "description": "Submit button label, a verb (\"Prüfung abschliessen\")."
        }
      }
    },
    "modes": {
      "type": "object",
      "additionalProperties": false,
      "description": "Flow: which presentations exist and the default.",
      "properties": {
        "guided": {
          "type": "boolean",
          "default": true,
          "description": "Allow guided mode."
        },
        "compact": {
          "type": "boolean",
          "default": true,
          "description": "Allow compact mode (false for public forms)."
        },
        "default": {
          "enum": [
            "guided",
            "compact",
            "auto"
          ],
          "default": "auto",
          "description": "auto: guided until the person completed the flow `compactAfter` times; editing existing data opens compact."
        },
        "compactAfter": {
          "type": "integer",
          "minimum": 0,
          "default": 2,
          "description": "Completions after which auto switches to compact."
        }
      }
    },
    "cancel": {
      "type": "object",
      "additionalProperties": false,
      "description": "Flow: cancel link/button.",
      "properties": {
        "label": {
          "type": "string",
          "description": "Label (default \"Abbrechen\")."
        },
        "href": {
          "type": "string",
          "description": "Where to go; without href the flow just closes (dialog)."
        }
      }
    },
    "draft": {
      "type": "boolean",
      "default": true,
      "description": "Flow: keep entered values as a draft (per device; server-side in the product) until submitted."
    }
  },
  "$defs": {
    "node": {
      "oneOf": [
        {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "component"
          ],
          "properties": {
            "component": {
              "type": "string",
              "description": "Component id (packages/ui/components/<id>.js)."
            },
            "data": {
              "type": "object",
              "description": "Data for the component's render(); validated against its contract. Values may be expressions."
            },
            "if": {
              "description": "Render only when this expression is truthy."
            },
            "requires": {
              "type": "array",
              "items": {
                "type": "string"
              },
              "description": "Capabilities needed for this node."
            },
            "note": {
              "type": "string",
              "description": "Free comment for authors; ignored."
            }
          }
        },
        {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "layout"
          ],
          "properties": {
            "layout": {
              "enum": [
                "stack",
                "cluster",
                "grid",
                "split",
                "section",
                "card"
              ],
              "description": "Layout primitive."
            },
            "id": {
              "type": "string",
              "description": "Element id (sections)."
            },
            "title": {
              "description": "Heading for section/card (literal or expression)."
            },
            "gap": {
              "enum": [
                "sm",
                "lg"
              ],
              "description": "Stack gap."
            },
            "justify": {
              "enum": [
                "between",
                "end",
                "center"
              ],
              "description": "Cluster alignment."
            },
            "min": {
              "type": "string",
              "pattern": "^\\d+(\\.\\d+)?rem$",
              "description": "Grid minimum column width, e.g. \"16rem\"."
            },
            "children": {
              "type": "array",
              "items": {
                "$ref": "#/$defs/node"
              },
              "description": "Child nodes."
            },
            "if": {
              "description": "Render only when this expression is truthy."
            },
            "requires": {
              "type": "array",
              "items": {
                "type": "string"
              },
              "description": "Capabilities needed for this node."
            },
            "note": {
              "type": "string",
              "description": "Free comment for authors; ignored."
            }
          }
        },
        {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "each",
            "node"
          ],
          "properties": {
            "each": {
              "type": "string",
              "description": "Path to an array in the context."
            },
            "as": {
              "type": "string",
              "default": "item",
              "description": "Name of the current item inside the repeated node."
            },
            "node": {
              "$ref": "#/$defs/node",
              "description": "Node rendered once per item."
            },
            "if": {
              "description": "Render only when truthy."
            },
            "note": {
              "type": "string",
              "description": "Free comment for authors; ignored."
            }
          }
        }
      ]
    }
  }
}