Skip to content

Commit 48d723d

Browse files
author
HugoFara
committed
doc(themes): themes are now better organized and better documented (#115).
1 parent 22d5102 commit 48d723d

File tree

16 files changed

+278
-559
lines changed

16 files changed

+278
-559
lines changed

CONTRIBUTING.md

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,54 @@ This is required for building frontend assets, running the dev server, and type
2424

2525
## Create and Edit Themes
2626

27-
Themes are stored at `src/frontend/css/themes/`. If you want to create a new theme, simply add it to a subfolder. You can also edit existing themes.
27+
Themes are stored at `src/frontend/css/themes/`. Each theme is a folder containing CSS overrides and optional assets.
2828

29-
To build themes, run:
29+
### Creating a New Theme
3030

31-
```bash
32-
npm run build:themes
33-
```
31+
1. Create a new folder in `src/frontend/css/themes/` with your theme name (use underscores for spaces, e.g., `My_Theme`)
32+
33+
2. Create a `theme.json` file with metadata:
34+
35+
```json
36+
{
37+
"name": "My Theme",
38+
"description": "A brief description of what this theme changes.",
39+
"mode": "light",
40+
"highlighting": "Description of word highlighting style",
41+
"wordBreaking": "Standard"
42+
}
43+
```
44+
45+
| Field | Description |
46+
|-------|-------------|
47+
| `name` | Display name shown in settings |
48+
| `description` | Explains what the theme changes |
49+
| `mode` | `"light"` or `"dark"` |
50+
| `highlighting` | How words are highlighted (e.g., "Background color", "Underline") |
51+
| `wordBreaking` | Word wrapping behavior (e.g., "Standard", "Modified") |
52+
53+
3. Create a `styles.css` file with your CSS overrides. Key classes to customize:
54+
55+
```css
56+
/* Status colors for word learning stages */
57+
.status0 { /* Unknown words */ }
58+
.status1 { /* Learning stage 1 */ }
59+
.status2 { /* Learning stage 2 */ }
60+
.status3 { /* Learning stage 3 */ }
61+
.status4 { /* Learning stage 4 */ }
62+
.status5 { /* Learned */ }
63+
.status98 { /* Ignored */ }
64+
.status99 { /* Well-known */ }
65+
66+
/* General styling */
67+
body { background-color: #fff; color: #000; }
68+
```
69+
70+
4. Build your theme:
3471

35-
This minifies CSS files and copies assets to `assets/themes/`.
72+
```bash
73+
npm run build:themes
74+
```
3675

3776
### Add Images to your Themes
3877

@@ -41,9 +80,20 @@ You can include images in your theme:
4180
* Use images from `assets/css/images/` with path `../../../assets/css/images/theimage.png`
4281
* Add your own files to your theme folder and reference with `./myimage.png`
4382

44-
### My theme does not contain all the Skinning Files
83+
### Theme Fallback System
84+
85+
When LWT looks for a file in `assets/themes/{{Theme}}/`, it checks if the file exists. If not, it falls back to `assets/css/`. This means your themes **only need to override files you want to change** - you don't need to copy all files from `src/frontend/css/base/`.
86+
87+
### Existing Themes
4588

46-
That's not a problem at all. When LWT looks for a file that should be contained in `assets/themes/{{The Theme}}/`, it checks if the file exists. If not, it falls back to `assets/css/` and tries to get the same file. With this system, your themes **do not need to have all the same files as `src/frontend/css/base/`**.
89+
| Theme | Mode | Description |
90+
|-------|------|-------------|
91+
| Default | Light | Standard theme with background color highlighting |
92+
| Default_Mod | Light | Modified word breaking rules |
93+
| Lingocracy | Light | Subtle underline highlighting |
94+
| Lingocracy_Dark | Dark | Dark version of Lingocracy |
95+
| Night_Mode | Dark | Black background, easy on the eyes |
96+
| White_Night | Dark | Dark theme with white highlighted text |
4797

4898
## Frontend Development (JavaScript/TypeScript)
4999

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "Default Modified",
3+
"description": "Light theme with modified word breaking rules. Punctuation may wrap separately from words.",
4+
"mode": "light",
5+
"highlighting": "Background color highlighting",
6+
"wordBreaking": "Modified - punctuation can break separately"
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "Lingocracy",
3+
"description": "Light theme with subtle underline highlighting. Clean, minimal appearance.",
4+
"mode": "light",
5+
"highlighting": "Discrete underline borders",
6+
"wordBreaking": "Modified - punctuation can break separately"
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "Lingocracy Dark",
3+
"description": "Dark version of Lingocracy with subtle underline highlighting on dark background.",
4+
"mode": "dark",
5+
"highlighting": "Discrete underline borders",
6+
"wordBreaking": "Modified - punctuation can break separately"
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "Night Mode",
3+
"description": "Dark theme with black background and standard highlighting. Easy on the eyes for nighttime reading.",
4+
"mode": "dark",
5+
"highlighting": "Background color highlighting",
6+
"wordBreaking": "Standard"
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "White Night",
3+
"description": "Dark theme where highlighted words remain white. Minimal visual distraction from word colors.",
4+
"mode": "dark",
5+
"highlighting": "White text on dark background",
6+
"wordBreaking": "Standard"
7+
}

