Skip to main content
Version: V3

Layout Template Reference

The complete catalog for template authors: every surface, tag, variable and component, plus the live styling contract that lets plain CSS react to a running server. For a guided introduction with full-page examples, start with Layout Templates; this page is the part you keep open while writing.

Everything here works in shared (sandboxed) templates unless a row says otherwise — the styling contract is deliberately built from inert markup and CSS, so a design downloaded from a plugin repository can be just as custom as one written locally.

Surfaces

A template is a set of surfaces — named regions the panel mounts. Add them in the editor's surface list; each has its own HTML and CSS, and the template also carries one base stylesheet that applies whenever the template is active.

Surface keyWhat it replaces
LoginPageThe entire login page. Must contain {% component "login-form" %}.
ShellThe shell around every page. Must contain body (the page content) or shell (the whole built-in chrome).
Shell.TopBarStartExtra content at the start (left) of the top app bar.
Shell.TopBarEndExtra content at the end (right) of the top app bar.
Shell.DrawerHeaderThe brand zone above the navigation menu.
Shell.DrawerFooterThe account zone below the navigation menu.
Shell.BeforeContentA banner strip immediately above the page content, on every page.
Shell.AfterContentA banner strip immediately below the page content, on every page.
Shell.FooterThe contents of the bottom bar.
Shell.MenuLinksExtra links appended to the end of the navigation menu (additive — the built-in menu stays).
GameService.HomeA game server's home page content. Voice servers use the GameService surfaces too.
GameService.HeaderA game server's header strip — shown on every page of the service (Home, Console, File Manager, …).
DockerService.HomeA Docker server's home page content.
DockerService.HeaderA Docker server's header strip, on every page of the service.

Blueprint variants. A service surface key may carry a numeric suffix: GameService.Home.526 applies only to servers of game 526 and wins over the plain GameService.Home. The editor's Limit to blueprint picker composes the key for you.

Per-viewer rendering. The four service surfaces are rendered fresh for the service and the person viewing it each time the page opens — that's why they get the extra Service.* / Permissions.* / Features.* variables and why {% t %} translations always match the viewer. All other surfaces render once per visit with the base variables.

Locked out?

Add ?safeMode=1 to the panel address to temporarily ignore the template and fall back to the built-in layout.

Tags

Surfaces are ordinary HTML plus the Liquid template language: {{ output }}, {% if %}, {% for %}, {% assign %} and the other standard tags all work, plus three TCAdmin-specific tags.

{{ Variable }} — insert a value

<h2>{{ Service.Name }}</h2>
<p>{{ Service.BlueprintName }} — {{ Service.HostnameOrIp }}:{{ Service.GamePort }}</p>

Output is HTML-encoded automatically. Values are a snapshot from when the page rendered — for numbers that change while the page is open, use the live value components instead.

{% component "name" %} — splice a live panel piece

{% component "login-form" %}
{% component "live-cpu" %}

Drops a real, interactive piece of the panel into your markup — everything in the component catalog below. Components keep working after render: they update themselves, obey permissions, and handle their own clicks.

{% surface "Key" %} — place a customizable region

{% surface "Shell.Footer" %}

Used inside a hand-built Shell to position the placeable regions: Shell.TopBarStart, Shell.TopBarEnd, Shell.DrawerHeader, Shell.DrawerFooter, Shell.BeforeContent, Shell.AfterContent and Shell.Footer. Each renders the author's surface if the template defines one, otherwise the built-in default. (Shell.MenuLinks and the service surfaces are mounted automatically and are never placed this way.)

{% t 'Key', 'Fallback' %} — translated text

Writes a panel UI translation in the viewer's language — the same translation catalog the rest of the panel uses (Settings → Languages). If the key has no translation in the viewer's language, the inline fallback is shown; with no fallback, the key itself is.

