09 — Theme tokens#
The theme is a small set of named values. Blocks and presets reference token names, never raw
colours or sizes. One row (theme_settings.tokens) therefore controls the look of the whole site,
and a non-technical owner cannot produce an unreadable page by choosing a colour.
9.1 Token document#
{
"version": 1,
"color": {
"primary": "#1d4ed8",
"primary_contrast": "#ffffff",
"secondary": "#0f766e",
"accent": "#f59e0b",
"text": "#111827",
"text_muted": "#4b5563",
"heading": "#0b1220",
"surface": "#ffffff",
"surface_alt": "#f3f4f6",
"surface_dark": "#0b1220",
"border": "#e5e7eb",
"success": "#15803d",
"warning": "#b45309",
"danger": "#b91c1c"
},
"font": {
"body": { "family": "Noto Sans Bengali", "weights": [400, 500, 700] },
"heading": { "family": "Noto Serif Bengali", "weights": [600, 700] },
"body_latin": { "family": "Inter", "weights": [400, 500, 700] },
"base_size": 16,
"scale": 1.25,
"line_height": 1.7,
"heading_line_height": 1.25,
"letter_spacing": 0
},
"space": { "unit": 4, "section_small": 32, "section_medium": 64, "section_large": 96, "section_xlarge": 128, "gap_sm": 8, "gap_md": 16, "gap_lg": 32 },
"radius": { "none": 0, "sm": 4, "md": 8, "lg": 16, "full": 999, "default": "md" },
"button": { "style": "solid", "radius": "md", "size": "md", "weight": 600, "uppercase": false, "shadow": "none" },
"layout": { "container": 1200, "wide": 1440, "content": 760, "breakpoint_tablet": 1024, "breakpoint_mobile": 768 },
"shadow": { "sm": "0 1px 2px rgba(0,0,0,.06)", "md": "0 4px 12px rgba(0,0,0,.08)", "lg": "0 12px 32px rgba(0,0,0,.12)" },
"image": { "default_ratio": "16/9", "rounded": true }
}
Only token names appear in block data. A section with background: "primary" means
var(--kp-color-primary); it does not store #1d4ed8. Re-theming is therefore instant and total.
9.2 Design screen#
Design -> Theme is organised the way an owner thinks, not the way the JSON is shaped:
| Group | Controls |
|---|---|
| Presets | 4 ready themes; picking one replaces all tokens after a confirm |
| Colours | primary, secondary, accent, then text/surface/border under More colours; each swatch shows a live contrast badge |
| Typography | body font, heading font, base size slider, scale, line height; Bengali fonts listed first with a Bengali sample |
| Buttons | style (solid / outline / soft / link), radius, size, weight, uppercase |
| Spacing | section padding scale (compact / normal / roomy) mapping to the space tokens |
| Layout | container width, content width, breakpoints (Advanced) |
| Custom CSS | Admin only, under Advanced, with a warning that it is not covered by support |
Every change previews live against a sample page in a split view, and Save is explicit. A
Discard changes button restores the saved tokens.
Contrast guard#
When a chosen colour pair falls below WCAG AA (4.5:1 for body text, 3:1 for large text and UI), the
field shows a warning and suggests the nearest passing shade with a one-click Use this button. It
warns; it does not block — an owner may have a brand requirement. The warning is recorded so
Dashboard -> Needs attention can list accessibility issues.
9.3 Shipped presets#
| Preset | Character | Primary | Heading font |
|---|---|---|---|
default |
neutral, corporate, blue | #1d4ed8 |
Noto Serif Bengali |
warm |
editorial, news-like, amber on cream | #b45309 |
Noto Serif Bengali |
fresh |
clinic / education, green, rounded | #15803d |
Noto Sans Bengali |
dark |
dark surfaces, high contrast, tech | #38bdf8 |
Inter + Noto Sans Bengali |
Each preset ships with matching button, radius and spacing choices, so switching preset is a real visual change rather than a palette swap. Presets are stored as seeder data, not as rows a user can corrupt.
9.4 Compilation to CSS#
TokenCompiler turns the token document into one stylesheet:
theme_settings.tokens
-> render resources/css/theme.css.blade.php with the tokens
-> minify
-> md5 -> theme_settings.css_hash
-> write storage/app/public/theme/theme-{hash}.css
-> pages link /storage/theme/theme-{hash}.css
- The file name carries the hash, so it is served with
Cache-Control: public, max-age=31536000, immutableand a token change busts it instantly. No query strings, no stale CSS. - The compiler runs on save, and on
kodepress:install. If the file is missing on a public request (fresh deploy, cleared storage), it is compiled on the fly once and written. - Old
theme-*.cssfiles are pruned by the nightly command, keeping the last three so a mid-deploy request never 404s.
Generated structure:
:root{
--kp-color-primary:#1d4ed8; --kp-color-primary-contrast:#fff; /* ... */
--kp-font-body:"Noto Sans Bengali",system-ui,sans-serif;
--kp-space-section-md:64px; --kp-radius-md:8px; --kp-container:1200px;
}
/* element defaults */
body{font-family:var(--kp-font-body);color:var(--kp-color-text);line-height:var(--kp-font-line-height)}
h1,h2,h3,h4{font-family:var(--kp-font-heading);color:var(--kp-color-heading)}
/* layout primitives */
.kp-container{max-width:var(--kp-container);margin-inline:auto;padding-inline:16px}
.kp-section{padding-block:var(--kp-space-section-md)}
.kp-grid{display:grid;grid-template-columns:repeat(12,1fr);gap:var(--kp-gap-md)}
.kp-col-7{grid-column:span 7}
/* component classes used by block views */
.kp-btn{...}.kp-btn--primary{...}.kp-heading{...}.kp-card{...}
/* responsive */
@media (max-width:1024px){ /* tablet spans */ }
@media (max-width:768px){ .kp-grid>*{grid-column:1/-1} /* stacking */ }
/* appended: block.css files of blocks in use, and custom_css last */
Utility classes are hand-written in theme.css.blade.php, not generated from a utility framework.
There is no Tailwind in the public output: the public CSS must be small, stable and fully determined
by tokens. The admin UI does use a compiled utility stylesheet, built by Vite and committed.
Budget: the generated public stylesheet stays under 40 KB uncompressed. A test asserts it.
9.5 Fonts#
- Fonts are self-hosted in
resources/fontsand served from/storage/fonts— no Google Fonts request at runtime (privacy, and reliability on networks where it is slow). - Bundled: Noto Sans Bengali, Noto Serif Bengali, Inter. Weights are subset to those declared in the tokens; the installer notes which files are present.
@font-faceusesfont-display: swap,unicode-rangesplitting Bengali from Latin, and<link rel="preload">for the body font only.- Bengali rendering requires the full glyph set including conjuncts; subsetting by Unicode range is safe, subsetting by observed characters is not, and the pipeline never does the latter.
- An owner may upload a custom font (woff2 only, Admin only); it is validated as a real font file and added to the family list.
9.6 Dark mode#
Out of scope as a visitor toggle in Phase 1–4. The dark preset is a dark design, not a mode. The
token document is shaped so a future tokens.dark block can be added and emitted under
@media (prefers-color-scheme: dark) without touching block views — because every block view reads
var(--kp-*) and nothing else. This is the reason block views are forbidden from inline colours.
9.7 Tests for this document#
| Test | Asserts |
|---|---|
token_change_regenerates_css_with_new_hash |
and the page links the new file |
token_change_clears_all_page_cache |
both of two cached pages are gone |
preset_switch_replaces_tokens |
and keeps page content untouched |
generated_css_under_budget |
< 40 KB uncompressed for the default preset |
block_views_contain_no_hex_colours |
a grep-style test over app/Blocks/*/view.blade.php |
contrast_warning_appears_for_low_contrast_pair |
warning, not a block |
missing_css_file_is_recompiled_on_request |
one compile, then served |
bengali_font_face_has_unicode_range |
and font-display: swap |
KodePress documentation · generated from the Markdown sources by
tools/build-docs-site.py · internal preview, not indexed.