assets/themes/chaosarium_light/styles.css

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/backend/Services/ThemeService.php

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,28 +24,92 @@
2424
*/
2525
class ThemeService
2626
{
27+
/**
28+
* Default theme metadata for the base theme.
29+
*/
30+
private const DEFAULT_THEME_METADATA = [
31+
'name' => 'Default',
32+
'description' => 'Standard light theme with background color highlighting.',
33+
'mode' => 'light',
34+
'highlighting' => 'Background color highlighting',
35+
'wordBreaking' => 'Standard'
36+
];
37+
2738
/**
2839
* Get available themes for select dropdown.
2940
*
3041
* Scans the assets/themes directory for available themes.
3142
*
32-
* @return array<int, array{path: string, name: string}> Array of theme data
43+
* @return array<int, array{
44+
* path: string,
45+
* name: string,
46+
* description: string,
47+
* mode: string,
48+
* highlighting: string,
49+
* wordBreaking: string
50+
* }> Array of theme data with metadata
3351
*/
3452
public function getAvailableThemes(): array
3553
{
3654
$themes = [];
3755
$themeDirs = glob('assets/themes/*', GLOB_ONLYDIR) ?: [];
3856

39-
// Add Default first
40-
$themes[] = ['path' => 'assets/themes/Default/', 'name' => 'Default'];
57+
// Add Default first (uses base CSS)
58+
$themes[] = array_merge(
59+
['path' => 'assets/themes/Default/'],
60+
self::DEFAULT_THEME_METADATA
61+
);
4162

4263
foreach ($themeDirs as $theme) {
4364
if ($theme !== 'assets/themes/Default') {
44-
$name = str_replace(['assets/themes/', '_'], ['', ' '], $theme);
45-
$themes[] = ['path' => $theme . '/', 'name' => $name];
65+
$metadata = $this->loadThemeMetadata($theme);
66+
$themes[] = array_merge(['path' => $theme . '/'], $metadata);
4667
}
4768
}
4869

4970
return $themes;
5071
}
72+
73+
/**
74+
* Load theme metadata from theme.json file.
75+
*
76+
* @param string $themePath Path to the theme directory
77+
*
78+
* @return array{
79+
* name: string,
80+
* description: string,
81+
* mode: string,
82+
* highlighting: string,
83+
* wordBreaking: string
84+
* } Theme metadata
85+
*/
86+
private function loadThemeMetadata(string $themePath): array
87+
{
88+
$jsonPath = $themePath . '/theme.json';
89+
$fallbackName = str_replace(['assets/themes/', '_'], ['', ' '], $themePath);
90+
91+
$defaults = [
92+
'name' => $fallbackName,
93+
'description' => '',
94+
'mode' => 'light',
95+
'highlighting' => '',
96+
'wordBreaking' => ''
97+
];
98+
99+
if (!file_exists($jsonPath)) {
100+
return $defaults;
101+
}
102+
103+
$content = file_get_contents($jsonPath);
104+
if ($content === false) {
105+
return $defaults;
106+
}
107+
108+
$metadata = json_decode($content, true);
109+
if (!is_array($metadata)) {
110+
return $defaults;
111+
}
112+
113+
return array_merge($defaults, $metadata);
114+
}
51115
}

src/backend/View/Helper/SelectOptionsBuilder.php

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -388,18 +388,34 @@ public static function forTexts(
388388
/**
389389
* Build theme select options from data array.
390390
*
391-
* @param array<int, array{path: string, name: string}> $themes Theme data from ThemeService
392-
* @param string|null $selected Selected theme path
391+
* @param array<int, array{
392+
* path: string,
393+
* name: string,
394+
* description?: string,
395+
* mode?: string,
396+
* highlighting?: string,
397+
* wordBreaking?: string
398+
* }> $themes Theme data from ThemeService
399+
* @param string|null $selected Selected theme path
393400
*
394401
* @return string HTML options string
395402
*/
396403
public static function forThemes(array $themes, ?string $selected): string
397404
{
398405
$result = '';
399406
foreach ($themes as $theme) {
407+
$modeIndicator = '';
408+
if (isset($theme['mode'])) {
409+
$modeIndicator = $theme['mode'] === 'dark' ? ' [Dark]' : ' [Light]';
410+
}
400411
$result .= '<option value="' . htmlspecialchars($theme['path'], ENT_QUOTES, 'UTF-8') . '"'
401412
. FormHelper::getSelected($selected, $theme['path'])
402-
. '>' . htmlspecialchars($theme['name'], ENT_QUOTES, 'UTF-8') . '</option>';
413+
. ' data-description="' . htmlspecialchars($theme['description'] ?? '', ENT_QUOTES, 'UTF-8') . '"'
414+
. ' data-mode="' . htmlspecialchars($theme['mode'] ?? 'light', ENT_QUOTES, 'UTF-8') . '"'
415+
. ' data-highlighting="' . htmlspecialchars($theme['highlighting'] ?? '', ENT_QUOTES, 'UTF-8') . '"'
416+
. ' data-word-breaking="' . htmlspecialchars($theme['wordBreaking'] ?? '', ENT_QUOTES, 'UTF-8') . '"'
417+
. '>' . htmlspecialchars($theme['name'], ENT_QUOTES, 'UTF-8')
418+
. htmlspecialchars($modeIndicator, ENT_QUOTES, 'UTF-8') . '</option>';
403419
}
404420
return $result;
405421
}

0 commit comments

Comments
 (0)