# Theme Settings

https://dev.wisecp.com/es/theme-settings

Declare the settings a theme accepts in its manifest. The admin form, the validation and the saved file follow. The view reads the result as one variable.

## Overview

A theme is data, not a class: it declares **what** it can be configured with, never **how** that is edited. The panel validates each field by its declared type and writes the values into the theme's own directory.

That is the boundary of the contract. A theme adds a field and gets a control, but does not decide the form's plumbing. A value outside the schema is never stored.

## Structure

### Two Files, Two Owners

- **theme.php**: Schema, defaults, and the theme's name, version and engine. Shipped with the theme, never written at runtime.
- **config.php**: The saved values only: a flat map written by the panel, absent until the first save and kept out of version control.
- **Reading**: The saved value if there is one, otherwise the schema default.
- **The form**: Generated from the schema. A theme wanting more ships its own admin markup, and is handed the builder and the saved values.

### The Shape of the Schema

```php
$manifest['settings'] = [
    // key => the group's heading and icon. The label is a LOCALE KEY of this theme.
    'groups' => [
        'topbar' => ['label' => 'grp_topbar', 'icon' => 'bi-megaphone'],
    ],
    // key => a descriptor. The key is the POST name, the config key and the name the
    // view reads, all at once: renaming it orphans whatever was already saved.
    'fields' => [
        'topbar_enabled' => ['type' => 'switch', 'group' => 'topbar', 'label' => 'set_topbar_enabled', 'default' => false],
    ],
];
```

## Step by Step

### Declare a Group

1. Add an entry to the groups map with a heading and an icon class.
2. Put the heading in the theme's own locale file, both languages, and reference it by key. An untranslated key prints as itself.
3. A field whose group is not declared still appears, ungrouped.

### Declare a Field

1. Pick the type from the list below. It decides the control, the validation and what is stored.
2. Give it a real default, not an empty placeholder. That is the value on a fresh install and after every reset.
3. Add `desc` for a help sentence, and `depends` when the field only makes sense while another is on.
4. Reload the configure page; the control is there, no other file touched.

### Read It in a View

1. Read it off the settings variable by key. It is a variable and not a function so that it works inside a condition.
2. For a colour, read the property value and its `_rgb` twin wherever a translucent version is needed.
3. From PHP, ask the theme object for one setting by key, without loading the whole map.

### Take Over the Admin Form

1. Add an `admin-settings.php` to the theme directory; it replaces the generated form entirely.
2. Build the markup with the form builder, saved values and schema you are handed. Return the HTML as a string.
3. Keep field names identical to the schema keys. A control named anything else is posted and discarded.

## Reference

### Field Types

| Type | Control | Stored as | Validation on save |
| --- | --- | --- | --- |
| `switch`, `checkbox` | Checkbox; `desc` is the label beside it | Boolean | Set means true, absent means false |
| `select` | Dropdown built from `options` | The chosen key | Must be a key of `options`, else the default |
| `number` | Number input | Integer | Cast to integer; non-numeric becomes zero |
| `color` | Colour picker plus a typeable hex box | Hex string with the leading hash | Three to eight hex digits, else the default |
| `textarea` | Textarea sized by `rows` | String, or a language map when multilingual | Tags stripped unless `html` is on, then allowlist-filtered |
| anything else, or a missing type | Text input | String, or a language map when multilingual | Same as a textarea |

### What a Descriptor Accepts

- **type**: One of the types above; absent means a text field.
- **group**: The group this field belongs to. An undeclared group leaves it ungrouped.
- **label**: A locale key of this theme, not literal text. Resolved in the operator's language, then English, then the key.
- **desc**: A locale key for the help line. On a checkbox it becomes the label beside the box instead.
- **placeholder**: A locale key for the hint inside a text control. Not a label; the field still needs one.
- **default**: The value until the operator saves one, and the fallback when a posted value fails validation. An empty colour makes the derived rgb twin three zeroes.
- **options**: Select only, as stored value to label key: `['rail' => 'opt_rail', 'card' => 'opt_card']`. Also the allowlist the saved value is checked against.
- **depends**: A map of another field to the value it must hold: `['topbar_enabled' => true]`. The row collapses until every condition matches, on load and as the operator types.
- **multilang**: Text and textarea only. One tab per active language, stored as a language map. Views receive a plain string.
- **rows**: Height of a textarea, defaulting to four. Ignored by every other type.
- **html · allowed_tags**: Keeps markup in a text value instead of stripping it. The allowlist is narrowable per field; the default covers basic formatting and links.
- **from_logo**: Colour only. Marks the field as a brand colour and gives its palette index. The "pick colours from the logo" button fills it from the site logo.

### The Settings API

```php
// The manifest's settings block, exactly as declared (groups + fields).
public function settingsSchema(): array;

// One value: the saved one if the key was ever saved, otherwise the schema default,
// otherwise null. This is how the platform reads a single theme capability.
public function setting(string $key): mixed;

// The saved values only (config.php). Empty until the first save.
public function savedConfig(): array;

// Every schema field merged with its value : what the view receives as $setting.
// A multilingual value is reduced to the ACTIVE language here, and a colour field
// also produces a "{key}_rgb" entry.
public function allSettings(): array;

// A hex colour to the comma-separated triplet an rgba() expression needs. Short forms
// are expanded and a short value is padded, so an empty string yields three zeroes
// rather than an error. This is what produces the "{key}_rgb" entries above.
public static function hexToRgb(string $hex): string;
```

