@thespielplatz/tsp-tools-theme 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -147,14 +147,55 @@ for the themed area.
|
|
|
147
147
|
| `TspThemeProvider` | Scoped wrapper — marks a sub-area as themed (`.tsp-theme`). |
|
|
148
148
|
| `TspSidebar` | App-shell sidebar frame; slots `#brand` `#nav` `#footer`. |
|
|
149
149
|
| `TspSidebarFooter` | User → Logout → divider → theme toggle → GitHub + version. |
|
|
150
|
+
| `TspSiteHeader` | Public/marketing header; slots `#brand` `#nav` `#actions`, prop `sticky`. |
|
|
151
|
+
| `TspSiteFooter` | Public/marketing footer; slots `#brand` `#links` `#actions`. |
|
|
150
152
|
| `TspThemeToggle` | Sun/moon light–dark toggle (uses `useTspColorMode`). |
|
|
151
153
|
| `TspNavItem` | Sidebar nav link (muted → filled-amber when active). |
|
|
152
154
|
| `TspWordmark` | Space Grotesk + amber brand wordmark. |
|
|
153
|
-
| `TspContainer` | Centered content column
|
|
155
|
+
| `TspContainer` | Centered content column; props `width` (`app` \| `site`) and `padded`. |
|
|
154
156
|
|
|
155
157
|
App-specific content (branding, nav lists, user identity, GitHub/version values) is injected
|
|
156
158
|
via slots/props — none of it lives in the layer.
|
|
157
159
|
|
|
160
|
+
### App shell vs site shell
|
|
161
|
+
|
|
162
|
+
`TspSidebar` + `TspSidebarFooter` are the **logged-in** shell. For a public/marketing surface
|
|
163
|
+
use `TspSiteHeader` + `TspSiteFooter` with the wide container:
|
|
164
|
+
|
|
165
|
+
```vue
|
|
166
|
+
<TspSiteHeader>
|
|
167
|
+
<template #brand><NuxtLink to="/"><TspWordmark>tsp<span class="text-primary">.</span>tools</TspWordmark></NuxtLink></template>
|
|
168
|
+
<template #nav><a href="#tools">Tools</a></template>
|
|
169
|
+
<template #actions><TspThemeToggle /></template>
|
|
170
|
+
</TspSiteHeader>
|
|
171
|
+
|
|
172
|
+
<TspContainer width="site"><NuxtPage /></TspContainer>
|
|
173
|
+
|
|
174
|
+
<TspSiteFooter>
|
|
175
|
+
<template #brand><TspWordmark>tsp<span class="text-primary">.</span>tools</TspWordmark></template>
|
|
176
|
+
<template #links><NuxtLink to="/imprint">Imprint</NuxtLink></template>
|
|
177
|
+
</TspSiteFooter>
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`TspContainer` defaults to `width="app"` (`max-w-4xl`, `px-6 sm:px-10`) — the app/admin measure.
|
|
181
|
+
`width="site"` is `max-w-[1240px]`, `px-6 md:px-10`, the marketing measure. Both add `pt-10 pb-16`
|
|
182
|
+
unless you pass `:padded="false"` (which is what the header/footer frames do internally).
|
|
183
|
+
|
|
184
|
+
### Localising the theme toggle
|
|
185
|
+
|
|
186
|
+
`TspThemeToggle` takes all four strings as props, named by destination — `…ToLight` is what the
|
|
187
|
+
button shows and announces while dark:
|
|
188
|
+
|
|
189
|
+
```vue
|
|
190
|
+
<TspThemeToggle
|
|
191
|
+
:label-to-light="t('theme.light')" :label-to-dark="t('theme.dark')"
|
|
192
|
+
:aria-label-to-light="t('theme.toLight')" :aria-label-to-dark="t('theme.toDark')"
|
|
193
|
+
/>
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The defaults are English. The default slot receives `{ isDark, label }` if you want different
|
|
197
|
+
markup (an icon-only square button, say) while keeping the mechanism.
|
|
198
|
+
|
|
158
199
|
---
|
|
159
200
|
|
|
160
201
|
## Why `primary` isn't set in `app.config`
|
|
@@ -203,6 +244,21 @@ npm run build:playground
|
|
|
203
244
|
The `playground/` (global) and `scoped/` demos are direct-child layers that `extends: ['..']`.
|
|
204
245
|
They are excluded from the published package (`files` allowlist).
|
|
205
246
|
|
|
247
|
+
## Release
|
|
248
|
+
|
|
249
|
+
The version bump and the git tag are **not** done by an npm script:
|
|
250
|
+
|
|
251
|
+
1. Bump `package.json` `.version`, commit, and push to `main`.
|
|
252
|
+
2. Publish a GitHub Release for that commit — GitHub creates the `vX.Y.Z` tag on publish.
|
|
253
|
+
3. Ship the package:
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
npm run release # lint, then npm publish
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
`npm run release` deliberately only lints and publishes. It never bumps the version or tags,
|
|
260
|
+
so it can't drift from the version that was already committed and tagged in steps 1–2.
|
|
261
|
+
|
|
206
262
|
## License
|
|
207
263
|
|
|
208
264
|
MIT © TheSpielplatz
|
|
@@ -1,9 +1,32 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
Centered content column
|
|
3
|
-
|
|
2
|
+
Centered content column.
|
|
3
|
+
|
|
4
|
+
`width` picks the measure (ADR 008, finding 3):
|
|
5
|
+
app — max-w-4xl, the logged-in app/admin measure (design-system.md → Layout). Default.
|
|
6
|
+
site — 1240px, the marketing measure from the approved storefront mockup.
|
|
7
|
+
|
|
8
|
+
`padded` controls the vertical rhythm (pt-10 pb-16). It suits a page body, so it
|
|
9
|
+
is on by default; header/footer rows want none, so pass `:padded="false"`.
|
|
10
|
+
The background stays full-bleed behind the column either way.
|
|
4
11
|
-->
|
|
5
12
|
<template>
|
|
6
|
-
<div class="
|
|
13
|
+
<div :class="classes">
|
|
7
14
|
<slot />
|
|
8
15
|
</div>
|
|
9
16
|
</template>
|
|
17
|
+
|
|
18
|
+
<script setup lang="ts">
|
|
19
|
+
const props = withDefaults(defineProps<{
|
|
20
|
+
width?: 'app' | 'site'
|
|
21
|
+
padded?: boolean
|
|
22
|
+
}>(), {
|
|
23
|
+
width: 'app',
|
|
24
|
+
padded: true,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const classes = computed(() => [
|
|
28
|
+
'mx-auto',
|
|
29
|
+
props.width === 'site' ? 'max-w-[1240px] px-6 md:px-10' : 'max-w-4xl px-6 sm:px-10',
|
|
30
|
+
props.padded ? 'pt-10 pb-16' : '',
|
|
31
|
+
])
|
|
32
|
+
</script>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Marketing/public site footer (ADR 008, finding 2). Frame only: top border,
|
|
3
|
+
site-width column, brand block over a wrapping link row. The app supplies the
|
|
4
|
+
content; the legal routes and their labels stay with the app.
|
|
5
|
+
|
|
6
|
+
#brand — logo + wordmark (+ tagline)
|
|
7
|
+
#links — inline links, muted
|
|
8
|
+
#actions — pushed to the right on ≥sm, e.g. a language toggle
|
|
9
|
+
|
|
10
|
+
The link row renders only when #links or #actions is filled.
|
|
11
|
+
-->
|
|
12
|
+
<template>
|
|
13
|
+
<footer class="border-t border-muted pt-9 pb-12">
|
|
14
|
+
<TspContainer
|
|
15
|
+
width="site"
|
|
16
|
+
:padded="false"
|
|
17
|
+
>
|
|
18
|
+
<div
|
|
19
|
+
v-if="$slots.brand"
|
|
20
|
+
class="flex items-center gap-3"
|
|
21
|
+
>
|
|
22
|
+
<slot name="brand" />
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div
|
|
26
|
+
v-if="$slots.links || $slots.actions"
|
|
27
|
+
class="mt-6 flex flex-wrap items-center gap-x-6 gap-y-2.5 text-sm text-muted"
|
|
28
|
+
>
|
|
29
|
+
<slot name="links" />
|
|
30
|
+
|
|
31
|
+
<div
|
|
32
|
+
v-if="$slots.actions"
|
|
33
|
+
class="sm:ml-auto"
|
|
34
|
+
>
|
|
35
|
+
<slot name="actions" />
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</TspContainer>
|
|
39
|
+
</footer>
|
|
40
|
+
</template>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Marketing/public site header (ADR 008, finding 2 — the counterpart to
|
|
3
|
+
TspSidebar, which only fits the logged-in shell). Frame only: sticky bar,
|
|
4
|
+
bottom border, translucent backdrop, site-width column. The app supplies the
|
|
5
|
+
content via slots; no branding, nav or copy lives here.
|
|
6
|
+
|
|
7
|
+
#brand — wordmark / logo link (left)
|
|
8
|
+
#nav — inline links; hidden below 900px, so pair it with your own
|
|
9
|
+
mobile disclosure if you need one
|
|
10
|
+
#actions — right-hand cluster, e.g. <TspThemeToggle> + a CTA
|
|
11
|
+
|
|
12
|
+
Pass `:sticky="false"` for a header that scrolls away.
|
|
13
|
+
-->
|
|
14
|
+
<template>
|
|
15
|
+
<header :class="['border-b border-muted bg-default/90 backdrop-blur', sticky ? 'sticky top-0 z-50' : '']">
|
|
16
|
+
<TspContainer
|
|
17
|
+
width="site"
|
|
18
|
+
:padded="false"
|
|
19
|
+
class="flex h-18 items-center"
|
|
20
|
+
>
|
|
21
|
+
<slot name="brand" />
|
|
22
|
+
|
|
23
|
+
<nav
|
|
24
|
+
v-if="$slots.nav"
|
|
25
|
+
class="ml-2 hidden gap-6 min-[900px]:flex"
|
|
26
|
+
>
|
|
27
|
+
<slot name="nav" />
|
|
28
|
+
</nav>
|
|
29
|
+
|
|
30
|
+
<div
|
|
31
|
+
v-if="$slots.actions"
|
|
32
|
+
class="ml-auto flex items-center gap-2.5"
|
|
33
|
+
>
|
|
34
|
+
<slot name="actions" />
|
|
35
|
+
</div>
|
|
36
|
+
</TspContainer>
|
|
37
|
+
</header>
|
|
38
|
+
</template>
|
|
39
|
+
|
|
40
|
+
<script setup lang="ts">
|
|
41
|
+
withDefaults(defineProps<{
|
|
42
|
+
sticky?: boolean
|
|
43
|
+
}>(), {
|
|
44
|
+
sticky: true,
|
|
45
|
+
})
|
|
46
|
+
</script>
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
Light/dark toggle. Flips the remembered preference (useTspColorMode); the
|
|
3
3
|
colour-mode plugin applies it. Default content is an icon + label; override via
|
|
4
|
-
the default slot (receives `{ isDark }`). The button is unstyled-ish so it
|
|
5
|
-
the sidebar footer or a top bar — pass classes to taste.
|
|
4
|
+
the default slot (receives `{ isDark, label }`). The button is unstyled-ish so it
|
|
5
|
+
fits the sidebar footer or a top bar — pass classes to taste.
|
|
6
|
+
|
|
7
|
+
All four strings are props so a bilingual site can pass its own (ADR 008,
|
|
8
|
+
finding 4). Naming is by *destination*: `…ToLight` is what the button shows and
|
|
9
|
+
announces while dark, because pressing it switches to light.
|
|
10
|
+
|
|
11
|
+
<TspThemeToggle
|
|
12
|
+
:label-to-light="t('theme.light')"
|
|
13
|
+
:aria-label-to-light="t('theme.toLight')"
|
|
14
|
+
… />
|
|
6
15
|
-->
|
|
7
16
|
<template>
|
|
8
17
|
<button
|
|
9
18
|
type="button"
|
|
10
19
|
data-testid="tsp-theme-toggle"
|
|
11
|
-
:aria-label="isDark ?
|
|
20
|
+
:aria-label="isDark ? ariaLabelToLight : ariaLabelToDark"
|
|
12
21
|
class="flex items-center gap-2 px-1 py-1.5 rounded-md text-xs text-muted hover:text-highlighted hover:bg-default"
|
|
13
22
|
@click="toggle"
|
|
14
23
|
>
|
|
@@ -16,11 +25,31 @@
|
|
|
16
25
|
:name="isDark ? 'i-tabler-sun' : 'i-tabler-moon'"
|
|
17
26
|
class="text-base"
|
|
18
27
|
/>
|
|
19
|
-
<slot
|
|
28
|
+
<slot
|
|
29
|
+
:is-dark="isDark"
|
|
30
|
+
:label="label"
|
|
31
|
+
>{{ label }}</slot>
|
|
20
32
|
</button>
|
|
21
33
|
</template>
|
|
22
34
|
|
|
23
35
|
<script setup lang="ts">
|
|
36
|
+
const props = withDefaults(defineProps<{
|
|
37
|
+
/** Visible label while dark (pressing switches to light). */
|
|
38
|
+
labelToLight?: string
|
|
39
|
+
/** Visible label while light. */
|
|
40
|
+
labelToDark?: string
|
|
41
|
+
/** aria-label while dark. */
|
|
42
|
+
ariaLabelToLight?: string
|
|
43
|
+
/** aria-label while light. */
|
|
44
|
+
ariaLabelToDark?: string
|
|
45
|
+
}>(), {
|
|
46
|
+
labelToLight: 'Light mode',
|
|
47
|
+
labelToDark: 'Dark mode',
|
|
48
|
+
ariaLabelToLight: 'Switch to light mode',
|
|
49
|
+
ariaLabelToDark: 'Switch to dark mode',
|
|
50
|
+
})
|
|
51
|
+
|
|
24
52
|
const { pref, toggle } = useTspColorMode()
|
|
25
53
|
const isDark = computed(() => pref.value === 'dark')
|
|
54
|
+
const label = computed(() => isDark.value ? props.labelToLight : props.labelToDark)
|
|
26
55
|
</script>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thespielplatz/tsp-tools-theme",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "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.",
|
|
5
5
|
"author": "inf0matics",
|
|
6
6
|
"license": "MIT",
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
"dev:scoped": "nuxi dev scoped",
|
|
48
48
|
"build:playground": "nuxi build playground",
|
|
49
49
|
"build:scoped": "nuxi build scoped",
|
|
50
|
-
"
|
|
51
|
-
"
|
|
50
|
+
"typecheck": "nuxi typecheck playground && nuxi typecheck scoped",
|
|
51
|
+
"release": "npm run lint && npm run typecheck && npm publish"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@nuxt/ui": "^4.10.0"
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@iconify-json/tabler": "^1.2.37",
|
|
67
67
|
"@nuxt/eslint-config": "^1.16.0",
|
|
68
|
-
"
|
|
68
|
+
"@types/node": "^26.1.2",
|
|
69
69
|
"eslint": "^10.8.0",
|
|
70
70
|
"nuxt": "^4.5.1",
|
|
71
71
|
"vue-tsc": "3.3.8"
|
|
@@ -35,7 +35,13 @@ export default defineNuxtPlugin((nuxtApp) => {
|
|
|
35
35
|
const apply = (path: string) => {
|
|
36
36
|
const mode = modeForPath(path)
|
|
37
37
|
colorMode.preference = mode
|
|
38
|
-
|
|
38
|
+
// colour-mode v4 types `value` as readonly, but still mutates it internally
|
|
39
|
+
// (it is a plain `reactive`, not a computed). We assign it deliberately: its
|
|
40
|
+
// client plugin derives `value` from `preference` only on the next tick, and
|
|
41
|
+
// its *server* plugin never derives it at all — so without this, `value`
|
|
42
|
+
// would fall back to colour-mode's own cookie during SSR and disagree with
|
|
43
|
+
// the route-derived mode in scoped setups.
|
|
44
|
+
;(colorMode as { value: string }).value = mode
|
|
39
45
|
if (import.meta.client) {
|
|
40
46
|
const el = document.documentElement
|
|
41
47
|
el.classList.remove('dark', 'light')
|