<span class="my-tab-label">{% t 'ServiceHome.Connect', 'Connect' %}</span>
<a href="{{ Service.Url }}/Logs" title="{% t 'Service.Logs', 'Logs' %}"></a>
  • Both arguments accept single or double quotes; single quotes read best inside HTML attributes.
  • Output is always HTML-encoded.
  • Text rendered by {% component %} tokens is already translated — the tag is for labels you write around them.
  • Reuse keys that already exist (browse them under Settings → Languages). Your theme-specific flavor text won't have a key — leave it as plain text rather than inventing keys nobody translates.

Liquid conditions — one warning

In Liquid, only nil and false are falsy. An empty string and an empty list both pass {% if %}, which is the single most common cause of a stray label or an empty row in a template. Test presence explicitly:

{# Text — compare against an empty string #}
{% if Service.ConnectUrl and Service.ConnectUrl != '' %}
<a href="{{ Service.ConnectUrl }}">Connect</a>
{% endif %}

{# Lists — compare the size #}
{% if Service.RegionNames.size > 0 %}
<span>{{ Service.RegionNames | join: ', ' }}</span>
{% endif %}

Base variables

Available on every surface:

VariableTypeDescription
AppNamestringThe application name.
AppVersionstringThe running version.
CulturestringThe current language code.
IsAuthenticatedbooleanWhether the visitor is signed in.
UserNamestringThe signed-in user's name.
Branding.TitlestringThe branding title.
Branding.SubtitlestringThe branding subtitle.
Branding.LogoUrlstringThe branding logo image URL.

Service page variables

Available on the four service surfaces (GameService.Home / .Header, DockerService.Home / .Header) and their blueprint variants, in addition to the base variables.

Service.*

VariableTypeDescription
Service.ServiceIdnumberThe service's id.
Service.NamestringThe service's name.
Service.ServiceTypestringGameService (also voice servers) or DockerService.
Service.BlueprintIdnumberThe game or Docker blueprint id.
Service.BlueprintNamestringThe game or Docker blueprint name.
Service.BlueprintShortNamestringThe blueprint's short name (e.g. HLL, GMOD) — the compact identifier to key an external system or query platform off. Empty when the blueprint sets none.
Service.ImageUrlstringThe blueprint's background artwork, ready to use as an image source. Empty when the blueprint has none.
Service.IconUrlstringThe blueprint's icon image, ready to use as an image source. Empty when the blueprint has none.
Service.IpAddressstringThe service's IP address.
Service.HostnamestringThe service's hostname, when one is set.
Service.HostnameOrIpstringThe hostname if set, otherwise the IP address.
Service.UrlstringThe relative address of the service's home page — for links to sub-pages, e.g. {{ Service.Url }}/FileManager.
Service.ConnectUrlstringThe blueprint's click-to-connect link, resolved for this service (e.g. steam://connect/1.2.3.4:28015). Empty when the blueprint defines none or a value in it can't be resolved.
Service.StatusstringThe status name when the page loaded (Started, Stopped, …) — a snapshot, not live.
Service.OwnerNamestringThe name of the user who owns the service.
Service.PrioritystringThe process priority the service runs at — Normal, High, BelowNormal, and so on (game servers). Only filled in for a viewer allowed to see the service's settings — see Restricted values.
Service.BillingIdstringThe billing system's identifier for this service. Restricted — see below.
Service.BillingStatusstringThe billing state — Active or Suspended. Restricted — see below.
Service.ServerIdnumberThe id of the server hosting the service.
Service.ServerNamestringThe name of the server hosting the service.
Service.ServerOperatingSystemstringThe hosting server's operating system — Windows or Linux.
Service.DatacenterIdnumberThe id of the datacenter the hosting server belongs to.
Service.DatacenterNamestringThe datacenter's name, e.g. Dallas, Texas, United States (DFW).
Service.RegionIdslistThe datacenter's region ids — see below.
Service.RegionNameslistThe datacenter's region names — see below.
Service.DiskSpacenumberThe disk quota in bytes (empty when unlimited).
Service.DiskUsagenumberThe last known disk usage in bytes.
Service.SlotsnumberPlayer slots (game servers).
Service.GamePortnumberThe game port (game servers).
Service.QueryPortnumberThe query port (game servers).
Service.RconPortnumberThe RCON port (game servers).
Service.VirtualServerNamestringThe virtual server hosting the service, when there is one.
Service.CpuLimitnumberThe CPU limit, as a percentage.
Service.MemoryLimitnumberThe memory limit in bytes.
Service.ImagestringThe container image (Docker servers).
Service.TagstringThe image tag (Docker servers).
Service.PortslistEvery named port — see below.

Each entry in Service.Ports:

VariableTypeDescription
NamestringThe port's name (for example GamePort).
PortnumberThe port number.
DescriptionstringThe port's description, when one is set.
UristringA click-to-connect link when the blueprint defines one.
{% for p in Service.Ports %}
<div class="my-port" data-part="item" data-kind="port">
<b>{{ p.Port }}</b> <span>{{ p.Name }}</span>
{% if p.Uri and p.Uri != '' %}<a href="{{ p.Uri }}">Open</a>{% endif %}
</div>
{% endfor %}

Restricted values

Most Service.* values are shown to anyone who can open the page. Three are not — they are filled in only for a viewer who is already allowed to see them elsewhere in the panel, and are empty for everyone else:

ValueShown to
Service.PriorityViewers who can see this service's settings — the same people the built-in Settings page is available to. Also empty for Docker servers, which have no priority.
Service.BillingIdViewers who can manage billing (Permissions.ManageBilling).
Service.BillingStatusViewers who can manage billing (Permissions.ManageBilling).

A template can't reveal a value the panel would otherwise withhold, so always guard the block — otherwise a customer sees your "Billing" label with nothing next to it:

{% if Permissions.ManageBilling %}
<div class="my-row">
<span>Billing</span>
<span class="my-billing my-billing--{{ Service.BillingStatus }}">
{{ Service.BillingStatus }}
</span>
</div>
{% if Service.BillingId %}
<div class="my-row">
<span>Invoice reference</span>
<span>{{ Service.BillingId }}</span>
</div>
{% endif %}
{% endif %}

{% if Service.Priority %}
<div class="my-row">
<span>Process priority</span>
<span>{{ Service.Priority }}</span>
</div>
{% endif %}
.my-billing--Active { color: #22c55e; }
.my-billing--Suspended { color: #ef4444; }
note

Service.BillingStatus is only Active or Suspended. There is no renewal or expiry date on a service, so a "days remaining" figure can't be built from it — that number lives in your billing system.

Where the service runs

The server, datacenter and region values describe the machine hosting the service, which is a different thing from the service itself. Service.Name is what the customer called their server; Service.ServerName is the node it happens to run on.

<div class="my-facts">
<div class="row">
<span>Game</span>
<span>
{{ Service.BlueprintName }}
{% if Service.BlueprintShortName != '' %}
<small>{{ Service.BlueprintShortName }}</small>
{% endif %}
</span>
</div>

{% if Service.DatacenterId %}
<div class="row" data-datacenter="{{ Service.DatacenterId }}">
<span>Datacenter</span>
<span>{{ Service.DatacenterName }}</span>
</div>
{% endif %}

{% if Service.ServerName %}
<div class="row">
<span>Node</span>
<span>{{ Service.ServerName }} ({{ Service.ServerOperatingSystem }})</span>
</div>
{% endif %}
</div>
note

Service.ServerOperatingSystem is the hosting server's operating system, not the game's. They can differ: a Windows game running under Wine sits on a Linux server, and a Linux game running under WSL sits on a Windows server. Label it accordingly if you show it to customers.

Region lists

A datacenter can belong to more than one region, so Service.RegionIds and Service.RegionNames are lists rather than single values. They always have the same length and the same order, so position 0 of one matches position 0 of the other, and both are empty when the service has no datacenter.

{# The usual case — one line, all regions #}
{{ Service.RegionNames | join: ', ' }}

{# Just the first #}
{{ Service.RegionNames | first }}

{# How many, and one by position #}
{{ Service.RegionNames.size }}
{{ Service.RegionNames[0] }}

{# Loop the names #}
{% for name in Service.RegionNames %}
<span class="my-region">{{ name }}</span>
{% endfor %}

{# Loop both together, paired by position #}
{% for id in Service.RegionIds %}
<span class="my-region" data-region="{{ id }}">
{{ Service.RegionNames[forloop.index0] }}
</span>
{% endfor %}

{# Is the service in a particular region? #}
{% if Service.RegionIds contains 9 %}
<span class="my-badge">Central</span>
{% endif %}

:::warning An empty list is truthy Just like an empty string, an empty list passes {% if %} — so {% if Service.RegionNames %} is always true and would print an empty line for a service with no regions. Always compare the size instead:

{% if Service.RegionNames.size > 0 %}
<div class="my-regions">{{ Service.RegionNames | join: ', ' }}</div>
{% endif %}

:::

Blueprint artwork

Service.ImageUrl and Service.IconUrl are ready to use directly as an image source — including in sandboxed templates, where an artwork address hosted elsewhere would normally be removed. Both are empty when the blueprint has no artwork, so test before using them:

{% if Service.ImageUrl %}
<img class="my-hero" src="{{ Service.ImageUrl }}" alt="{{ Service.BlueprintName }}">
{% endif %}

Permissions.*

What the built-in pages would show this viewer for this service — visibility only; every action is re-checked when used. The stat booleans already include LiveStats, and Console / FastDL already include their feature toggles, so each can be tested on its own.

VariableDescription
Permissions.ControlStart, stop and restart the service.
Permissions.KillServiceForce-stop the service.
Permissions.ConsoleUse the console (game) or the interactive terminal (Docker).
Permissions.LogsView the service's log files.
Permissions.ServiceActivityView the service's activity history.
Permissions.LiveStatsSee live usage statistics.
Permissions.PlayerStatsSee player counts.
Permissions.CpuStatsSee CPU usage.
Permissions.MemoryStatsSee memory usage.
Permissions.NetworkStatsSee network traffic.
Permissions.ServiceSettingsOpen the service's settings.
Permissions.CustomScriptsManage the service's custom scripts.
Permissions.ReinstallReinstall the service.
Permissions.DeleteDelete the service.
Permissions.ModsInstall mods.
Permissions.FastDLManage Fast Downloads.
Permissions.LogConsoleView the container log (Docker servers; always false for game servers).
Permissions.FileManagerUse the File Manager.
Permissions.ConfigFilesEdit configuration files.
Permissions.ScheduledTasksManage scheduled tasks.
Permissions.BackupsManage backups.
Permissions.FtpSee FTP connection details.
Permissions.ManageBillingSee the service's billing identifier and status. Unlike the others this is a panel-wide permission rather than a per-service one.

Features.*

Permission-independent blueprint/config toggles — to tell "this viewer can't see it" apart from "this service doesn't have it". All booleans: Features.ConsoleEnabled, Features.FileManagerEnabled, Features.ConfigFilesEnabled, Features.BackupsEnabled, Features.ScheduledTasksEnabled, Features.UpdatesEnabled, Features.SteamUpdateEnabled, Features.FastDLEnabled.

Components

Placed with {% component "name" %}. Every service-page component self-gates: it shows and hides itself with the same permission and feature rules as the built-in pages, and renders nothing outside a service's pages — wrap one in {% if Permissions.… %} only when your surrounding markup should disappear too. service-… components work on both service types; game-… / docker-… only on their own.

Shell and login

ComponentWhat it adds
bodyThe current page's content (required in a hand-built shell).
shellThe entire built-in shell — a quick base when you only want to restyle with CSS.
menuThe navigation drawer and menu.
menu-toggleThe button that opens and collapses the drawer.
topbarThe complete built-in top bar.
breadcrumbsThe breadcrumb trail.
login-formThe sign-in form (required on the login page).
brandThe logo and branding block.
dark-mode-toggleThe light / dark switch.
language-selectorThe language picker.
connection-statusA warning shown only when the live connection drops.
connection-indicatorAn always-on connection dot (green / amber / red).
health-indicatorA system-health indicator (shown only to permitted users).
announcementsThe customer announcements control.
impersonation-bannerThe "you are impersonating…" strip with its exit button (shows only while impersonating — always include it in a custom shell).
footer-resourcesThe current page's live stats — a game or Docker server's disk, CPU, memory, network and players — for custom footers.

Service building blocks (composed)

The ready-made blocks of the built-in service pages:

ComponentWhat it adds
service-titleThe service's name, icon and address line (updates live).
service-powerThe Start / Stop / Restart / Kill button strip.
service-advanced-actionsThe extra action buttons — settings, scripts, reinstall, delete, move.
service-detailsThe service details table.
service-uptime-tileThe uptime tile.
service-cpu-tileThe live CPU tile.
service-memory-tileThe live memory tile.
service-disk-tileThe disk usage tile.
service-recent-logsThe recent log files panel.
service-recent-activityThe recent activity timeline.
game-status-alertThe warning shown while a game server's status is unknown.
game-connection-infoThe connection, FTPS/SFTP and RCON info cards.
game-players-tileThe live players tile.
game-network-tileThe live network traffic tile.
game-consoleThe game's web console (text or screen capture).
docker-status-headerThe container status header with uptime, ports and quick settings.
docker-portsThe IP and port chips.
docker-logs-terminalThe read-only container log terminal.
docker-exec-terminalThe interactive container terminal.
Consoles need a fixed height

Always give game-console and the Docker terminals a fixed-height container (for example height: 480px; min-width: 0; overflow: hidden;), and use minmax(0, …) for CSS grid tracks around them. In an auto-sized cell, the terminal and the cell resize each other in a loop and the browser tab can freeze.

Live values (headless)

Bare, self-updating values with no chrome at all — you own the tile markup around them. Each renders as a small <span> whose text refreshes every second; a stat with no data yet shows --, and a permission-gated stat renders nothing.

ComponentShows
live-nameThe service name (stays live through renames).
live-statusThe localized status text ("Running", "Stopped", …).
live-status-orbAn empty span carrying data-status="started|stopped|starting|stopping|unknown" — a styling hook for your own status dot.
live-uptimeCompact uptime, e.g. 8h 40m.
live-cpuCPU percent, including the % sign.
live-memoryMemory used, humanized (e.g. 1.4 GB).
live-memory-limitThe memory limit, humanized.
live-diskDisk used, humanized.
live-disk-limitThe disk quota, humanized (-- when unlimited).
live-playersCurrent players, bare count (game).
live-players-maxPlayer slots, bare count (game).
live-network-upBytes sent, humanized (game).
live-network-downBytes received, humanized (game).
live-mapThe map the game reports it is running (game).
live-game-typeThe game type / gamemode the game reports (game).
live-query-statusThe localized query state — "Online", "Offline" or "Unknown" (game).
live-query-orbAn empty span carrying data-query="online|offline|unknown" — a styling hook for your own query dot (game).

:::note Query state is not power state live-status reports the service: whether the panel started it. live-query-status reports the game: whether it is answering queries. They disagree on purpose — a server that has just started but hasn't finished loading is Running and Offline, which is the honest answer, because nobody can join it yet. Show both if your customers care about whether the server is joinable.

"Unknown" means the game has never answered — not the same as offline. :::

The map and game type show -- when the game reports no value for them — some games report neither. For a viewer who isn't allowed to see live stats they render nothing at all, which would leave the label you wrote sitting next to a blank. Put the label and the value in one wrapper and hide the wrapper when the value didn't render:

<div class="my-row my-row--optional">
<span>Map</span>
<span>{% component "live-map" %}</span>
</div>
/* No live value inside → drop the whole labelled row. */
.my-row--optional:not(:has(.tca-live)) { display: none; }

This is worth doing around any component that can hide itself — the FTP and SFTP addresses listed under Single-purpose atoms behave the same way.

A query dot needs no JavaScript — the empty orb carries the state and your CSS does the rest:

<span class="my-query">
{% component "live-query-orb" %}
{% component "live-query-status" %}
</span>
.my-query .tca-live-query-orb {
display: inline-block; width: 8px; height: 8px; border-radius: 50%;
background: #6b7280; /* unknown */
}
.my-query .tca-live-query-orb[data-query="online"] { background: #22c55e; }
.my-query .tca-live-query-orb[data-query="offline"] { background: #ef4444; }

A live span that represents a percentage also carries --pct — so a hand-built gauge needs no JavaScript:

<div class="my-tile">
{% component "live-cpu" %}
<span class="my-label">{% t 'ServiceHome.CPU', 'CPU' %}</span>
</div>
/* The live span fills its own background from the percentage it shows. */
.my-tile .tca-live-cpu {
display: block;
background: linear-gradient(90deg, #ff7a2e calc(var(--pct, 0) * 1%), transparent 0);
}

Single-purpose atoms

Pieces of the composed blocks, for templates that own all the surrounding chrome:

ComponentWhat it adds
power-startThe lone Start button (visible while stopped/unknown).
power-stopThe lone Stop button (visible while started).
power-restartThe lone Restart button (visible while started).
power-killThe lone Kill button (visible while starting/stopping).
service-iconThe service's category icon alone.
game-connection-addresshost:gameport plus a copy button, chrome-less (game).
game-ftps-addresshost:ftpport plus a copy button (game).
game-sftp-addresshost:sftpport plus a copy button (game) — renders nothing when the host offers no SFTP.
game-rcon-addresshost:rconport plus a copy button (game).
service-host-addressThe bare host/IP plus a copy button (both types — the one copyable address on Docker pages).
service-logs-listThe recent-logs list only, no panel chrome.
service-activity-listThe recent-activity timeline only, no panel chrome.
{% if Permissions.Control %}
<div class="my-power-row">
{% component "power-start" %}
{% component "power-restart" %}
{% component "power-stop" %}
{% component "power-kill" %}
</div>
{% endif %}

Each power atom appears and disappears with the service's state on its own — the row above needs no conditions beyond the permission around your wrapper.

Live styling

The pieces that let plain CSS produce living pages: restyle the built-in components without touching their internals, and make your own markup react when the server starts, stops, or its numbers move. Everything in this section is inert markup and CSS — it all works in sandboxed templates.

The data-part contract

Every service component marks its outermost element with data-tca-component="<token>" and its inner elements with data-part="<role>" (plus data-kind="<variant>" where one role repeats). So you restyle by role, never by internal class names:

/* The CPU tile's number, whatever element happens to render it. */
[data-tca-component="service-cpu-tile"] [data-part="value"] { font-size: 2rem; }

/* Every action button anywhere — and the delete one specifically. */
[data-part="action"] { border-radius: 10px; }
[data-part="action"][data-kind="delete"] { color: #ff5470; }

The role vocabulary:

data-partMeaning
labelThe caption of a block ("CPU", "Recent Logs").
valueThe primary value the block exists to show.
unitA denominator or suffix qualifying the value ("/ 48 GB", "%").
metaSecondary supporting text (a timestamp, an identity line).
iconA decorative or semantic icon.
barA progress / meter element — carries --pct.
mediaAn avatar / art / status-orb block.
statusAn element whose appearance tracks the service status.
listA container of repeated items.
itemOne entry in a list, or one card in a repeated set.
rowOne row of a details table.
actionSomething the user clicks. Usually carries data-kind.
panelA secondary region that opens/collapses.
emptyThe "nothing to show" placeholder a list renders instead of items.

You may write data-part / data-kind on your own markup too (they're allowed in sandboxed templates), so one rule can style your hand-built tiles and the built-in ones together. data-tca-component is engine identity and can't be written by a template.

The percentage hook

Any element that stands for a 0–100 percentage — every compiled progress bar (data-part="bar") and every live span with a computable percentage — publishes it as the CSS custom property --pct. Replace a progress bar with your own gradient, ring or gauge without naming a single internal class:

[data-tca-component="service-memory-tile"] [data-part="bar"] {
background: linear-gradient(90deg, #3ddc97 calc(var(--pct, 0) * 1%), #222 0);
}

The page state bridge

While a service page is open, the panel publishes that service's live state onto the <html> element — refreshed every second, so page-wide CSS can react to the service without any JavaScript:

On <html>Values
data-tca-statusstarted | stopped | starting | stopping | unknown
data-tca-service-typegame | docker
data-tca-service-idThe service id.
data-tca-blueprint-idThe blueprint id — per-game styling from one global stylesheet.
CSS variable on <html>Value
--svc-cpuCPU usage, 0100.
--svc-memoryMemory usage as a percentage of the limit, 0100.
--svc-diskDisk usage as a percentage of the quota, 0100.
--svc-playersPlayers as a percentage of slots, 0100.
--svc-players-countRaw player count (not clamped).
--svc-players-maxRaw slot count.

A variable the viewer isn't allowed to see (or that has no data) is removed, not zeroed — use the var() fallback to tell "no data" from "zero": var(--svc-cpu, 0).

The classic use — a connect button that dies and revives with the server, live, even though Liquid only rendered once:

{% if Service.ConnectUrl and Service.ConnectUrl != '' %}
<a class="my-connect" href="{{ Service.ConnectUrl }}">
{% t 'ServiceHome.Connect', 'Connect' %} — {{ Service.HostnameOrIp }}
</a>
{% endif %}
.my-connect { transition: opacity .2s; }
html[data-tca-status="stopped"] .my-connect,
html[data-tca-status="unknown"] .my-connect {
opacity: .35;
filter: grayscale(1);
pointer-events: none; /* unclickable until the server is back */
}

Or tint a hero by state and drive a page-wide meter:

html[data-tca-status="started"] .my-hero { border-color: #3ddc97; }
html[data-tca-status="stopped"] .my-hero { border-color: #555; }
.my-hero-meter { width: calc(var(--svc-players, 0) * 1%); }
Load-time vs live

Liquid decides structure once, at render time (which tab is first, whether a section exists at all). The bridge and the live components handle everything that changes while the page is open. Don't use {{ Service.Status }} for anything that should react live — it's a snapshot.

Sandboxed vs trusted templates

Templates imported from files or a plugin repository always run sandboxed. A locally-authored theme can be marked Trusted template on the theme's settings — a deliberate admin decision.

Sandboxed (default for shared)Trusted
Scripts / event handlersStripped.Allowed (inline <script> works).
Links, images, CSS url(…)Same-origin only — anything pointing at another site is removed, in HTML and CSS alike.Any origin (CDNs, fonts, external links).
<form>Not allowed (sign-in is the login-form component).Allowed.
CSS@import, @media, @keyframes, vendor-prefixed properties and custom properties are filtered — except --tca-*, which sandboxed authors may define freely (same-origin values only).Full CSS.
class / id / inline style / data-part / data-kindAllowed.Allowed.
Design for the sandbox

Everything in Live stylingdata-part, --pct, the state bridge — plus the live components, atoms and {% t %} work fully sandboxed. Reach for trust only when you genuinely need external assets, media queries, keyframe animations or script. For responsive behavior in the sandbox, the panel's helper classes still work (d-none d-md-flex).

Author checklist

  • Empty string is truthy in Liquid — presence tests are {% if x and x != '' %}.
  • Snapshot vs live{{ Service.* }} values are render-time; live numbers come from live-* components and live behavior from the state bridge.
  • Consoles and terminals need a fixed-height container, or the page can freeze.
  • Don't gate what already gates itself — service components hide themselves; only wrap them in {% if %} when your surrounding chrome must vanish too.
  • Use single quotes inside attributestitle="{% t 'Messages.Open', 'Open' %}".
  • Test both states — open your design with the server running and stopped; the state bridge makes the difference visible immediately.
  • A broken surface never locks you out — service surfaces fall back to the built-in page, and ?safeMode=1 rescues the shell and login.