@thespielplatz/tsp-tools-theme
v0.1.1
Published
tsp.tools design-system theme as a Nuxt layer (Nuxt UI v4 + Tailwind v4): amber-on-anthracite tokens (light + dark), Nunito/Space Grotesk fonts, per-area colour mode, and the shared app shell. Use globally via `extends`, or scoped to a sub-area.
Maintainers
Readme
@thespielplatz/tsp-tools-theme
The tsp.tools design-system theme as a Nuxt layer — amber-on-anthracite tokens (light + dark), Nunito / Space Grotesk fonts, a per-area colour-mode policy, and the shared app shell (sidebar, content container, theme toggle).
Built for the platform stack: Nuxt 4 · Nuxt UI v4 · Tailwind v4. It is the extracted, generalised form of the proven piggybank admin implementation (ADR 008, step 3).
Two consumption modes:
- Global —
extendsthe layer and the whole app is themed (e.g. trips). - Scoped — apply the theme to a sub-area only, leaving the rest of the app on default Nuxt UI, with per-area colour mode (e.g. piggybank: admin is themed + toggleable, the public site stays light).
Install
npm i -D @thespielplatz/tsp-tools-themePeer expectations (provide them in the consuming app):
nuxt^4,@nuxt/ui^4 (Tailwind v4 comes with Nuxt UI).- Tabler icons — the theme uses
i-tabler-*(e.g.i-tabler-sun,i-tabler-brand-github). Install@iconify-json/tablerfor offline icons, or rely on Iconify's network fetch. Icons are not bundled. Keep one icon family across the platform; don't mix in a second set.
Extend by package name (
@thespielplatz/tsp-tools-theme) — resolved throughnode_modules, this reliably registers the layer's components, composables and plugin. (A deep relative path such asextends: ['../..']can fail to pick up the source dirs; the package name always works.)
Global usage (whole app themed)
nuxt.config.ts:
export default defineNuxtConfig({
extends: ['@thespielplatz/tsp-tools-theme'],
runtimeConfig: {
public: { tspTheme: { apply: 'global' } }, // this is also the default
},
})That's it — the colour-mode plugin puts the .tsp-theme scope class on <html>, so every
Nuxt UI component renders amber-on-anthracite, dark by default, toggleable app-wide. Drop the
shell components wherever you like:
<template>
<div class="flex min-h-svh bg-default text-default">
<TspSidebar>
<template #brand><TspWordmark>trips<span class="text-primary">.</span></TspWordmark></template>
<template #nav>
<TspNavItem to="/" icon="i-tabler-car">Fahrten</TspNavItem>
<TspNavItem to="/settings" icon="i-tabler-settings">Einstellungen</TspNavItem>
</template>
<template #footer>
<TspSidebarFooter github-link="https://github.com/…" version="v1.2.0">
<template #user>…your user link…</template>
<template #logout>…your logout…</template>
</TspSidebarFooter>
</template>
</TspSidebar>
<main class="flex-1 min-w-0 bg-default">
<TspContainer><NuxtPage /></TspContainer>
</main>
</div>
</template>Scoped usage (one sub-area themed)
nuxt.config.ts:
export default defineNuxtConfig({
extends: ['@thespielplatz/tsp-tools-theme'],
runtimeConfig: {
public: {
tspTheme: {
apply: 'scoped',
pathPrefix: '/admin', // routes that are "themed"
publicMode: 'light', // colour mode forced on every other route
defaultMode: 'dark', // default for themed routes (remembered in a cookie)
},
},
},
})Wrap the themed area in <TspThemeProvider>; everything outside it stays default Nuxt UI:
<!-- pages/admin/index.vue -->
<template>
<TspThemeProvider class="flex min-h-svh">
<TspSidebar> … </TspSidebar>
<main class="flex-1 min-w-0 bg-default">
<TspContainer> … </TspContainer>
</main>
</TspThemeProvider>
</template>The colour-mode plugin then forces publicMode on non-pathPrefix routes and uses the
remembered preference inside the themed area — so the public and themed surfaces never fight
over light/dark.
What's in the box
Tokens (assets/css/theme.css) — exposed as Nuxt UI CSS variables under the .tsp-theme
scope:
| Group | Light | Dark |
|---|---|---|
| primary (amber) | #fbad18 | #fbad18 |
| error (tomato) | #ff6347 | #ff6347 |
| bg | #f8f9fa | #212529 |
| surface (bg-elevated) | #ffffff | #2b3035 |
| border | #dee2e6 | #404041 |
| text | #212529 | #f0f0f0 |
| on-primary (text-inverted) | #212529 | #212529 |
Brand colours (amber, tomato) are the same in both modes; only bg/surface/border/text flip.
The amber ramp is also available as Tailwind utilities (*-tsp-amber-{50..950}).
Fonts — Nunito (text + headings) and Space Grotesk (logo), via @nuxt/fonts. Use the
tsp-wordmark class (or <TspWordmark>) for Space Grotesk; everything else is Nunito.
Composable — useTspColorMode() → { pref, toggle }, the remembered (cookie) preference
for the themed area.
Components (auto-imported):
| Component | Purpose |
|---|---|
| TspThemeProvider | Scoped wrapper — marks a sub-area as themed (.tsp-theme). |
| TspSidebar | App-shell sidebar frame; slots #brand #nav #footer. |
| TspSidebarFooter | User → Logout → divider → theme toggle → GitHub + version. |
| TspThemeToggle | Sun/moon light–dark toggle (uses useTspColorMode). |
| TspNavItem | Sidebar nav link (muted → filled-amber when active). |
| TspWordmark | Space Grotesk + amber brand wordmark. |
| TspContainer | Centered content column (max-w-4xl, px-6 sm:px-10, pt-10 pb-16). |
App-specific content (branding, nav lists, user identity, GitHub/version values) is injected via slots/props — none of it lives in the layer.
Why primary isn't set in app.config
app.config is global. Setting ui.colors.primary = 'amber' there would make every Nuxt
UI component amber across the whole app, which breaks scoped mode. Instead all brand theming
lives in the .tsp-theme CSS scope, which custom properties resolve per element — so amber
only applies inside the scope. For global mode the plugin simply puts .tsp-theme on <html>.
The mode groups are written for both layouts: .dark .tsp-theme (scoped) and
.dark.tsp-theme (global, both classes on <html>).
CSS ownership
assets/css/theme.css owns the framework import (@import "tailwindcss"; @import "@nuxt/ui";)
and is added to the Nuxt css array automatically when you extend the layer. How a consumer
handles its own CSS depends on whether it has its own @theme:
No own design tokens (global apps, or scoped apps whose non-themed area uses plain Nuxt UI): you don't need any framework import — the layer provides it. If you had your own
@import "tailwindcss", drop it.Has its own
@theme(e.g. a scoped app with a separate public-area palette/fonts): keep your own@import "tailwindcss"; @import "@nuxt/ui";in that stylesheet. A@themeblock only compiles inside a Tailwind entry, so it can't piggyback on this layer's stylesheet — yours is a separate Tailwind graph. The duplicate import is intentional and harmless: both graphs share the same runtime CSS variables, and.tsp-themeonly re-points Nuxt UI's variables inside the themed subtree.Proven by the piggybank adoption: the public Piggy palette stays in the app's own
main.css(its own Tailwind entry); the admin tsp tokens come from this layer'stheme.css.
The layer's stylesheet is also exposed for manual import if you ever need it:
@import "@thespielplatz/tsp-tools-theme/assets/css/theme.css".
Develop
npm install
npm run dev # global demo (playground/) → http://localhost:3000
npm run dev:scoped # scoped demo (scoped/) → public vs /admin
npm run lint
npm run build:playgroundThe playground/ (global) and scoped/ demos are direct-child layers that extends: ['..'].
They are excluded from the published package (files allowlist).
License
MIT © TheSpielplatz
