# Making a ZnoteX theme A theme is one folder in `layouts/`. It holds **HTML, CSS, JS and images only** — never database queries, never business logic. The root pages keep doing the work; your theme decides how the result looks. Nothing outside your folder needs to change. You never touch `engine/`, the root `.php` files, or `config.php`. --- ## Quick start 1. Copy `layouts/_example/` and rename it, e.g. `layouts/oldhell/`. Folder names may only contain `a-z`, `0-9`, `-` and `_`. 2. Edit `theme.json`. 3. Open the admin panel → **Layout** → **Activate**. That is the whole install. Your theme appears in the panel the moment the folder exists; there is no registry to edit. Declare compatibility independently from the theme's own version: ```json { "name": "My Theme", "version": "1.0.0", "requires": { "znotex": ">=2.0.0 <3.0.0", "php": ">=8.1", "api": "^1.0" } } ``` An incompatible theme remains installed and visible in the panel, but ZnoteX will not activate it. The theme version does not need to match a plugin version. --- ## What a theme folder can contain ``` layouts/yourtheme/ theme.json name, author, version, description, update [recommended] screenshot.png thumbnail in the admin panel [optional] shells/ default.php the page frame [REQUIRED] wide.php any other frame you want [optional] views/ index.php the middle block of index.php [optional] highscores.php the middle block of highscores.php [optional] pages/ wiki.php a page YOUR theme adds to the site [optional] assets/ css/style.css your stylesheet js/theme.js your scripts img/... your images menu.php only if your shell calls theme_menu() [optional] aside.php only if your shell calls theme_sidebar() [optional] widgets/ only if your shell calls widget() [optional] ``` **`shells/default.php` is the only required file.** Everything else falls back to `layouts/default/`. A theme made of one shell and one stylesheet already redresses the entire site. --- ## The shell The frame every page renders inside. Plain HTML plus a few one-liners: ```php <?= theme_title() ?>
``` `theme_content()` is the only line you cannot remove — it is where the page goes. Everything else is yours to move, delete or rewrite. ### Functions available in a shell, view or page | Call | Does | | --- | --- | | `theme_content()` | prints the page body — **required, once, in the shell** | | `theme_title()` | site title from `config.php`, already escaped | | `theme_body_class()` | `"theme-yourtheme page_highscores"` | | `theme_asset('css/style.css')` | URL of a file in your `assets/` | | `theme_menu()` | includes your `menu.php` | | `theme_sidebar()` | includes your `aside.php` | | `widget('login')` | includes one file from your `widgets/` | | `theme_shell('wide')` | render this page in `shells/wide.php` instead | | `$config` | everything from `config.php` | | `user_logged_in()`, `is_admin($user_data)` | session state | For stable access to configuration, settings, cache, database and hooks, use `$api = znote_theme_api()`. The same object exposes `asset()`, `themeOption()`, `apiVersion()` and `znoteVersion()`. Write your menu directly in the shell if you prefer — `theme_menu()` exists only if you want it. Nothing is imposed. --- ## Child themes — building on another theme Name a parent in `theme.json` and your theme only has to ship what it changes: ```json { "name": "Exodus Dark", "parent": "default" } ``` Files are then looked up **child → parent → default**. A stylesheet and two views on top of a full parent is a complete, working theme — and a fix in the parent reaches every child without touching them. `layouts/_childexample/` is exactly that: one stylesheet, nothing else. Parents can themselves have parents, up to 8 levels. A cycle or a missing parent is ignored rather than fatal — the chain just falls through to `default`, so a typo degrades the look instead of taking the site down. ### One rule that matters Inside a shell, use **`theme_include()`**, never `theme_path()`: ```php // correct // breaks children ``` `theme_path()` points at the active theme only. A child that does not ship `parts/head.php` would include nothing and render without its frame. `theme_include()` walks the chain. Pass variables as a second argument, since an include from inside a function cannot see the caller's locals: ```php $title]); ?> ``` --- ## Views — restyling an existing page A view is the middle block of one root page. The page's logic has already run, so every variable it prepared is yours to use. `layouts/yourtheme/views/highscores.php`: ```php

Ranking for

#NameLevel
``` **Only write the views you actually want to change.** Any page without a view of its own uses the default theme's markup, inside your shell, styled by your CSS. To find out which variables a page gives you, open the root file — e.g. `highscores.php` — and read the logic above `view('highscores')`. --- ## Pages — adding pages of your own Drop a file in `pages/` and it is live. No registration. `layouts/yourtheme/pages/wiki.php` → **`page.php?p=wiki`** ```php

Wiki

Anything you want.

``` It renders inside your shell like every other page, and gets the body class `page_wiki` so you can target it from CSS. Pretty URLs, if you want them, in `.htaccess`: ```apache RewriteRule ^([a-z0-9_-]+)\.html$ page.php?p=$1 [L,QSA] ``` --- ## Several frames in one theme Some pages need a different structure — a landing page with no sidebar, a full-width page. Add another shell and ask for it from the view or page: ```php
...
``` `shells/wide.php` is a complete frame, just like `default.php`. --- ## The CSS contract This is the part people miss. The root pages emit some markup themselves, with class names your theme does not control. **Style these or those pages render unstyled.** The full list: | Class | Where | | --- | --- | | `table`, `table-striped`, `table-hover`, `tbl-hover` | every listing page | | `tr.yellow` | table header rows — Znote does not use `` | | `znoteTable`, `ThreadTable` | forum and helpdesk | | `btn`, `btn-primary`, `btn-success`, `btn-warning`, `btn-danger`, `btn-info` | every form | | `form-control` | inputs | | `special` | highlighted rows | | `txt`, `zheadline`, `bighr` | text helpers | | `outfitColumn` | outfit images in listings | | `span12`, `show`, `wtf`, `nav_link` | odds and ends | `layouts/_example/assets/css/style.css` styles all of them and is annotated — copy that section as your starting point. If your shell calls `theme_sidebar()` or `widget()`, you also need `.well`, `.widget` and `.header`. --- ## Rules - **No logic in a theme.** No `mysql_insert`, no `UPDATE`. Reading data for a page of your own is fine; that is what `pages/` is for. - **Escape anything from the database**: `htmlspecialchars($x, ENT_QUOTES, 'UTF-8')`. - **Never edit `layouts/_example/`.** It is the reference every theme is copied from. Copy it, do not modify it. - **Never edit `layouts/default/`** either, unless you mean to change the fallback for every theme on the site. - **Reference your files through `theme_asset()`**, not with a hardcoded path. It keeps working when your folder is renamed, and falls back to the default theme when a file is missing. Shared vendor files that are not part of any theme live in `assets/` (`assets/fontawesome/`, `assets/js/jquery.js`). The admin panel uses them too, which is why they are not inside a theme. --- ## Troubleshooting **The site is unstyled.** Your shell is probably not loading your CSS. Check `theme_asset('css/style.css')` and that the file is at `layouts/yourtheme/assets/css/style.css`. **A page is blank.** Look in the PHP error log for `[ZnoteX theme]`. A view that fails to resolve is logged there. **A page renders but has no frame.** `shells/default.php` is missing or has a parse error. The admin panel refuses to activate a theme without it, but it can break after activation. **My change does nothing.** Confirm which theme is active: admin panel → Layout. The active one is marked. The setting lives in the `znote_config` table, not in `config.php`. **The admin panel looks unchanged.** That is intentional. The panel has its own styling in `admin/assets/` and is identical for every theme.