# zSettingsFrame — Developer Guide

A self-contained, schema-driven settings panel library for World of Warcraft addons (Retail 12.0.x+). No external dependencies — no Ace libraries, no LibSharedMedia, no LibStub required. Drop it in and go.

---

## Table of Contents

1. [Quick Start](#quick-start)
2. [Installation](#installation)
3. [Architecture Overview](#architecture-overview)
4. [The Adapter](#the-adapter)
5. [Public API](#public-api)
6. [Schema Structure](#schema-structure)
7. [Navigation Hierarchy](#navigation-hierarchy)
8. [Conditional Visibility on Nav and Tab Entries](#conditional-visibility-on-nav-and-tab-entries)
9. [Widget Reference](#widget-reference)
10. [Common Schema Fields](#common-schema-fields)
11. [Style Constants](#style-constants)
12. [LibSharedMedia Integration](#libsharedmedia-integration)
13. [React / State Pattern Note](#react--state-pattern-note)
14. [Example: Bridge Without AceDB](#example-bridge-without-acedb)
15. [Example: Bridge With AceDB](#example-bridge-with-acedb)
16. [Example: Schema File](#example-schema-file)
17. [Known Integration Requirements](#known-integration-requirements)

---

## Quick Start

```lua
-- MyAddon_Bridge.lua (loaded AFTER your defaults and schema files)

-- 1. Define your adapter
local adapter = {
    addonName = "MyAddon",
    schema    = MY_ADDON_SCHEMA,  -- your schema table (see §Schema Structure)

    get = function(key)
        return MyAddonDB[key]
    end,

    set = function(key, val)
        MyAddonDB[key] = val
    end,

    getDefault = function(key)
        return MY_ADDON_DEFAULTS[key]
    end,

    reset = function(keys)
        for _, k in ipairs(keys) do
            MyAddonDB[k] = MY_ADDON_DEFAULTS[k]
        end
    end,
}

-- 2. Register — capture the returned context
local zsfCtx = zSettingsFrame.Register(adapter)
```

That's it. zSF handles panel creation, WoW Settings integration, sidebar navigation, widget rendering, data binding, and layout.

Use `zsfCtx:Open()` to open the panel from a slash command or minimap button:

```lua
SLASH_MYADDON1 = "/myaddon"
SlashCmdList["MYADDON"] = function()
    zsfCtx:Open()
end
```

---

## Installation

1. Copy the entire `zSettingsFrame` folder into your addon's `Libs/` directory.
2. Reference the XML loader in your `.toc` file **before** your bridge and schema files:

```toc
## Interface: 120001

Libs\zSettingsFrame\zSettingsFrame.xml

MyAddon_Defaults.lua
MyAddon_Schema.lua
MyAddon_Bridge.lua
MyAddon.lua
```

The XML loads the library files in the correct order:

```
zSF_Init.lua        → Namespace, public API, adapter registration
zSF_Style.lua       → Visual style constants (colors, dimensions)
zSF_Primitives.lua  → Low-level frame helpers, tween engine
zSF_Widgets.lua     → Primitive UI controls (pill toggle, slider, dropdown, etc.)
zSF_Constructor.lua → Schema walker, BUILDERS dispatch, row packing
zSF_Panel.lua       → Top-level panel, sidebar, tabs, scrolling, WoW integration
```

**Bundled assets** (in `Assets/`): background textures, a circle/pill mask for the toggle, a close button, and **OpenSans-CondBold.ttf** (default font for `enUS` clients; other locales fall back to `STANDARD_TEXT_FONT` unless overridden in `adapter.fontStyles`).

---

## Architecture Overview

```
┌─────────────┐     ┌──────────────┐     ┌──────────────┐
│  Your Addon  │────▶│   Adapter    │────▶│ zSettingsFrame│
│  (Bridge)    │     │  get/set/etc │     │  (Library)    │
└─────────────┘     └──────────────┘     └──────────────┘
       │                                        │
       │  Schema table                          │ Reads schema,
       ▼                                        │ builds widgets,
┌─────────────┐                                 │ binds data
│   Schema    │─────────────────────────────────┘
│  (widgets)  │
└─────────────┘
```

- **Schema**: a Lua table describing every nav item, tab, and widget. Purely declarative.
- **Adapter**: a table of callbacks (`get`, `set`, `getDefault`, `reset`) that zSF uses for all data access. zSF never touches your saved variables directly.
- **Nav Hierarchy**: resolved automatically by zSF from `adapter.schema` inside `Register()`. Tells zSF how to build the sidebar and content panels.
- **Widgets**: built automatically from the schema. Each widget reads/writes through the adapter.

---

## The Adapter

The adapter is a table you pass to `zSettingsFrame.Register()`. It tells zSF everything it needs to know about your addon.

### Required Fields

| Field | Type | Description |
|-------|------|-------------|
| `addonName` | `string` | Display name shown in the WoW Settings panel title. |
| `addonFolder` | `string` | Folder name of the addon if it differs from `addonName` (e.g. `"!GatherAddon"` when the display name is `"GatherDude"`). Used when calling `C_AddOns.GetAddOnMetadata`. Omit if the folder and display name are the same. |
| `schema` | `table` | Root schema table — an array of schema entries (see §Schema Structure). |
| `get` | `function(key) → any` | Read a setting value from your DB. Called with the widget's `key`. |
| `set` | `function(key, value)` | Write a setting value to your DB. Called when the user changes a widget. |
| `getDefault` | `function(key) → any` | Return the default value for a key. Used by reset buttons. |
| `reset` | `function(keys)` | Reset a list of keys to defaults. `keys` is an array of strings. |

### Optional Fields

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `panelWidth` | `number` | `960` | Panel width in pixels. |
| `panelHeight` | `number` | `600` | Panel height in pixels. |
| `onPanelShow` | `function()` | `nil` | Called each time the settings panel opens. |
| `onPanelHide` | `function()` | `nil` | Called each time the settings panel closes. |
| `fontStyles` | `table<string, string>` | `nil` | Locale-keyed font map: `{ enUS = "Interface\\...", koKR = "..." }`. The client locale (`GetLocale()`) selects the path. Locales absent from the table fall back to `STANDARD_TEXT_FONT`. The built-in default maps `enUS` to the bundled OpenSans-CondBold.ttf — supply an `enUS` entry to override it. |
| `navAnim` | `string` | `"glide"` | Sidebar click animation: `"flash"` (streak sweep) or `"glide"` (expand then slide off). |
| `attachOptions` | `boolean` | `true` | `true`/`nil`: registers the panel directly as the WoW Settings canvas (opens inside the Blizzard settings window). `false`: shows a stub "Open Options" button in WoW Settings instead. |
| `themeColors` | `{ primary, secondary }` | `nil` | Theme accent colors. Each value is a `{ r, g, b }` array, a `ZAF.CLASS_*` constant, or a `ColorMixin` (e.g. `RAID_CLASS_COLORS["WARRIOR"]`). Defaults to `ZAF.CLASS_EVOKER` (teal) as primary and `ZAF.CLASS_PALADIN` (pink) as secondary if omitted. |
| `fadeInAnimation` | `string` | `nil` | Content panel transition style. Set to `"slide"` to animate panels in from the left when selected. Omit (or any other value) for the default fade. |
| `subscribe` | `function(callback) → function` | `nil` | Called when the panel opens. Receives a callback that should be fired whenever external code changes a setting. Must return an unsubscribe function (called when the panel closes). When provided, zSF automatically syncs the panel to external state changes — no manual `RefreshAll()` needed. See §React / State Pattern Note. |
| `tabRowMax` | `number` | `6` | Maximum number of tabs displayed per row in the standalone panel. When the tab count exceeds this value, a second row of tabs is added automatically. |
| `tabRowMaxBlizz` | `number` | `6` | Maximum tabs per row when the panel is hosted inside the Blizzard Settings window (narrower canvas). Same row-wrapping behavior as `tabRowMax`. |
| `bgTopLayer` | `string` | `nil` | Path to the top background image layer (e.g. `"Interface\\AddOns\\MyAddon\\Assets\\my-top.png"`). Pass `"off"` to hide this layer entirely. Defaults to the built-in `bg-top.png`. |
| `bgTopLayerColor` | `number[]`, `ColorMixin`, or `"off"` | `nil` | `{ r, g, b }` array or `ColorMixin` to desaturate and tint the top layer, or `"off"` to show the image as-is (no desaturation, no tint). When `nil`, the layer is desaturated and tinted with the primary theme colour. |
| `bgBottomLayer` | `string` | `nil` | Path to the bottom background image layer. Pass `"off"` to hide this layer entirely. Defaults to the built-in `bg-bottom.png`. |
| `bgBottomLayerColor` | `number[]`, `ColorMixin`, or `"off"` | `nil` | `{ r, g, b }` array or `ColorMixin` to desaturate and tint the bottom layer, or `"off"` to show the image as-is. When `nil`, the layer is desaturated and tinted with the secondary theme colour. |
| `colors` | `table` | `nil` | Per-panel chrome color overrides (see §Style Constants). Each value is a `{ r, g, b [, a] }` array or a `ColorMixin`. Any key omitted falls back to the global default. |
| `cpuProfile` | `boolean` | `nil` / `false` | Show the host addon's CPU share in the status bar next to the version string. Displays a 1-second rolling average as a percentage of total application time (e.g. `CPU 0.4%`). Polled every second while the panel is open; silently hidden if `C_AddOnProfiler` is not enabled. |
| `memoryProfile` | `boolean` | `nil` / `false` | Show the host addon's memory usage in the status bar next to the version string. Displays in KB or MB (e.g. `Mem 128 KB`, `Mem 1.2 MB`). Polled at most once every 15 seconds; a single `UpdateAddOnMemoryUsage()` call is shared across all open zSF panels to avoid redundant work. |

---

## Public API

### `zSettingsFrame.Register(adapter)`

Register your addon with the framework. Call this at **file-load time** (before `ADDON_LOADED`). Returns a **per-addon context object** (`ctx`) that owns the panel, widgets, and fonts for this addon.

Multiple addons can each call `Register()` independently — each receives its own isolated context.

```lua
local zsfCtx = zSettingsFrame.Register(adapter)
```

When `adapter.schema` is provided, zSF resolves the nav hierarchy automatically. No pre-flight step required.

### `ctx:Open()`

Open this addon's settings panel programmatically (from a slash command, minimap button, etc.). Lazy-builds the panel on first call.

When `attachOptions` is `true` and WoW's SettingsPanel is already open, this navigates to the registered category instead of opening a duplicate window.

```lua
local zsfCtx = zSettingsFrame.Register(adapter)

SLASH_MYADDON1 = "/myaddon"
SlashCmdList["MYADDON"] = function()
    zsfCtx:Open()
end
```

`zSettingsFrame.Open(addonName)` is also available as a named dispatcher when you don't have the context at hand. Pass the `addonName` string to open that addon's panel.

### `ctx:RefreshAll(force)`

Re-read every widget's value from the DB through `adapter.get` and re-evaluate all `showWhen` / `disableWhen` conditions — on widgets, nav entries, navChild entries, and tab entries — for this addon's panel.

- The panel's `OnShow` calls this automatically.
- Call it explicitly when external code writes settings while the panel might be visible.
- Pass `force = true` to skip change-detection optimisation (use on profile switches where many values change at once).

```lua
zsfCtx:RefreshAll()       -- normal refresh
zsfCtx:RefreshAll(true)   -- force full refresh (profile switch)
```

`zSettingsFrame.RefreshAll(force, addonName)` is also available. Pass `addonName` to target a specific addon, or omit it to refresh all registered addons.

---

## Schema Structure

The schema is a flat array of **entries**. Each entry is a Lua table with at minimum a `widgetType` field. The top level must contain only `nav` entries (sidebar items). Everything else is nested inside `nav` via `children`.

```lua
MY_ADDON_SCHEMA = {
    {
        widgetType = "nav",
        name       = "General",
        children   = {
            { widgetType = "header", name = "Appearance" },
            { widgetType = "toggle", key = "showTooltips",  name = "Show Tooltips",  desc = "Display tooltips on hover." },
            { widgetType = "range",  key = "iconSize",      name = "Icon Size",      desc = "Size of icons in pixels.", min = 12, max = 64, step = 1 },
        },
    },
    {
        widgetType = "nav",
        name       = "Advanced",
        children   = { ... },
    },
}
```

---

## Navigation Hierarchy

The sidebar is built from the schema's top-level `nav` entries. Inside a `nav`, you can nest:

- **Plain widgets** — rendered directly in that nav page.
- **`navChild`** entries — create sub-items in the sidebar. Each has its own content panel.
- **`tab`** entries — create a tab strip at the top of the content area.

A `nav` can have `navChild` entries **and** `tab` entries simultaneously. When both are present, clicking the nav parent shows the tabs; clicking a sidebar child shows that child's content. If the nav also has plain widgets (own content), they are ignored when tabs exist — tabs replace the parent's own content area.

`navChild` entries **cannot** have further `navChild` nesting. Their `children` **can** contain `tab` entries — the tab strip appears when that navChild is selected in the sidebar.

`tab` entries cannot have further `tab` or `navChild` nesting.

When the number of tabs exceeds `adapter.tabRowMax` (or `adapter.tabRowMaxBlizz` when hosted inside the Blizzard window), a second row of tabs is added automatically — the tab list is never truncated. Both settings default to `6` per row.

### How It Works

When you include `schema = MY_ADDON_SCHEMA` in your adapter table, `Register()` automatically walks the schema and builds the internal nav hierarchy. You do not need to call anything extra.

### Schema Examples

**Plain nav** (widgets directly inside):
```lua
{ widgetType = "nav", name = "General", children = {
    { widgetType = "toggle", key = "enabled", name = "Enable Addon" },
}}
```

> **`desc` on nav entries**: A top-level `nav` entry accepts an optional `desc` field. When provided, it is displayed as a subtitle line in the content area header below the nav name — useful for a brief section summary.
> ```lua
> { widgetType = "nav", name = "Advanced", desc = "Power-user options.", children = { ... } }
> ```

**Nav with sidebar children** (each `navChild` is a sub-page):
```lua
{ widgetType = "nav", name = "Trackers", children = {
    -- Plain widgets here become the nav parent's own content page
    { widgetType = "header", name = "Global Tracker Settings" },
    { widgetType = "toggle", key = "syncTrackers", name = "Sync All" },
    -- navChild entries become sidebar sub-items
    { widgetType = "navChild", name = "Tracker 1", trackerID = 0, children = {
        { widgetType = "toggle", key = "enabled", name = "Enable", trackerKey = true },
    }},
    { widgetType = "navChild", name = "Tracker 2", trackerID = 1, children = {
        { widgetType = "toggle", key = "enabled", name = "Enable", trackerKey = true },
    }},
}}
```

**Nav with tabs**:
```lua
{ widgetType = "nav", name = "Colors", children = {
    { widgetType = "tab", name = "Text", children = {
        { widgetType = "color", key = "textColor", name = "Text Color" },
    }},
    { widgetType = "tab", name = "Background", children = {
        { widgetType = "color", key = "bgColor", name = "Background Color" },
    }},
}}
```

**Nav with sidebar children AND tabs** (tabs replace the parent content area):
```lua
{ widgetType = "nav", name = "Trackers", children = {
    -- Tab entries shown when the nav parent is selected
    { widgetType = "tab", name = "Layout", children = {
        { widgetType = "toggle", key = "compactMode", name = "Compact Mode" },
    }},
    { widgetType = "tab", name = "Assignment", children = {
        { widgetType = "header", name = "Category Assignment" },
    }},
    -- navChild entries remain in the sidebar
    { widgetType = "navChild", name = "Tracker 1", trackerID = 0, children = {
        { widgetType = "toggle", key = "enabled", name = "Enable", trackerKey = true },
    }},
    { widgetType = "navChild", name = "Tracker 2", trackerID = 1, children = {
        { widgetType = "toggle", key = "enabled", name = "Enable", trackerKey = true },
    }},
}}
```

**navChild with tabs** (tabs appear when the navChild is selected):
```lua
{ widgetType = "nav", name = "Appearance", children = {
    { widgetType = "navChild", name = "Fonts", children = {
        { widgetType = "range", key = "fontSize", name = "Font Size", min = 8, max = 24, step = 1 },
    }},
    { widgetType = "navChild", name = "Colors", children = {
        { widgetType = "tab", name = "Text", children = {
            { widgetType = "color", key = "textColor", name = "Text Color" },
        }},
        { widgetType = "tab", name = "Background", children = {
            { widgetType = "colorAlpha", key = "bgColor", name = "Background Color" },
        }},
    }},
}}
```

---

## Conditional Visibility on Nav and Tab Entries

`showWhen` and `disableWhen` can be placed on `nav`, `navChild`, and `tab` schema entries — not just on widget entries. Every `RefreshAll()` re-evaluates these conditions and rebuilds the sidebar and tab strip accordingly.

Unlike widget-level callbacks (which receive `trackerID` as an argument), nav and tab callbacks receive **no arguments**.

### `nav` entries

```lua
{
    widgetType = "nav",
    name       = "Advanced",
    showWhen   = function() return MyAddonDB.advancedMode end,
    children   = { ... },
}
```

- `showWhen = function() → boolean` — when it returns `false`, the entire nav entry and its sidebar button are hidden. The sidebar reflows automatically. If the entry was active when hidden, the framework auto-selects the first remaining visible nav item.
- `disableWhen = function() → boolean` — when it returns `true`, the nav label is greyed out and the button is unclickable. The item remains visible but non-interactive.

### `navChild` entries

```lua
{ widgetType = "nav", name = "Trackers", children = {
    {
        widgetType = "navChild",
        name       = "Tracker 1",
        trackerID  = 0,
        showWhen   = function() return MyAddonDB.trackerCount > 0 end,
        children   = { ... },
    },
    {
        widgetType = "navChild",
        name       = "Tracker 2",
        trackerID  = 1,
        showWhen   = function() return MyAddonDB.trackerCount > 1 end,
        children   = { ... },
    },
}}
```

Same semantics as nav-level `showWhen` / `disableWhen`. Hidden children collapse in the sidebar list; the framework auto-corrects the active selection if the currently active child becomes hidden.

### `tab` entries

```lua
{ widgetType = "nav", name = "Display", children = {
    { widgetType = "tab", name = "Basic",    children = { ... } },
    {
        widgetType = "tab",
        name       = "Advanced",
        showWhen   = function() return MyAddonDB.advancedMode end,
        children   = { ... },
    },
}}
```

- When `showWhen` returns `false`, the tab is removed from the strip and the strip reflows. If the active tab is hidden, the framework switches to the first visible tab automatically.
- If **all** tabs in a strip evaluate to hidden, the entire tab strip collapses.
- `disableWhen` greys out the tab button and prevents clicks. The tab remains visible in the strip.

Tab entries inside `navChild` blocks work identically:

```lua
{ widgetType = "navChild", name = "Colors", children = {
    { widgetType = "tab", name = "Text",   children = { ... } },
    {
        widgetType = "tab",
        name       = "Border",
        showWhen   = function() return MyAddonDB.showBorder end,
        children   = { ... },
    },
}}
```

---

## Widget Reference

Every widget type and its type-specific fields. All widgets also accept the [Common Schema Fields](#common-schema-fields).

### `toggle`

A pill-shaped on/off toggle switch. Stored value is `boolean`.

| Field | Type | Description |
|-------|------|-------------|
| `key` | `string` | DB key. |
| `confirm` | `string` | Confirmation prompt shown for any direction change. |
| `confirmOn` | `string` | Confirmation prompt shown only when turning the toggle **on**. |
| `confirmOff` | `string` | Confirmation prompt shown only when turning the toggle **off**. |

`confirm`, `confirmOn`, and `confirmOff` are mutually exclusive — `confirm` takes priority if more than one is set.

**Default width**: `full`. Supports: `full`, `half`, `third`, `quarter`.

---

### `range`

A fill-bar drag slider with numeric input and stepper arrows.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `key` | `string` | yes | DB key. |
| `min` | `number` | yes | Minimum value. |
| `max` | `number` | yes | Maximum value. |
| `step` | `number` | yes | Step increment. Controls decimal precision in the display. |

**Default width**: `full`. Supports: `full`, `half`, `third`, `quarter`.

---

### `select`

A dropdown with a popup item list.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `key` | `string` | yes | DB key. Stored value is the selected key from `values`. |
| `values` | `table` or `function() → table` | yes | Map of `{ key = "Display Text" }`. |
| `order` | `table` | no | Array of keys specifying menu order. Without it, keys sort alphabetically. |
| `selected` | `string` | no | Default display text when no value is set. |

**Default width**: `full`. Supports: `full`, `half`, `third`, `quarter`.

---

### `color`

An RGB color swatch that opens a color picker popup.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `key` | `string` | yes | DB key. Stored value is `{ r, g, b }` (0–1 each). |

**Default width**: `full`. Supports: `full`, `half`, `third`, `quarter`.

---

### `colorAlpha`

An RGBA color swatch with an alpha percentage label. Same as `color` but includes an alpha channel.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `key` | `string` | yes | DB key. Stored value is `{ r, g, b, a }` (0–1 each). |

**Default width**: `full`. Supports: `full`, `half`, `third`, `quarter`.

---

### `input`

A single-line text edit box.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `key` | `string` | yes | DB key. |
| `maxLen` | `number` | no | Maximum character count. |
| `default` | `string` | no | Pre-fill text for empty fields. |
| `placeholder` | `string` | no | Greyed-out hint text shown when the box is empty. |

**Default width**: `full`. Supports: `full`, `half`, `third`, `quarter`.

---

### `header`

A section title with a horizontal rule line. No data binding — purely decorative.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | `string` | yes | Header text. |

**Default width**: `full`. Fixed full-width only.

---

### `spacer`

Forces the next widget onto a new row. With no options a spacer has zero height — it only flushes the current partial row. Add padding and/or a separator line for visible spacing.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `separator` | `boolean` | `false` | When `true`, draws a 1px horizontal rule in the secondary theme color at 30% opacity. |
| `breakSection` | `boolean` | `false` | When `true`, closes the current section border so it does not extend past this spacer. |
| `paddingTop` | `number` | `4` when `separator = true`, `0` otherwise | Pixels of space above the separator line (or above the spacer when no separator). |
| `paddingBottom` | `number` | `4` when `separator = true`, `0` otherwise | Pixels of space below the separator line (or below the spacer when no separator). |

**Default width**: `full`. Fixed full-width only.

```lua
-- Bare row flush (zero height)
{ widgetType = "spacer" }

-- Visible separator with padding
{ widgetType = "spacer", separator = true, paddingTop = 6, paddingBottom = 6 }
```

---

### `button`

An action button — typically used for reset-to-defaults or custom actions.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | `string` | yes | Button label text. |
| `resetKeys` | `string[]` | no | Array of DB keys to reset when clicked (calls `adapter.reset`). Also causes the reset icon to appear by default. |
| `showResetIcon` | `boolean` | no | Force the reset (refresh) icon on (`true`) or off (`false`). Defaults to `nil`, which shows the icon only when `resetKeys` is provided. |
| `func` | `function()` | no | Custom click handler. Runs instead of reset if both are provided. |
| `iconAtlas` | `string` | no | WoW atlas name for an icon on the button. |
| `colorIconAtlas` | `false\|string\|table` | no | How to colour the icon. `false` = show atlas raw (no tint). `"primary"` or `"secondary"` = desaturate + tint with that colour. `{ r, g, b }` = desaturate + tint with a custom colour. Omit (or `nil`) to default to `"secondary"` when `iconAtlas` is set. |
| `rotateIcon` | `number` | no | Clockwise rotation in degrees applied to the icon (e.g. `90`, `45`, `180`). |
| `flipIcon` | `"horizontal"\|"vertical"` | no | Mirror the icon on the given axis. |
| `desc` | `string` | no | Tooltip description. |
| `borderColor` | `string\|table` | no | Border colour: `"primary"` (default), `"secondary"`, or `{ r, g, b }`. |

**Default width**: `third`. Supports: `full`, `half`, `third`, `quarter`. Three buttons pack into one row by default.

---

### `filler`

An invisible layout spacer. Takes up width in a multi-column row without rendering anything. Useful for pushing buttons or other partial-width widgets to a specific column position.

**Default width**: `full`. Supports any width.

---

### `text`

A static text label. No data binding.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | `string` | yes | Text content. |
| `height` | `number` | no | Custom row height in pixels. |
| `fontObject` | `string` | no | WoW font object name (e.g. `"GameFontNormalLarge"`). |

**Default width**: `full`.

---

### `group`

A container for child widgets. Renders children inline without any visual distinction — purely for logical grouping.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `children` | `table[]` | yes | Array of schema entries to render as children. |

**Default width**: `full`.

---

### `textarea`

A multi-line scrollable text input.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `key` | `string` | yes | DB key. |
| `lines` | `number` | no | Visible line count (determines height). Default: 6. |
| `maxLetters` | `number` | no | Maximum character count. |
| `placeholder` | `string` | no | Greyed-out hint text. |

**Default width**: `full`.

---

### `output`

A read-only variant of `textarea`. Identical fields but the text is not editable. Useful for displaying computed or diagnostic information.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `key` | `string` | yes | DB key (read from `adapter.get`). |
| `lines` | `number` | no | Visible line count. Default: 6. |

**Default width**: `full`.

---

### `orderList`

A reorderable list of items. Supports two styles: arrow buttons (default) and drag-and-drop.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `key` | `string` | yes | DB key. Stored value is an ordered array of keys. |
| `items` | `table` or `function() → table` | yes | Map of `{ key = "Display Text" }`. |
| `order` | `table` | no | Initial/fallback order (array of keys). Used when no saved value exists. |
| `orderStyle` | `string` | no | `"drag"` for drag-and-drop. Omit for arrow buttons (default). |
| `name` | `string` | no | Label displayed above the item list. |
| `desc` | `string` | no | Description text shown inline below the name. |

**Default width**: `full`.

---

### `multiselect`

A dropdown that opens a checkbox list for multiple selections.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `key` | `string` | yes | DB key. Stored value is a table of `{ [key] = boolean }`. |
| `values` | `table` or `function() → table` | yes | Map of `{ key = "Display Text" }`. |
| `order` | `table` | no | Array of keys specifying display order. |

**Default width**: `full`.

---

### `keybind`

A key-capture button for binding a keyboard shortcut.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `key` | `string` | yes | DB key. Stored value is a WoW key string (e.g. `"CTRL-F"`) or `nil`. |

**Default width**: `full`.

---

### `fontSelect`

A dropdown with a live font preview for each entry. Designed for use with LibSharedMedia but works with any `values` + `previewFetch` pair.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `key` | `string` | yes | DB key. |
| `values` | `function() → table` | yes | Returns `{ key = "Display Text" }` (e.g. LSM font names). |
| `previewFetch` | `function(key) → string` | yes | Returns the font file path for a given key (e.g. LSM fetch). |

**Default width**: `full`.

---

### `sound`

A dropdown with a speaker icon button to preview sounds. Similar to `fontSelect` but for audio.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `key` | `string` | yes | DB key. |
| `values` | `function() → table` | yes | Returns `{ key = "Display Text" }`. |
| `previewFetch` | `function(key) → string` | yes | Returns the sound file path for a given key. |

**Default width**: `full`.

---

### `bgSelect` / `barSelect` / `borderSelect`

Dropdown selectors that show a texture swatch preview for each item. Three separate type names for semantic clarity, but they share the same builder. Use for LSM background, statusbar, and border textures respectively.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `key` | `string` | yes | DB key. |
| `values` | `function() → table` | yes | Returns `{ key = "Display Text" }`. |
| `previewFetch` | `function(key) → string` | yes | Returns the texture file path for a given key. |

**Default width**: `full`.

---

## Common Schema Fields

These fields are available on **all** widget types (in addition to their type-specific fields):

| Field | Type | Description |
|-------|------|-------------|
| `widgetType` | `string` | **Required.** One of the type names above. |
| `key` | `string` | DB key used for `adapter.get(key)` / `adapter.set(key, value)`. |
| `name` | `string` | The label displayed next to the widget. |
| `desc` | `string` | Description text — shown inline below the name and/or in the tooltip. |
| `width` | `string` | Layout width: `"full"`, `"half"`, `"third"`, or `"quarter"`. Defaults vary by type. Partial-width widgets are packed side-by-side in rows automatically. |
| `widthBlizzard` | `string` | Explicit width override when the panel is hosted inside WoW's Settings window. When omitted, all widgets default to `"full"` in hosted mode (the `width` field is ignored). |
| `interactableWidth` | `string` | Controls how wide the interactive control portion is on `full` and `half` rows for `input`, `select`, `multiselect`, `fontSelect`, `sound`, `bgSelect`, `barSelect`, and `borderSelect`. Set to `"third"` to size it like a third-column cell instead of the default quarter-column. |
| `showWhen` | `function(trackerID) → boolean` | When provided, the widget is only visible when this returns `true`. Re-evaluated on every `RefreshAll()`. The panel dynamically reflows when visibility changes. |
| `disableWhen` | `function(trackerID) → boolean` | When provided, the widget is greyed out and non-interactive when this returns `true`. |
| `confirm` | `string` | Confirmation dialog text. If set, a "Confirm / Cancel" popup appears before committing the change. |
| `get` | `function(key) → any` | Custom getter — overrides `adapter.get` for this widget only. |
| `set` | `function(key, value)` | Custom setter — overrides `adapter.set` for this widget only. |
| `descVisibility` | `string` | Set to `"hide"` to suppress the inline description text (desc is still shown in the tooltip). |
| `tooltipVisibility` | `string` | Set to `"hide"` to suppress the tooltip entirely. |
| `divider` | `boolean` | Set to `false` to hide the vertical column divider in multi-column rows. |
| `trackerKey` | `boolean` | When `true`, the actual DB key becomes `"t{trackerID}{key}"`. Allows the same `key` to be reused per tracker/instance. |
| `trackerID` | `number` | Per-entry tracker ID override. Normally inherited from the parent `navChild`. |
| `children` | `table[]` | Child schema entries. Used by `nav`, `navChild`, `tab`, and `group`. |

### Layout (Width & Row Packing)

Widgets are placed top-to-bottom. Partial-width widgets (`half`, `third`, `quarter`) are packed side-by-side in rows until the row fills up, then the row is flushed and a new one starts. Full-width widgets always start a new row.

Example: two `half` toggles share one row:
```lua
{ widgetType = "toggle", key = "opt1", name = "Option 1", width = "half" },
{ widgetType = "toggle", key = "opt2", name = "Option 2", width = "half" },
```

Example: three `third` buttons share one row (this is the default for buttons):
```lua
{ widgetType = "button", name = "Reset Colors", resetKeys = { "color1", "color2" } },
{ widgetType = "button", name = "Reset Fonts",  resetKeys = { "font1" } },
{ widgetType = "button", name = "Reset All",    resetKeys = { "color1", "color2", "font1" } },
```

### Conditional Visibility (`showWhen`)

`showWhen` is re-evaluated every time `RefreshAll()` runs (including on panel open). When visibility changes, the content panel dynamically reflows — hidden widgets collapse and remaining widgets close the gap.

```lua
{
    widgetType = "range",
    key        = "iconSize",
    name       = "Icon Size",
    min = 12, max = 64, step = 1,
    showWhen   = function()
        return MyAddonDB.showIcons == true
    end,
},
```

### Conditional Disable (`disableWhen`)

The widget remains visible but becomes non-interactive and dims to 40% alpha.

```lua
{
    widgetType = "select",
    key        = "theme",
    name       = "Theme",
    values     = { dark = "Dark", light = "Light" },
    disableWhen = function()
        return MyAddonDB.locked == true
    end,
},
```

### Tracker Keys (`trackerKey` / `trackerID`)

When `trackerKey = true`, the widget's actual DB key is prefixed with `"t{trackerID}"`. This lets you reuse the same schema for multiple instances (e.g. multiple tracker panels) with per-instance settings.

```lua
-- Inside a navChild with trackerID = 2:
{ widgetType = "toggle", key = "enabled", name = "Enable", trackerKey = true }
-- Actual DB key: "t2enabled"
-- adapter.get("t2enabled") is called
```

---

## Style Constants

Visual constants are defined in `zAF_Style.lua` as globals.

The recommended way to theme a panel is via **`adapter.colors`** — each key overrides the matching global for that panel's build pass only:

```lua
local adapter = {
    ...
    colors = {
        sidebarBg     = { 0.05, 0.05, 0.10, 0.55 },  -- darker blue-tinted sidebar
        rowHover      = { 0.20, 0.50, 1.00, 0.14 },
        border        = { 0.30, 0.60, 1.00, 1.00 },
        label         = { 0.95, 0.95, 1.00, 1.00 },
        desc          = { 0.55, 0.60, 0.70, 1.00 },
        headerText    = { 0.40, 0.70, 1.00, 1.00 },
        disabled      = { 0.40, 0.40, 0.45, 1.00 },
        disabledAlpha = 0.35,
        tabActive     = { 0.00, 0.00, 0.10, 0.40 },
        separator     = { 0.20, 0.25, 0.35, 1.00 },
        rowStripeOdd  = { 1.00, 1.00, 1.00, 0.05 },
        rowStripeEven = { 0.00, 0.00, 0.05, 0.30 },
        colDivider    = { 1.00, 1.00, 1.00, 0.05 },
    },
}
```

All keys are optional — omitted keys fall back to the global default (or the PRI-driven value for `border` and `headerText`). `ZAF.COLOR_PANEL_BG` and the class-color constants are not overridable through `adapter.colors`. Color values may be `{ r, g, b [, a] }` arrays or `ColorMixin` objects — zSF normalizes both formats automatically.

You can still override the globals directly in your bridge file before calling `Register()` — that remains valid for simple one-addon setups.

### Colors

| Constant | `adapter.colors` key | Default | Description |
|----------|----------------------|---------|-------------|
| `ZAF.COLOR_PANEL_BG` | *(not overridable via adapter)* | `{ 0, 0, 0, 0.10 }` | Content area background. |
| `ZAF.COLOR_SIDEBAR_BG` | `sidebarBg` | `{ 0, 0, 0, 0.45 }` | Sidebar, title bar, tab strip overlay. |
| `ZAF.COLOR_ROW_HOVER` | `rowHover` | `{ 1.0, 0.90, 0.65, 0.12 }` | Row hover highlight tint. |
| `ZAF.COLOR_BORDER` | `border` | PRI-driven | Outer border and widget edges. Defaults to primary theme colour. |
| `ZAF.COLOR_LABEL` | `label` | `{ 0.90, 0.90, 0.90, 1.0 }` | Primary label text. |
| `ZAF.COLOR_DESC` | `desc` | `{ 0.58, 0.58, 0.58, 1.0 }` | Description / sub-label text. |
| `ZAF.COLOR_HEADER_TEXT` | `headerText` | PRI-driven | Section header text. Defaults to primary theme colour. |
| `ZAF.COLOR_DISABLED` | `disabled` | `{ 0.38, 0.38, 0.38, 1.0 }` | Disabled control text/icons. |
| `ZAF.COLOR_TAB_ACTIVE` | `tabActive` | `{ 0, 0, 0, 0.35 }` | Active tab/nav highlight. |
| `ZAF.COLOR_SEPARATOR` | `separator` | `{ 0.22, 0.22, 0.22, 1.0 }` | Horizontal separator lines. |
| `ZAF.COLOR_ROW_STRIPE_ODD` | `rowStripeOdd` | `{ 0, 0, 0, 0.08 }` | Odd row stripe background. |
| `ZAF.COLOR_ROW_STRIPE_EVEN` | `rowStripeEven` | `{ 0, 0, 0, 0.32 }` | Even row stripe background. |
| `ZAF.COLOR_COL_DIVIDER` | `colDivider` | `{ 1, 1, 1, 0.06 }` | Vertical column divider in multi-column rows. |
| `ZAF.DISABLED_ALPHA` | `disabledAlpha` | `0.40` | Alpha for disabled widgets (plain number, not a color table). |

### Dimensions (logical pixels)

| Constant | Default | Description |
|----------|---------|-------------|
| `ZSF.ROW_HEIGHT` | `50` | Standard setting row height. |
| `ZSF.HEADER_HEIGHT` | `33` | Section header row height. |
| `ZSF.HEADER_PAD_TOP` | `20` | Top buffer above each section header's text. |
| `ZSF.SPACER_HEIGHT` | `8` | Spacer height. |
| `ZSF.SIDEBAR_WIDTH` | `225` | Sidebar width. |
| `ZSF.CONTENT_PAD_X` | `42` | Horizontal padding inside the content area. |
| `ZSF.CONTENT_PAD_TOP` | `10` | Vertical padding at the top of the content area. |
| `ZSF.ROW_GAP_Y` | `0` | Gap between rows. |
| `ZSF.ACTION_COL_WIDTH` | `120` | Width reserved for action controls on the right. |
| `ZSF.BORDER_SIZE` | `1` | Border line thickness. |
| `ZSF.COL_INNER_PAD` | `6` | Inner padding against column dividers. |

### Theme Colors

zSF uses two theme accent colors, **primary** and **secondary**. Set them via the adapter:

```lua
local adapter = {
    ...
    themeColors = {
        primary   = { 0.85, 0.60, 0.00 },  -- accent for borders, headers, nav highlight
        secondary = { 1.00, 0.82, 0.00 },  -- accent for toggles, sliders, hover states
    },
}
```

If omitted, zSF uses `ZAF.CLASS_EVOKER` (teal) as primary and `ZAF.CLASS_PALADIN` (pink) as secondary. You can pass raw `{ r, g, b }` arrays, any `ZAF.CLASS_*` constant, or a `ColorMixin` object such as a `RAID_CLASS_COLORS` entry — zSF normalizes all formats automatically:

```lua
-- All three forms are equivalent:
themeColors = { primary = RAID_CLASS_COLORS["WARRIOR"],        secondary = RAID_CLASS_COLORS["MAGE"] }
themeColors = { primary = ZAF.CLASS_WARRIOR,                   secondary = ZAF.CLASS_MAGE }
themeColors = { primary = { 0.78, 0.61, 0.43 },               secondary = { 0.25, 0.78, 0.92 } }
```

### Class Color Constants

Pre-defined class color tables available as `ZAF.CLASS_*` (defined in `zAF_Style.lua`, available at adapter-definition time):

| Constant | Class | Value |
|----------|-------|-------|
| `ZAF.CLASS_DEATHKNIGHT` | Death Knight | `{ 0.77, 0.12, 0.23 }` |
| `ZAF.CLASS_DEMONHUNTER` | Demon Hunter | `{ 0.64, 0.19, 0.79 }` |
| `ZAF.CLASS_DRUID` | Druid | `{ 1.0, 0.49, 0.04 }` |
| `ZAF.CLASS_EVOKER` | Evoker | `{ 0.20, 0.58, 0.50 }` |
| `ZAF.CLASS_HUNTER` | Hunter | `{ 0.67, 0.83, 0.45 }` |
| `ZAF.CLASS_MAGE` | Mage | `{ 0.25, 0.78, 0.92 }` |
| `ZAF.CLASS_MONK` | Monk | `{ 0.0, 1.0, 0.60 }` |
| `ZAF.CLASS_PALADIN` | Paladin | `{ 0.96, 0.55, 0.73 }` |
| `ZAF.CLASS_PRIEST` | Priest | `{ 1.0, 1.0, 1.0 }` |
| `ZAF.CLASS_ROGUE` | Rogue | `{ 1.0, 0.96, 0.41 }` |
| `ZAF.CLASS_SHAMAN` | Shaman | `{ 0.0, 0.44, 0.87 }` |
| `ZAF.CLASS_WARLOCK` | Warlock | `{ 0.53, 0.53, 0.93 }` |
| `ZAF.CLASS_WARRIOR` | Warrior | `{ 0.78, 0.61, 0.43 }` |

```lua
themeColors = {
    primary   = ZAF.CLASS_DEATHKNIGHT,
    secondary = ZAF.CLASS_HUNTER,
},
```

### `ZSF.NormalizeColor(c)`

A public utility that converts any color input to a plain numeric array `{ r, g, b [, a] }` suitable for `unpack()` or array indexing. Useful if your addon code handles colors in mixed formats.

- `{ r, g, b [, a] }` arrays are returned as-is.
- `ColorMixin` / field-keyed tables (`{ r = ..., g = ..., b = ... }`) are converted to arrays.
- Non-table values (`nil`, `"off"`) are returned unchanged.

```lua
local arr = ZSF.NormalizeColor(RAID_CLASS_COLORS["WARRIOR"])
-- arr is now { 0.78, 0.61, 0.43, nil }
```

---

## LibSharedMedia Integration

zSF has zero dependency on LibSharedMedia (LSM). The `fontSelect`, `sound`, `bgSelect`, `barSelect`, and `borderSelect` widget types are **consumer-driven**: you provide `values()` and `previewFetch()` callbacks in the schema, and zSF renders previews using whatever strings those callbacks return.

If your addon uses LSM, wire it like this:

```lua
local LSM = LibStub("LibSharedMedia-3.0")

-- Helper: return a function that provides { key = displayName } for a media type.
local function LSM_Values(mediaType)
    return function()
        local t = {}
        for _, name in ipairs(LSM:List(mediaType)) do
            t[name] = name
        end
        return t
    end
end

-- Helper: return a function that fetches the file path for a given key.
local function LSM_Fetch(mediaType)
    return function(key)
        return LSM:Fetch(mediaType, key)
    end
end
```

Then in your schema:
```lua
{
    widgetType   = "fontSelect",
    key          = "mainFont",
    name         = "Main Font",
    desc         = "Font used for quest text.",
    values       = LSM_Values("font"),
    previewFetch = LSM_Fetch("font"),
},
{
    widgetType   = "barSelect",
    key          = "barTexture",
    name         = "Bar Texture",
    values       = LSM_Values("statusbar"),
    previewFetch = LSM_Fetch("statusbar"),
},
{
    widgetType   = "bgSelect",
    key          = "bgTexture",
    name         = "Background",
    values       = LSM_Values("background"),
    previewFetch = LSM_Fetch("background"),
},
{
    widgetType   = "sound",
    key          = "alertSound",
    name         = "Alert Sound",
    values       = LSM_Values("sound"),
    previewFetch = LSM_Fetch("sound"),
},
```

If your addon does **not** use LSM, you can provide plain tables:
```lua
{
    widgetType   = "fontSelect",
    key          = "mainFont",
    name         = "Main Font",
    values       = function()
        return {
            default  = "Default",
            frizqt   = "Friz Quadrata",
            morpheus = "Morpheus",
        }
    end,
    previewFetch = function(key)
        local paths = {
            default  = "Fonts\\FRIZQT__.TTF",
            frizqt   = "Fonts\\FRIZQT__.TTF",
            morpheus = "Fonts\\MORPHEUS.TTF",
        }
        return paths[key] or "Fonts\\FRIZQT__.TTF"
    end,
},
```

---

## React / State Pattern Note

If your addon uses a reactive / signal-based state system (e.g. custom observables, or a pattern like React's `useState`), the adapter pattern maps cleanly:

- `adapter.get(key)` → read from your reactive store.
- `adapter.set(key, val)` → write to your reactive store. If your store fires side-effects, they run normally.
- `adapter.subscribe(callback)` → wire your store's change notification (event bus, signal, callback, etc.) to the callback. Return an unsubscribe function.

When `subscribe` is provided, zSF automatically calls `RefreshAll()` whenever the callback fires — debounced to one refresh per frame. Writes originating from zSF's own widgets are excluded automatically (via an internal guard flag), so you won't get redundant refreshes when the user interacts with the panel.

This means the panel stays in sync with your DB at all times while it's open, with no manual wiring required.

### Example: subscribe with CallbackHandler

```lua
subscribe = function(callback)
    MyAddon.events:RegisterCallback("SETTING_CHANGED", callback, "zSF_Sub")
    return function()
        MyAddon.events:UnregisterCallback("SETTING_CHANGED", "zSF_Sub")
    end
end,
```

### Example: subscribe with AceDB profile switches

```lua
subscribe = function(callback)
    MyAddon.db.RegisterCallback(MyAddon, "OnProfileChanged", callback)
    MyAddon.db.RegisterCallback(MyAddon, "OnProfileCopied",  callback)
    MyAddon.db.RegisterCallback(MyAddon, "OnProfileReset",   callback)
    return function()
        MyAddon.db.UnregisterCallback(MyAddon, "OnProfileChanged")
        MyAddon.db.UnregisterCallback(MyAddon, "OnProfileCopied")
        MyAddon.db.UnregisterCallback(MyAddon, "OnProfileReset")
    end
end,
```

### Without subscribe

If you don't provide `subscribe`, zSF falls back to pull-only mode: it reads values on-demand via `adapter.get()` and refreshes only when `RefreshAll()` is called explicitly or when individual widgets are interacted with. You are then responsible for calling `RefreshAll()` whenever external code writes settings while the panel is open.

---

## Example: Bridge Without AceDB

This example uses raw `SavedVariables` with no library dependencies.

### `MyAddon.toc`
```toc
## Interface: 120001
## Title: MyAddon
## SavedVariables: MyAddonDB

Libs\zSettingsFrame\zSettingsFrame.xml

MyAddon_Defaults.lua
MyAddon_Schema.lua
MyAddon_Bridge.lua
MyAddon.lua
```

### `MyAddon_Defaults.lua`
```lua
MY_ADDON_DEFAULTS = {
    enabled     = true,
    iconSize    = 24,
    showTooltips = true,
    textColor   = { 1.0, 0.9, 0.7 },
    bgAlpha     = 0.8,
    greeting    = "Hello, World!",
}
```

### `MyAddon_Bridge.lua`
```lua
-- Initialize saved variables on first load.
local f = CreateFrame("Frame")
f:RegisterEvent("ADDON_LOADED")
f:SetScript("OnEvent", function(self, event, addonName)
    if addonName ~= "MyAddon" then return end
    self:UnregisterEvent("ADDON_LOADED")

    -- Ensure DB exists and fill in missing defaults.
    if not MyAddonDB then MyAddonDB = {} end
    for k, v in pairs(MY_ADDON_DEFAULTS) do
        if MyAddonDB[k] == nil then
            -- Deep-copy tables so defaults aren't shared by reference.
            if type(v) == "table" then
                local copy = {}
                for i, val in ipairs(v) do copy[i] = val end
                for key, val in pairs(v) do
                    if type(key) ~= "number" then copy[key] = val end
                end
                MyAddonDB[k] = copy
            else
                MyAddonDB[k] = v
            end
        end
    end
end)

-- Adapter.
local adapter = {
    addonName   = "MyAddon",
    schema      = MY_ADDON_SCHEMA,
    themeColors = { primary = { 0.40, 0.70, 1.00 }, secondary = { 0.60, 0.85, 1.00 } },

    get = function(key)
        return MyAddonDB[key]
    end,

    set = function(key, val)
        MyAddonDB[key] = val
    end,

    getDefault = function(key)
        return MY_ADDON_DEFAULTS[key]
    end,

    reset = function(keys)
        for _, k in ipairs(keys) do
            local def = MY_ADDON_DEFAULTS[k]
            if type(def) == "table" then
                local copy = {}
                for i, v in ipairs(def) do copy[i] = v end
                for key, v in pairs(def) do
                    if type(key) ~= "number" then copy[key] = v end
                end
                MyAddonDB[k] = copy
            else
                MyAddonDB[k] = def
            end
        end
    end,

    panelWidth  = 960,
    panelHeight = 600,
}

-- Register.
local zsfCtx = zSettingsFrame.Register(adapter)

-- Slash command.
SLASH_MYADDON1 = "/myaddon"
SlashCmdList["MYADDON"] = function()
    zsfCtx:Open()
end
```

---

## Example: Bridge With AceDB

If your addon uses AceDB-3.0 for profile management, the bridge looks like this:

### `MyAddon_Bridge.lua`
```lua
-- Adapter.  db.profile is the AceDB profile table.
-- This assumes MyAddon.db is set up in your core file's OnInitialize.
local adapter = {
    addonName   = "MyAddon",
    schema      = MY_ADDON_SCHEMA,
    themeColors = { primary = { 0.85, 0.60, 0.00 }, secondary = { 1.00, 0.82, 0.00 } },

    get = function(key)
        return MyAddon.db.profile[key]
    end,

    set = function(key, val)
        MyAddon.db.profile[key] = val
    end,

    getDefault = function(key)
        return MyAddon.db.defaults.profile[key]
    end,

    reset = function(keys)
        for _, k in ipairs(keys) do
            MyAddon.db.profile[k] = MyAddon.db.defaults.profile[k]
        end
    end,

    panelWidth  = 1080,
    panelHeight = 700,

    onPanelShow = function()
        -- Sync anything that needs to be fresh when the panel opens.
    end,

    -- Auto-sync the panel whenever the addon writes settings externally.
    subscribe = function(callback)
        MyAddon.db.RegisterCallback(MyAddon, "OnProfileChanged", callback)
        MyAddon.db.RegisterCallback(MyAddon, "OnProfileCopied",  callback)
        MyAddon.db.RegisterCallback(MyAddon, "OnProfileReset",   callback)
        return function()
            MyAddon.db.UnregisterCallback(MyAddon, "OnProfileChanged")
            MyAddon.db.UnregisterCallback(MyAddon, "OnProfileCopied")
            MyAddon.db.UnregisterCallback(MyAddon, "OnProfileReset")
        end
    end,
}

-- Register.
local zsfCtx = zSettingsFrame.Register(adapter)

-- Slash command.
SLASH_MYADDON1 = "/myaddon"
SlashCmdList["MYADDON"] = function()
    zsfCtx:Open()
end
```

---

## Example: Schema File

A complete schema demonstrating most widget types:

### `MyAddon_Schema.lua`
```lua
MY_ADDON_SCHEMA = {
    -- ── General ──────────────────────────────────────────────────────────
    {
        widgetType = "nav",
        name       = "General",
        children   = {
            { widgetType = "header", name = "Core" },
            {
                widgetType = "toggle",
                key        = "enabled",
                name       = "Enable Addon",
                desc       = "Master toggle for the addon.",
            },
            {
                widgetType = "toggle",
                key        = "showTooltips",
                name       = "Show Tooltips",
                desc       = "Display tooltips on hover.",
                width      = "half",
            },
            {
                widgetType = "toggle",
                key        = "debugMode",
                name       = "Debug Mode",
                width      = "half",
                showWhen   = function() return MyAddonDB.enabled end,
            },

            { widgetType = "header", name = "Sizing" },
            {
                widgetType = "range",
                key        = "iconSize",
                name       = "Icon Size",
                desc       = "Size of icons in pixels.",
                min        = 12,
                max        = 64,
                step       = 1,
            },
            {
                widgetType = "range",
                key        = "bgAlpha",
                name       = "Background Alpha",
                desc       = "Transparency of the background.",
                min        = 0,
                max        = 1,
                step       = 0.05,
                width      = "half",
            },
            {
                widgetType = "range",
                key        = "padding",
                name       = "Padding",
                min        = 0,
                max        = 20,
                step       = 1,
                width      = "half",
            },

            { widgetType = "header", name = "Text" },
            {
                widgetType = "input",
                key        = "greeting",
                name       = "Greeting Message",
                desc       = "Shown when the addon starts.",
                maxLen     = 100,
                placeholder = "Enter a greeting...",
            },
            {
                widgetType = "select",
                key        = "language",
                name       = "Language",
                values     = { en = "English", fr = "French", de = "German" },
                order      = { "en", "fr", "de" },
            },

            { widgetType = "spacer" },

            { widgetType = "header", name = "Colors" },
            {
                widgetType = "color",
                key        = "textColor",
                name       = "Text Color",
                desc       = "Main text color.",
                width      = "half",
            },
            {
                widgetType = "colorAlpha",
                key        = "highlightColor",
                name       = "Highlight Color",
                desc       = "Highlight overlay with alpha.",
                width      = "half",
            },

            { widgetType = "spacer" },
            {
                widgetType = "button",
                name       = "Reset Colors",
                desc       = "Reset all colors to defaults.",
                resetKeys  = { "textColor", "highlightColor" },
            },
        },
    },

    -- ── Tabs example ─────────────────────────────────────────────────────
    {
        widgetType = "nav",
        name       = "Display",
        children   = {
            {
                widgetType = "tab",
                name       = "Layout",
                children   = {
                    { widgetType = "toggle", key = "showHeader",  name = "Show Header" },
                    { widgetType = "toggle", key = "showFooter",  name = "Show Footer" },
                    { widgetType = "range",  key = "columns",     name = "Columns", min = 1, max = 4, step = 1 },
                },
            },
            {
                widgetType = "tab",
                name       = "Fonts",
                children   = {
                    {
                        widgetType = "select",
                        key        = "fontFace",
                        name       = "Font Face",
                        values     = function()
                            return {
                                default  = "Default",
                                frizqt   = "Friz Quadrata",
                                morpheus = "Morpheus",
                            }
                        end,
                        order = { "default", "frizqt", "morpheus" },
                    },
                    {
                        widgetType = "range",
                        key        = "fontSize",
                        name       = "Font Size",
                        min        = 8,
                        max        = 24,
                        step       = 1,
                    },
                },
            },
        },
    },

    -- ── Multi-instance (navChild + trackerKey) ───────────────────────────
    {
        widgetType = "nav",
        name       = "Panels",
        children   = {
            -- Shared settings (nav parent's own content)
            { widgetType = "header", name = "Shared Settings" },
            { widgetType = "toggle", key = "syncPanels", name = "Sync All Panels" },

            -- Per-panel settings (sidebar sub-items)
            {
                widgetType = "navChild",
                name       = "Panel 1",
                trackerID  = 0,
                children   = {
                    { widgetType = "toggle", key = "visible", name = "Visible", trackerKey = true },
                    { widgetType = "range",  key = "scale",   name = "Scale",   trackerKey = true,
                      min = 0.5, max = 2.0, step = 0.1 },
                    { widgetType = "colorAlpha", key = "panelColor", name = "Panel Color",
                      trackerKey = true },
                },
            },
            {
                widgetType = "navChild",
                name       = "Panel 2",
                trackerID  = 1,
                children   = {
                    { widgetType = "toggle", key = "visible", name = "Visible", trackerKey = true },
                    { widgetType = "range",  key = "scale",   name = "Scale",   trackerKey = true,
                      min = 0.5, max = 2.0, step = 0.1 },
                    { widgetType = "colorAlpha", key = "panelColor", name = "Panel Color",
                      trackerKey = true },
                },
            },
        },
    },

    -- ── Advanced ─────────────────────────────────────────────────────────
    {
        widgetType = "nav",
        name       = "Advanced",
        children   = {
            { widgetType = "header", name = "Keybinds" },
            {
                widgetType = "keybind",
                key        = "toggleKey",
                name       = "Toggle Addon",
                desc       = "Keybind to show/hide the addon.",
            },

            { widgetType = "header", name = "Notes" },
            {
                widgetType = "textarea",
                key        = "userNotes",
                name       = "Personal Notes",
                desc       = "Free-form text stored per character.",
                lines      = 6,
                maxLetters = 500,
                placeholder = "Type anything here...",
            },

            { widgetType = "header", name = "Diagnostics" },
            {
                widgetType = "output",
                key        = "diagnosticLog",
                name       = "Diagnostic Output",
                lines      = 4,
            },
        },
    },
}
```

---

## Known Integration Requirements

### External setting changes

If you provide `adapter.subscribe`, zSF handles external state changes automatically — the panel refreshes itself whenever the subscribe callback fires. No manual work needed.

If you do **not** provide `subscribe` and external code writes a setting while the panel may be open (e.g. a legacy AceConfig panel, a profile switch callback, a WoW Settings handler), call:

```lua
zsfCtx:RefreshAll()
```

This re-reads every widget's value from the DB through `adapter.get` and re-evaluates all `showWhen`/`disableWhen` conditions on widgets, nav entries, navChild entries, and tab entries. The panel's `OnShow` calls this automatically, so you only need to call it explicitly when the panel might already be visible at the time of the write.

### Order of operations

1. Define your defaults table.
2. Define your schema table.
3. Call `zSettingsFrame.Register(adapter)` and capture the returned context: `local zsfCtx = zSettingsFrame.Register(adapter)`.
4. Call `zsfCtx:Open()` whenever you want to show the panel.

All of these must happen at **file-load time** — before `ADDON_LOADED` fires. `Register()` internally hooks `PLAYER_LOGIN` to wire into the WoW Settings API.

### What zSF does NOT do

- **It does not manage your saved variables.** You are responsible for initializing, loading, and saving your DB. zSF only reads/writes through your adapter callbacks.
- **It does not depend on any library.** No AceDB, no LSM, no LibStub. If you want LSM integration, you provide the `values()` and `previewFetch()` callbacks yourself (see §LibSharedMedia Integration).
- **It does not create slash commands or minimap buttons.** You wire those yourself and call `zSettingsFrame.Open()`.

---

*zSettingsFrame — schema-driven settings, zero dependencies.*