## Example

A group with four fields, the saved file it produces and the view that reads it back. One reason to show them together: the schema key, the file key and the view name are the same string.

```php
return [
    'engine' => 'smarty',
    'status' => 'ready',
    'meta'   => [
        'name'    => 'Acme',
        'version' => '1.0.0',
        'author'  => 'Acme Ltd',
        'image'   => 'cover.png',
    ],
    'settings' => [
        'groups' => [
            'brand'    => ['label' => 'grp_brand', 'icon' => 'bi-palette'],
            'topbar'   => ['label' => 'grp_topbar', 'icon' => 'bi-megaphone'],
            'checkout' => ['label' => 'grp_checkout', 'icon' => 'bi-cart3'],
        ],
        'fields' => [
            // A brand colour. `from_logo` is its index in the palette the panel extracts
            // from the site logo, so the "pick colours from the logo" button fills it.
            // The default is a hash plus six hex digits (the picker's own format) —
            // replace the placeholder with this theme's real colour. Leave it empty and
            // the derived primary_color_rgb reads as three zeroes, which paints every
            // translucent surface built from it black.
            'primary_color' => [
                'type'      => 'color',
                'group'     => 'brand',
                'label'     => 'set_primary_color',
                'from_logo' => 0,
                'default'   => '#RRGGBB',
            ],

            // The gate. Everything below it depends on this one.
            'topbar_enabled' => [
                'type'    => 'switch',
                'group'   => 'topbar',
                'label'   => 'set_topbar_enabled',
                'desc'    => 'set_topbar_enabled_desc',
                'default' => false,
            ],

            // Per-language, and allowed to carry markup: an announcement usually has a
            // link in it. The stored value is a map; the view still gets a string.
            'topbar_text' => [
                'type'      => 'textarea',
                'group'     => 'topbar',
                'label'     => 'set_topbar_text',
                'default'   => '',
                'rows'      => 3,
                'multilang' => true,
                'html'      => true,
                'depends'   => ['topbar_enabled' => true],
            ],

            // The keys are what gets stored; the values are locale keys of this theme.
            'checkout_sidebar' => [
                'type'    => 'select',
                'group'   => 'checkout',
                'label'   => 'set_checkout_sidebar',
                'options' => [
                    'rail'  => 'opt_checkout_rail',
                    'card'  => 'opt_checkout_card',
                    'stack' => 'opt_checkout_stack',
                ],
                'default' => 'rail',
            ],

            'popup_width' => [
                'type'    => 'number',
                'group'   => 'checkout',
                'label'   => 'set_popup_width',
                'default' => 500,
            ],
        ],
    ],
];
```

```php
return [
    // Stored with the leading hash, exactly as the picker posted it. Note that EVERY
    // schema field is written on every save, whether the operator touched it or not.
    'primary_color'    => '#RRGGBB',
    'topbar_enabled'   => true,
    // Multilingual: stored per language, one entry per ACTIVE language at save time.
    'topbar_text'      => [
        'en' => 'Free migration this month.',
        'tr' => 'Bu ay ücretsiz taşıma.',
    ],
    'checkout_sidebar' => 'card',
    'popup_width'      => 520,
];
```

```smarty
{* A boolean reads directly inside a condition, which is why this is a VARIABLE and
   not a function: a registered function cannot be called from {if}. *}
{if $setting.topbar_enabled}
    {* Markup was kept on save, so it is printed unfiltered here. *}
    <div class="topbar">{$setting.topbar_text nofilter}</div>
{/if}

{* A select is just its stored key: use it as a class rather than branching four times. *}
<div class="checkout-layout-{$setting.checkout_sidebar}">

{* A colour field also produces an _rgb twin, for a translucent version of the same colour. *}
<style>
    :root {
        --acme-brand: {$setting.primary_color};
        --acme-brand-rgb: {$setting.primary_color_rgb};
    }
</style>
```

## Pitfalls

> **Renaming a key orphans what was already saved**
> 
> The key is the form name, the stored key and the name the view reads. A value under the old name is never read again and never cleaned up. Every installation that had configured the theme falls back to the default.

> **The saved file is generated, not source**
> 
> It holds values and nothing else. Name and version stay in the manifest, where the theme listing and the upgrade check read them. Shipping one in a theme package hands your test configuration to everyone who installs it.

> **A multilingual value is a map in the file and a string in the view**
> 
> PHP asking for the raw setting gets the whole map. The view gets the active language resolved. Code confusing the two works in exactly one place.

> **A collapsed dependent field is still saved and still read**
> 
> The dependency hides the row; it does not disable the value. A view reading a dependent setting must check the field it depends on too. Otherwise it shows something the operator believes is off.

## Related Articles

- [The Theme Engine](https://dev.wisecp.com/en/the-theme-engine)
- [Theme Anatomy](https://dev.wisecp.com/en/theme-anatomy)
- [Template Variables](https://dev.wisecp.com/en/template-variables)
- [Translating a Theme](https://dev.wisecp.com/en/translating-a-theme)
- [The Admin Form Builder](https://dev.wisecp.com/en/the-admin-form-builder)
