mirror of
https://github.com/ancsemi/Haven.git
synced 2026-10-27 09:05:01 +00:00
546 lines
20 KiB
Markdown
546 lines
20 KiB
Markdown
# Theme API v1 authoring reference
|
|
|
|
Haven themes are ordinary CSS files served by the Haven server. Theme API v1
|
|
defines the CSS variables and semantic layout regions that theme authors can
|
|
rely on without depending on Haven's internal class names or DOM structure.
|
|
|
|
Theme API v1 is intentionally a CSS-only contract. It does not expose JavaScript
|
|
helpers, events, component internals, or permission APIs. Structural changes
|
|
that require moving elements or creating interactive controls belong in a
|
|
plugin, not a theme.
|
|
|
|
## Theme file format
|
|
|
|
A theme filename must end in `.theme.css` and should begin with a metadata
|
|
comment:
|
|
|
|
```css
|
|
/**
|
|
* @name My Theme
|
|
* @description A short description.
|
|
* @author YourName
|
|
* @version 1.0
|
|
* @icon M
|
|
* @haven-theme-api 1
|
|
*/
|
|
```
|
|
|
|
The theme list reads `@name`, `@description`, `@author`, `@version`, `@icon`,
|
|
`@section`, and `@haven-theme-api`. `@section Seasonal` puts the theme in the
|
|
picker's Seasonal section; any other `@section` name gets a section of its own,
|
|
and a theme without one is listed under Community. The picker's preview card is
|
|
drawn from the file's `--bg-primary`, `--bg-secondary`, `--accent` and
|
|
`--text-primary` values when they are plain colours. Haven checks the API declaration before it injects the
|
|
stylesheet. Older Haven versions ignore metadata they do not recognise.
|
|
Declare each field on its own metadata line. Multiple `@haven-theme-api`
|
|
declarations make the theme invalid.
|
|
|
|
Use a short text character or emoji for `@icon`. Do not put HTML in metadata.
|
|
|
|
## Compatibility
|
|
|
|
Haven classifies each installed theme before loading it:
|
|
|
|
| Status | Meaning | Loaded |
|
|
| --- | --- | --- |
|
|
| Compatible | Declares `@haven-theme-api 1` | Yes |
|
|
| Legacy | Does not declare a Theme API | Yes |
|
|
| Unsupported | Declares another positive integer version | No |
|
|
| Invalid | Declares an empty, fractional, zero, negative, or non-numeric version | No |
|
|
|
|
Legacy themes remain enabled for backward compatibility. Declaring an API is
|
|
recommended because it lets Haven stop a known-incompatible theme before the
|
|
theme can hide or break recovery controls.
|
|
|
|
Incompatible themes remain visible in Plugins & Themes and in the admin theme
|
|
list for diagnosis, but cannot be enabled, newly published, or selected as a
|
|
new server default. If a selected theme disappears or becomes incompatible, Haven
|
|
falls back to its built-in theme and updates the user's saved preference.
|
|
|
|
Haven caches the last server compatibility verdict so a previously verified
|
|
file theme can still load before first paint. A theme with no cached verdict
|
|
waits for `/api/themes` on its first load. The server verdict is refreshed when
|
|
the theme list loads, and the server validates the file again whenever it serves
|
|
the stylesheet. The cache is only a flash-prevention optimisation.
|
|
|
|
## Installing and publishing
|
|
|
|
1. Put the file in the server's `themes/` directory.
|
|
2. Restart Haven or refresh the Plugins & Themes list.
|
|
3. Open the theme picker (the palette button in the sidebar). As an admin you
|
|
see it under Available to publish.
|
|
4. Switch Publish on under its card to add it to every user's theme picker.
|
|
Settings, Admin, Branding, Custom Themes has the same switches.
|
|
|
|
A published file is a selectable base theme. Haven applies the built-in Haven
|
|
theme as the stable layout base, then loads the selected file after the core
|
|
stylesheet. A file that is installed but not published is only for people who
|
|
manage the server: they can try it, or enable it per browser as an additive CSS
|
|
tweak from Plugins & Themes. Members do not see it there or in the picker, and
|
|
the server will not save it as a member's theme. A member who already uses a
|
|
theme when it is unpublished keeps it until they pick another.
|
|
|
|
Themes are server-managed files. Haven does not download themes from arbitrary
|
|
URLs or provide a per-user raw CSS editor.
|
|
|
|
`themes/compact.theme.css` is a shipped Theme API v1 example that uses only
|
|
public tokens, page markers, and layout regions. It deliberately leaves message
|
|
geometry to the user's Layout Density setting; structural compaction belongs in
|
|
a plugin.
|
|
|
|
## Previewing a theme without a server
|
|
|
|
`public/theme-preview.html` is a static copy of the app with sample content:
|
|
a conversation with every kind of markdown, a picture, a poll, reactions and
|
|
a reply, the member list, an open profile card, the Settings modal, and a
|
|
forum channel. It loads Haven's real stylesheet, so a theme file applied to
|
|
it looks the way it will in the app.
|
|
|
|
1. Open `public/theme-preview.html` from a checkout in a browser, or in a
|
|
live editor such as Phoenix Code that previews as you type.
|
|
2. In the toolbar at the top right, type the path to your theme relative to
|
|
the file (`../themes/my.theme.css` for a file in the repository's
|
|
`themes/` folder) and press Apply, or open the page as
|
|
`theme-preview.html?theme=../themes/my.theme.css`.
|
|
3. Chat and Forum switch the view; Settings modal and Profile card show or
|
|
hide those.
|
|
|
|
The page is a snapshot, so a change to the app's markup reaches it the next
|
|
time it is regenerated with `scripts/theme-preview-capture.js`; the file's
|
|
header comment and the script say how. A running server also serves it at
|
|
`/theme-preview.html`.
|
|
|
|
## Scoping a theme
|
|
|
|
Both the login page and the main application expose the API version:
|
|
|
|
```css
|
|
html[data-haven-theme-api="1"] {
|
|
/* Theme API v1 rules */
|
|
}
|
|
```
|
|
|
|
Use the page marker when a rule belongs to only one page:
|
|
|
|
```css
|
|
[data-haven-page="app"] {
|
|
/* Main application only */
|
|
}
|
|
|
|
[data-haven-page="auth"] {
|
|
/* Login and registration only */
|
|
}
|
|
```
|
|
|
|
## Public design tokens
|
|
|
|
Override public tokens on `:root`. All tokens are optional; omitted values come
|
|
from the Haven base theme.
|
|
|
|
### Backgrounds
|
|
|
|
| Token | Purpose |
|
|
| --- | --- |
|
|
| `--bg-primary` | Main content background |
|
|
| `--bg-secondary` | Sidebars and primary panels |
|
|
| `--bg-tertiary` | Nested panels and controls |
|
|
| `--bg-hover` | Hovered controls and rows |
|
|
| `--bg-active` | Selected controls and rows |
|
|
| `--bg-input` | Text inputs and editors |
|
|
| `--bg-card` | Cards, messages, and modal surfaces |
|
|
|
|
### Accent and text
|
|
|
|
| Token | Purpose |
|
|
| --- | --- |
|
|
| `--accent` | Primary accent colour |
|
|
| `--accent-hover` | Hovered accent controls |
|
|
| `--accent-glow` | Accent shadow or glow colour |
|
|
| `--accent-text` | Preferred foreground for core accent controls |
|
|
| `--text-primary` | Primary text |
|
|
| `--text-secondary` | Secondary text and timestamps |
|
|
| `--text-muted` | Hints, placeholders, and quiet labels |
|
|
| `--text-link` | Links |
|
|
|
|
Always check the contrast between `--accent` and `--accent-text`. A light accent
|
|
usually needs dark accent text. A few specialised media controls use a fixed
|
|
foreground for compatibility with existing themes, so verify those too.
|
|
|
|
### Borders and semantic states
|
|
|
|
| Token | Purpose |
|
|
| --- | --- |
|
|
| `--border` | Default border |
|
|
| `--border-light` | Stronger or raised border |
|
|
| `--success` | Positive and connected states |
|
|
| `--success-text` | Foreground on success-filled controls |
|
|
| `--danger` | Destructive and error states |
|
|
| `--danger-text` | Foreground on danger-filled controls |
|
|
| `--warning` | Warning states |
|
|
| `--warning-text` | Foreground on warning-filled controls |
|
|
| `--led-on` | Connected presence indicator |
|
|
| `--led-off` | Disconnected presence indicator |
|
|
| `--led-glow` | Connected indicator glow |
|
|
|
|
Semantic colours can also appear as text or icons on Haven surfaces. Check each
|
|
state colour against your backgrounds as well as against its paired foreground.
|
|
|
|
### Typography and geometry
|
|
|
|
| Token | Purpose |
|
|
| --- | --- |
|
|
| `--font-main` | Main interface font stack |
|
|
| `--font-mono` | Codes and technical values |
|
|
| `--font-heading` | Headings and display labels |
|
|
| `--radius` | Default radius |
|
|
| `--radius-sm` | Compact control radius |
|
|
| `--transition` | Standard transition timing |
|
|
| `--sidebar-width` | Navigation sidebar width |
|
|
| `--right-width` | Context sidebar width |
|
|
|
|
Use `rem` for dimensions. Haven's interface zoom changes the root font size, so
|
|
pixel-based shell dimensions do not scale with the rest of the interface.
|
|
|
|
### Decorative tokens
|
|
|
|
| Token | Purpose |
|
|
| --- | --- |
|
|
| `--msg-glow` | Optional message hover glow |
|
|
| `--scanline` | Optional scanline overlay value |
|
|
|
|
### Internal values
|
|
|
|
Message geometry properties such as `--msg-pad-x`, `--msg-pad-y`,
|
|
`--msg-avatar`, `--msg-gap`, and `--msg-gutter` are not part of Theme API v1.
|
|
Haven changes them for density preferences and responsive breakpoints, and file
|
|
themes load after those core rules. Treating them as unconditional theme tokens
|
|
would override the user's density and mobile geometry. They may be formalised in
|
|
a later API after the related state has a stable pre-paint contract.
|
|
|
|
## Public layout regions
|
|
|
|
Select major areas through `data-haven-region`:
|
|
|
|
```css
|
|
[data-haven-region="navigation-sidebar"] {
|
|
background: var(--bg-secondary);
|
|
}
|
|
```
|
|
|
|
### Application regions
|
|
|
|
| Region | Purpose |
|
|
| --- | --- |
|
|
| `app-shell` | Primary application shell, including the status bar |
|
|
| `workspace` | Main multi-column workspace |
|
|
| `server-rail` | Server navigation rail |
|
|
| `server-list` | Dynamic server list inside the server rail |
|
|
| `navigation-sidebar` | Channels and direct-message sidebar |
|
|
| `account` | Current account identity and account actions |
|
|
| `sidebar-content` | Reorderable sidebar content |
|
|
| `join-channel` | Join-channel section |
|
|
| `create-channel` | Create-channel section |
|
|
| `channels` | Channel list section |
|
|
| `direct-messages` | Direct-message list section |
|
|
| `sidebar-footer` | Pinned sidebar footer and controls |
|
|
| `sidebar-actions` | Persistent sidebar action buttons |
|
|
| `theme-picker` | Theme selector |
|
|
| `main` | Main content column |
|
|
| `channel-header` | Active channel header |
|
|
| `welcome` | No-channel welcome state |
|
|
| `message-area` | Active chat area |
|
|
| `webcams` | Webcam viewer |
|
|
| `screen-shares` | Screen-share viewer |
|
|
| `music-player` | Listen Together player |
|
|
| `pinned-messages` | Docked pinned-message panel |
|
|
| `message-list` | Scrollable message list |
|
|
| `composer` | Message composer |
|
|
| `soundboard` | Docked soundboard panel |
|
|
| `context-sidebar` | Voice and member context sidebar |
|
|
| `search-results` | Docked search-results panel |
|
|
| `voice-roster` | Current voice participant list |
|
|
| `member-list` | Current channel member list |
|
|
| `voice-settings` | Voice device and quality settings |
|
|
| `voice-controls` | Active call controls |
|
|
| `status-bar` | Debug and connection status bar |
|
|
| `thread-panel` | Thread conversation panel |
|
|
| `settings` | Settings surface |
|
|
|
|
### Authentication regions
|
|
|
|
| Region | Purpose |
|
|
| --- | --- |
|
|
| `auth-shell` | Authentication page layout |
|
|
| `auth-card` | Login and registration card |
|
|
| `auth-header` | Authentication branding header |
|
|
| `theme-picker` | Authentication theme selector |
|
|
|
|
`theme-picker` exists on both pages, but only one page is loaded in a document.
|
|
|
|
## Public layout state
|
|
|
|
Haven exposes three state attributes for themes and layout plugins:
|
|
|
|
| Hook | Values | Purpose |
|
|
| --- | --- | --- |
|
|
| `data-haven-density` on `html` | `compact`, `cozy`, `spacious` | The user's message-density choice |
|
|
| `data-haven-layout-editing` on `html` | `1` while active, otherwise absent | Mod Mode is editing the native layout |
|
|
| `data-haven-layout-owner` on `html` | Plugin-defined owner ID, otherwise absent | The structural layout plugin currently moving regions |
|
|
|
|
The density attribute is applied before first paint. Message geometry properties
|
|
remain internal; use the state only to adapt surrounding layout without
|
|
overriding the user's density choice.
|
|
|
|
Interactive plugins can also listen for synchronous state changes:
|
|
|
|
```js
|
|
document.addEventListener('haven:density-change', (event) => {
|
|
console.log(event.detail.density);
|
|
});
|
|
|
|
document.addEventListener('haven:layout-editing', (event) => {
|
|
if (event.detail.active) restoreNativeLayout();
|
|
else reapplyLayout();
|
|
});
|
|
|
|
document.addEventListener('haven:layout-owner-change', (event) => {
|
|
console.log(event.detail.owner);
|
|
});
|
|
```
|
|
|
|
`haven:layout-editing` fires before Mod Mode starts moving panels and after it
|
|
has restored the saved native arrangement. Structural plugins should suspend
|
|
on the first event and reapply only after the second. `event.detail.owner`
|
|
identifies the owner that was active before editing. Haven reserves ownership
|
|
for that plugin until the second event completes, so other layout plugins
|
|
cannot take its place based on listener order.
|
|
|
|
Structural layout plugins must also acquire the shared owner before moving
|
|
regions and release it after restoring them:
|
|
|
|
```js
|
|
if (HavenApi.Layout.acquire('MyLayout')) {
|
|
movePublicRegions();
|
|
}
|
|
|
|
// During stop(), below the desktop breakpoint, or before Mod Mode edits:
|
|
restorePublicRegions();
|
|
HavenApi.Layout.release('MyLayout');
|
|
```
|
|
|
|
`acquire()` returns `false` while another structural plugin owns the layout.
|
|
Listen for `haven:layout-owner-change` to retry after that owner releases it.
|
|
This prevents one plugin from recording another plugin's temporary DOM as the
|
|
native restore position. `HavenApi.Layout.owner` exposes the current owner.
|
|
|
|
### Appearing in the Layout picker
|
|
|
|
The **Sidebar Layout** picker in Settings, Layout lists **Original** (Haven's own layout) and
|
|
every layout plugin on the server, one at a time. A plugin shows up there when:
|
|
|
|
- its file is named `<Name>Layout.plugin.js` (the picker shows its `@name`,
|
|
without a trailing "Layout");
|
|
- its class has `_engage()` and `_disengage()`, which move and restore the
|
|
regions;
|
|
- it acquires the layout under its file's base name, for example
|
|
`HavenApi.Layout.acquire('MyLayout')` for `MyLayout.plugin.js`;
|
|
- it keeps its on/off as `HavenApi.Data.save('MyLayout', 'layoutOn', '1' | '0')`
|
|
and reads it back on `start()`, as Braid and Compact do.
|
|
|
|
Picking another layout disengages the current one and saves `layoutOn = '0'`
|
|
for it, so a plugin never has to know about the others.
|
|
|
|
## Stability policy
|
|
|
|
For Theme API v1, Haven intends to keep these stable:
|
|
|
|
- `data-haven-theme-api="1"`
|
|
- `data-haven-page` values
|
|
- documented `data-haven-region` values
|
|
- documented `data-haven-density`, `data-haven-layout-editing`, and `data-haven-layout-owner` states
|
|
- documented `haven:density-change`, `haven:layout-editing`, and `haven:layout-owner-change` events
|
|
- `HavenApi.Layout.acquire()`, `release()`, and `owner`
|
|
- documented public design tokens
|
|
|
|
The following are not part of Theme API v1:
|
|
|
|
- element IDs and class names
|
|
- exact DOM nesting or sibling order
|
|
- inline styles used to represent runtime state
|
|
- dynamically generated message, channel, member, and modal internals
|
|
- undocumented custom properties
|
|
- undocumented plugin APIs or JavaScript objects
|
|
|
|
A region keeps its semantic responsibility, but Haven may change its element
|
|
type, class, ID, children, or location. Write selectors against the region and
|
|
avoid depending on its internal descendants when possible.
|
|
|
|
## Responsive layout
|
|
|
|
Haven's core breakpoints are:
|
|
|
|
- above `900px`: full desktop shell
|
|
- `769px` through `900px`: tablet layout with an overlay context sidebar
|
|
- `768px` and below: mobile overlay navigation and context sidebars
|
|
- `480px` and below: phone sizing
|
|
|
|
Restrict deep desktop layout changes to the full desktop shell unless the theme
|
|
also implements and tests the tablet and mobile states:
|
|
|
|
```css
|
|
@media (min-width: 901px) {
|
|
[data-haven-region="navigation-sidebar"] {
|
|
width: 15rem;
|
|
}
|
|
|
|
[data-haven-region="context-sidebar"] {
|
|
width: 15rem;
|
|
}
|
|
}
|
|
```
|
|
|
|
Do not remove the mobile overlay controls solely because they are hidden on a
|
|
desktop screenshot.
|
|
|
|
## Assets and backgrounds
|
|
|
|
Assets placed beside a theme are served from `/themes/`. Use a relative path:
|
|
|
|
```css
|
|
[data-haven-region="main"] {
|
|
background-image:
|
|
linear-gradient(rgba(0, 0, 0, 0.55), rgba(0, 0, 0, 0.55)),
|
|
url("wallpaper.jpg");
|
|
background-position: center;
|
|
background-size: cover;
|
|
}
|
|
```
|
|
|
|
Keep assets small and provide enough contrast for text. Remote resources expose
|
|
members' IP addresses to their hosts and may be blocked by Haven's security
|
|
policy. Prefer files installed with the theme.
|
|
|
|
## Motion and accessibility
|
|
|
|
Respect reduced-motion preferences:
|
|
|
|
```css
|
|
@media (prefers-reduced-motion: reduce) {
|
|
* {
|
|
animation-duration: 0.01ms !important;
|
|
animation-iteration-count: 1 !important;
|
|
scroll-behavior: auto !important;
|
|
}
|
|
}
|
|
```
|
|
|
|
Keep visible focus states, readable contrast, keyboard access, and touch target
|
|
sizes. Hiding a duplicate-looking control is unsafe unless every function it
|
|
provides remains reachable elsewhere.
|
|
|
|
## Theme or plugin?
|
|
|
|
Use a theme for:
|
|
|
|
- colour, typography, spacing, radii, and backgrounds
|
|
- sizing major regions
|
|
- styling or hiding optional decoration
|
|
- reordering direct flex or grid children when behavior remains intact
|
|
|
|
Use a plugin for:
|
|
|
|
- moving controls between different DOM parents
|
|
- creating menus, buttons, or interactive state
|
|
- preserving access to actions removed from persistent chrome
|
|
- reacting to voice, channel, media, or permission changes
|
|
- restoring changed DOM when the customization is disabled
|
|
|
|
CSS can visually position an element outside its parent, but that does not move
|
|
its semantics, clipping context, keyboard order, or event assumptions. Prefer a
|
|
reversible plugin when a layout requires a real structural move.
|
|
|
|
`plugins/CompactLayout.plugin.js` is the minimal bundled example. It moves only
|
|
complete public regions, records their original parents and sibling positions,
|
|
restores the native layout below the desktop breakpoint and during Mod Mode,
|
|
and leaves message density under the user's control. Its `Ctrl+Alt+C` shortcut
|
|
and sidebar button provide an immediate way back to the classic layout.
|
|
|
|
## Security
|
|
|
|
Themes are trusted server-managed CSS. CSS cannot use Haven's JavaScript API,
|
|
but it can hide or imitate interface elements and can request external assets.
|
|
Only install themes from sources you trust.
|
|
|
|
Plugins have a different security model: they execute JavaScript in the Haven
|
|
page and are fully trusted code. Theme API v1 does not make plugins safe or
|
|
sandbox them.
|
|
|
|
## Extension Safe Mode
|
|
|
|
If a theme hides controls or a plugin breaks startup, open Haven with:
|
|
|
|
```text
|
|
/app.html?haven-safe-mode=1
|
|
```
|
|
|
|
The login page also accepts `?haven-safe-mode=1`. Safe mode is stored only for
|
|
the current browser tab, so it survives the login-to-app navigation without
|
|
changing saved preferences.
|
|
|
|
While safe mode is active:
|
|
|
|
- Haven uses its built-in base theme.
|
|
- File themes and additive CSS tweaks are not injected.
|
|
- Plugin files are not fetched, evaluated, or started.
|
|
- Installed extensions are still listed from server-provided metadata.
|
|
- Enabled extensions can be identified and disabled.
|
|
|
|
Open Settings, Plugins & Themes to either reset all theme/plugin choices or exit
|
|
safe mode without changing them. Resetting keeps unrelated account and display
|
|
preferences, updates the server-synchronised theme preference to Haven, and then
|
|
reloads without the safe-mode query parameter.
|
|
If the server cannot confirm the reset, extensions remain suppressed and the
|
|
recovery notice offers a retry after the connection returns.
|
|
|
|
## Minimal complete example
|
|
|
|
```css
|
|
/**
|
|
* @name Calm Slate
|
|
* @description A small Theme API v1 example.
|
|
* @author Example
|
|
* @version 1.0
|
|
* @icon S
|
|
* @haven-theme-api 1
|
|
*/
|
|
|
|
:root {
|
|
--bg-primary: #202225;
|
|
--bg-secondary: #17191c;
|
|
--bg-tertiary: #292c30;
|
|
--bg-hover: #33373d;
|
|
--bg-active: #3c4249;
|
|
--bg-input: #111315;
|
|
--bg-card: #24272b;
|
|
--accent: #7aa2f7;
|
|
--accent-hover: #8fb1fa;
|
|
--accent-glow: rgba(122, 162, 247, 0.25);
|
|
--accent-text: #101216;
|
|
--text-primary: #f0f1f3;
|
|
--text-secondary: #b3b7bd;
|
|
--text-muted: #7f858e;
|
|
--text-link: #8ab4f8;
|
|
--border: #34383e;
|
|
--border-light: #454a52;
|
|
}
|
|
|
|
@media (min-width: 901px) {
|
|
[data-haven-region="navigation-sidebar"] {
|
|
width: 15rem;
|
|
}
|
|
}
|
|
```
|
|
|
|
For a commented template containing every public token, copy
|
|
`themes/custom.css.example`.
|