1# Making a ZnoteX theme
2
3A theme is one folder in `layouts/`. It holds **HTML, CSS, JS and images only** —
4never database queries, never business logic. The root pages keep doing the work;
5your theme decides how the result looks.
6
7Nothing outside your folder needs to change. You never touch `engine/`,
8the root `.php` files, or `config.php`.
9
10---
11
12## Quick start
13
141. Copy `layouts/_example/` and rename it, e.g. `layouts/oldhell/`.
15 Folder names may only contain `a-z`, `0-9`, `-` and `_`.
162. Edit `theme.json`.
173. Open the admin panel → **Layout** → **Activate**.
18
19That is the whole install. Your theme appears in the panel the moment the folder
20exists; there is no registry to edit.
21
22Declare compatibility independently from the theme's own version:
23
24```json
25{
26 "name": "My Theme",
27 "version": "1.0.0",
28 "requires": {
29 "znotex": ">=2.0.0 <3.0.0",
30 "php": ">=8.1",
31 "api": "^1.0"
32 }
33}
34```
35
36An incompatible theme remains installed and visible in the panel, but ZnoteX
37will not activate it. The theme version does not need to match a plugin version.
38
39---
40
41## What a theme folder can contain
42
43```
44layouts/yourtheme/
45 theme.json name, author, version, description, update [recommended]
46 screenshot.png thumbnail in the admin panel [optional]
47
48 shells/
49 default.php the page frame [REQUIRED]
50 wide.php any other frame you want [optional]
51
52 views/
53 index.php the middle block of index.php [optional]
54 highscores.php the middle block of highscores.php [optional]
55
56 pages/
57 wiki.php a page YOUR theme adds to the site [optional]
58
59 assets/
60 css/style.css your stylesheet
61 js/theme.js your scripts
62 img/... your images
63
64 menu.php only if your shell calls theme_menu() [optional]
65 aside.php only if your shell calls theme_sidebar() [optional]
66 widgets/ only if your shell calls widget() [optional]
67```
68
69**`shells/default.php` is the only required file.** Everything else falls back
70to `layouts/default/`. A theme made of one shell and one stylesheet already
71redresses the entire site.
72
73---
74
75## The shell
76
77The frame every page renders inside. Plain HTML plus a few one-liners:
78
79```php
80<!DOCTYPE html>
81<html lang="en">
82<head>
83 <meta charset="utf-8">
84 <title><?= theme_title() ?></title>
85 <link rel="stylesheet" href="<?= theme_asset('css/style.css') ?>">
86</head>
87<body class="<?= theme_body_class() ?>">
88
89 <header class="my-header">
90 <nav>
91 <a href="index.php">Home</a>
92 <a href="highscores.php">Highscores</a>
93 </nav>
94 </header>
95
96 <main>
97 <?php theme_content(); ?>
98 </main>
99
100 <footer>© <?= theme_title() ?></footer>
101</body>
102</html>
103```
104
105`theme_content()` is the only line you cannot remove — it is where the page goes.
106Everything else is yours to move, delete or rewrite.
107
108### Functions available in a shell, view or page
109
110| Call | Does |
111| --- | --- |
112| `theme_content()` | prints the page body — **required, once, in the shell** |
113| `theme_title()` | site title from `config.php`, already escaped |
114| `theme_body_class()` | `"theme-yourtheme page_highscores"` |
115| `theme_asset('css/style.css')` | URL of a file in your `assets/` |
116| `theme_menu()` | includes your `menu.php` |
117| `theme_sidebar()` | includes your `aside.php` |
118| `widget('login')` | includes one file from your `widgets/` |
119| `theme_shell('wide')` | render this page in `shells/wide.php` instead |
120| `$config` | everything from `config.php` |
121| `user_logged_in()`, `is_admin($user_data)` | session state |
122
123For stable access to configuration, settings, cache, database and hooks, use
124`$api = znote_theme_api()`. The same object exposes `asset()`,
125`themeOption()`, `apiVersion()` and `znoteVersion()`.
126
127Write your menu directly in the shell if you prefer — `theme_menu()` exists only
128if you want it. Nothing is imposed.
129
130---
131
132## Child themes — building on another theme
133
134Name a parent in `theme.json` and your theme only has to ship what it changes:
135
136```json
137{
138 "name": "Exodus Dark",
139 "parent": "default"
140}
141```
142
143Files are then looked up **child → parent → default**. A stylesheet and two
144views on top of a full parent is a complete, working theme — and a fix in the
145parent reaches every child without touching them.
146
147`layouts/_childexample/` is exactly that: one stylesheet, nothing else.
148
149Parents can themselves have parents, up to 8 levels. A cycle or a missing
150parent is ignored rather than fatal — the chain just falls through to
151`default`, so a typo degrades the look instead of taking the site down.
152
153### One rule that matters
154
155Inside a shell, use **`theme_include()`**, never `theme_path()`:
156
157```php
158<?php theme_include('parts/head.php'); ?> // correct
159<?php include theme_path() . '/parts/head.php'; ?> // breaks children
160```
161
162`theme_path()` points at the active theme only. A child that does not ship
163`parts/head.php` would include nothing and render without its frame.
164`theme_include()` walks the chain. Pass variables as a second argument, since
165an include from inside a function cannot see the caller's locals:
166
167```php
168<?php theme_include('parts/box.php', ['title' => $title]); ?>
169```
170
171---
172
173## Views — restyling an existing page
174
175A view is the middle block of one root page. The page's logic has already run,
176so every variable it prepared is yours to use.
177
178`layouts/yourtheme/views/highscores.php`:
179
180```php
181<div class="my-panel">
182 <h1>Ranking for <?= skillName($type) ?></h1>
183
184 <table class="table table-striped">
185 <tr class="yellow"><td>#</td><td>Name</td><td>Level</td></tr>
186 <?php foreach ($players as $player): ?>
187 <tr>
188 <td><?= (int)$player['rank'] ?></td>
189 <td><a href="characterprofile.php?name=<?= urlencode($player['name']) ?>">
190 <?= htmlspecialchars($player['name'], ENT_QUOTES, 'UTF-8') ?>
191 </a></td>
192 <td><?= (int)$player['level'] ?></td>
193 </tr>
194 <?php endforeach; ?>
195 </table>
196</div>
197```
198
199**Only write the views you actually want to change.** Any page without a view of
200its own uses the default theme's markup, inside your shell, styled by your CSS.
201
202To find out which variables a page gives you, open the root file — e.g.
203`highscores.php` — and read the logic above `view('highscores')`.
204
205---
206
207## Pages — adding pages of your own
208
209Drop a file in `pages/` and it is live. No registration.
210
211`layouts/yourtheme/pages/wiki.php` → **`page.php?p=wiki`**
212
213```php
214<h1>Wiki</h1>
215<p>Anything you want.</p>
216```
217
218It renders inside your shell like every other page, and gets the body class
219`page_wiki` so you can target it from CSS.
220
221Pretty URLs, if you want them, in `.htaccess`:
222
223```apache
224RewriteRule ^([a-z0-9_-]+)\.html$ page.php?p=$1 [L,QSA]
225```
226
227---
228
229## Several frames in one theme
230
231Some pages need a different structure — a landing page with no sidebar, a
232full-width page. Add another shell and ask for it from the view or page:
233
234```php
235<?php theme_shell('wide'); ?>
236<div class="hero">...</div>
237```
238
239`shells/wide.php` is a complete frame, just like `default.php`.
240
241---
242
243## The CSS contract
244
245This is the part people miss.
246
247The root pages emit some markup themselves, with class names your theme does not
248control. **Style these or those pages render unstyled.** The full list:
249
250| Class | Where |
251| --- | --- |
252| `table`, `table-striped`, `table-hover`, `tbl-hover` | every listing page |
253| `tr.yellow` | table header rows — Znote does not use `<th>` |
254| `znoteTable`, `ThreadTable` | forum and helpdesk |
255| `btn`, `btn-primary`, `btn-success`, `btn-warning`, `btn-danger`, `btn-info` | every form |
256| `form-control` | inputs |
257| `special` | highlighted rows |
258| `txt`, `zheadline`, `bighr` | text helpers |
259| `outfitColumn` | outfit images in listings |
260| `span12`, `show`, `wtf`, `nav_link` | odds and ends |
261
262`layouts/_example/assets/css/style.css` styles all of them and is annotated —
263copy that section as your starting point.
264
265If your shell calls `theme_sidebar()` or `widget()`, you also need `.well`,
266`.widget` and `.header`.
267
268---
269
270## Rules
271
272- **No logic in a theme.** No `mysql_insert`, no `UPDATE`. Reading data for a
273 page of your own is fine; that is what `pages/` is for.
274- **Escape anything from the database**: `htmlspecialchars($x, ENT_QUOTES, 'UTF-8')`.
275- **Never edit `layouts/_example/`.** It is the reference every theme is copied
276 from. Copy it, do not modify it.
277- **Never edit `layouts/default/`** either, unless you mean to change the fallback
278 for every theme on the site.
279- **Reference your files through `theme_asset()`**, not with a hardcoded path.
280 It keeps working when your folder is renamed, and falls back to the default
281 theme when a file is missing.
282
283Shared vendor files that are not part of any theme live in `assets/`
284(`assets/fontawesome/`, `assets/js/jquery.js`). The admin panel uses them too,
285which is why they are not inside a theme.
286
287---
288
289## Troubleshooting
290
291**The site is unstyled.** Your shell is probably not loading your CSS. Check
292`theme_asset('css/style.css')` and that the file is at
293`layouts/yourtheme/assets/css/style.css`.
294
295**A page is blank.** Look in the PHP error log for `[ZnoteX theme]`. A view that
296fails to resolve is logged there.
297
298**A page renders but has no frame.** `shells/default.php` is missing or has a
299parse error. The admin panel refuses to activate a theme without it, but it can
300break after activation.
301
302**My change does nothing.** Confirm which theme is active: admin panel →
303Layout. The active one is marked. The setting lives in the `znote_config`
304table, not in `config.php`.
305
306**The admin panel looks unchanged.** That is intentional. The panel has its own
307styling in `admin/assets/` and is identical for every theme.
308