@thespielplatz/tsp-tools-theme 0.2.1 → 0.3.1
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/CHANGELOG.md +144 -0
- package/README.md +148 -44
- package/assets/css/theme.css +86 -6
- package/components/TspBanner.vue +97 -0
- package/components/TspBrandTile.vue +46 -0
- package/components/TspCodeBlock.vue +68 -0
- package/components/TspContainer.vue +10 -2
- package/components/TspCopyButton.vue +40 -0
- package/components/TspCopyField.vue +102 -0
- package/components/TspFooterRow.vue +56 -0
- package/components/TspLanguageToggle.vue +75 -0
- package/components/TspNavBadge.vue +36 -0
- package/components/TspNavGroup.vue +58 -0
- package/components/TspNavItem.vue +68 -12
- package/components/TspNavLabel.vue +56 -0
- package/components/TspNavSection.vue +33 -0
- package/components/TspNavSubItem.vue +35 -0
- package/components/TspPageTabs.vue +46 -0
- package/components/TspReleaseNotes.vue +143 -0
- package/components/TspSectionCard.vue +52 -0
- package/components/TspSidebar.vue +96 -3
- package/components/TspSidebarFooter.vue +332 -37
- package/components/TspSiteFooter.vue +47 -16
- package/components/TspSiteHeader.vue +18 -2
- package/components/TspThemeToggle.vue +24 -5
- package/components/TspToolOf.vue +42 -0
- package/components/TspTopBar.vue +69 -0
- package/components/TspUserCard.vue +127 -0
- package/components/TspVersionBadge.vue +49 -0
- package/components/TspWordmark.vue +36 -4
- package/composables/useTspCopy.ts +72 -0
- package/composables/useTspNavDrawer.ts +14 -0
- package/composables/useTspReveal.ts +18 -0
- package/docs/api.md +394 -0
- package/docs/design-system.md +297 -0
- package/nuxt.config.ts +11 -0
- package/package.json +24 -13
- package/page-meta.d.ts +11 -0
- package/plugins/01.tspColorMode.ts +12 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
# tsp.tools Design System
|
|
2
|
+
|
|
3
|
+
**This file is the source of truth for how tsp.tools services look.**
|
|
4
|
+
|
|
5
|
+
It lives in the package that ships the design, not beside it. Values here are read from
|
|
6
|
+
`assets/css/theme.css` and the components in `components/`; if the two ever disagree, the code is
|
|
7
|
+
right and this file is a bug. The planning project's `architecture/design-system.md` is a pointer
|
|
8
|
+
to this document, not a second copy — one description of the design, in the repo that implements
|
|
9
|
+
it (ADR 008, ADR 017).
|
|
10
|
+
|
|
11
|
+
The **playground is the reference**, never a mockup. `npm run dev` and look at it.
|
|
12
|
+
|
|
13
|
+
Last reconciled with the code: 2026-09-22, theme `0.3.0`.
|
|
14
|
+
|
|
15
|
+
**The complete prop, slot and emit surface is [`api.md`](api.md)** — generated from the
|
|
16
|
+
components, checked in the gate, so it cannot drift. This file is the *why*; that one is the
|
|
17
|
+
signature.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Stack and installation
|
|
22
|
+
|
|
23
|
+
Nuxt 4 · Nuxt UI 4 · Tailwind 4. The package is a Nuxt **layer**: it ships the tokens, the fonts,
|
|
24
|
+
the colour-mode plugin and the shell components pre-configured. Extend it; do not restate tokens.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm i -D @thespielplatz/tsp-tools-theme
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
export default defineNuxtConfig({
|
|
32
|
+
extends: ['@thespielplatz/tsp-tools-theme'],
|
|
33
|
+
runtimeConfig: { public: { tspTheme: { apply: 'global' } } },
|
|
34
|
+
})
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Two consumption modes
|
|
38
|
+
|
|
39
|
+
| Mode | What is themed | Used by |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| `global` | the whole app; the plugin puts `.tsp-theme` on `<html>` | most services |
|
|
42
|
+
| `scoped` | only routes under `pathPrefix`, wrapped in `<TspThemeProvider>`; the rest stays default Nuxt UI | piggybank (`/admin`) |
|
|
43
|
+
|
|
44
|
+
**Anything that touches the scope class must be checked in both.** A change verified only
|
|
45
|
+
globally is half verified — `playground/` is the global demo, `scoped/` the scoped one, and the
|
|
46
|
+
E2E runs against both.
|
|
47
|
+
|
|
48
|
+
`primary` is deliberately **not** set in `app.config`. `app.config` is global, so it would make
|
|
49
|
+
every Nuxt UI component amber even in a scoped consumer's unthemed area. Brand theming comes from
|
|
50
|
+
the `.tsp-theme` CSS scope, which resolves per element.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Tokens
|
|
55
|
+
|
|
56
|
+
### Brand
|
|
57
|
+
|
|
58
|
+
`--color-tsp-amber-50 … 950`. The two that matter:
|
|
59
|
+
|
|
60
|
+
| Token | Value | Use |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| `--color-tsp-amber-400` | `#fbad18` | the brand. Fills, and text on dark. |
|
|
63
|
+
| `--color-tsp-amber-600` | `#f47920` | the same brand as **text on light**, where `#fbad18` fails contrast. |
|
|
64
|
+
|
|
65
|
+
`--ui-primary` is amber-400 in both modes, because it paints **fills**, which are dark-text-on-
|
|
66
|
+
amber either way. For amber **text** use `--tsp-text-brand` / `.tsp-text-brand`, which steps down
|
|
67
|
+
to amber-600 on light. Getting this wrong is the single most repeated mistake in this codebase.
|
|
68
|
+
|
|
69
|
+
### Surfaces — three rungs, and they are not interchangeable
|
|
70
|
+
|
|
71
|
+
| Token | Dark | Light | Means |
|
|
72
|
+
|---|---|---|---|
|
|
73
|
+
| `--ui-bg` | `#212529` | `#f1f3f5` | the page |
|
|
74
|
+
| `--ui-bg-muted` | `#2b3035` | `#f8f9fa` | **the shell** — sidebar, top bar |
|
|
75
|
+
| `--ui-bg-elevated` | `#31373e` | `#ffffff` | **raised content** — cards, the user card |
|
|
76
|
+
| `--ui-bg-accented` | `#404041` | `#dee2e6` | pressed/selected chrome |
|
|
77
|
+
|
|
78
|
+
`muted` and `elevated` were once the same value, which left a card no surface of its own: it
|
|
79
|
+
matched either the page or the sidebar, and it matched the sidebar in stash, gage and trips. They
|
|
80
|
+
are separate rungs now. **The shell uses `muted`; content uses `elevated`.**
|
|
81
|
+
|
|
82
|
+
### Text
|
|
83
|
+
|
|
84
|
+
| Token | Dark | Light |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| `--ui-text-highlighted` | `#ffffff` | `#16191c` |
|
|
87
|
+
| `--ui-text` | `#f0f0f0` | `#212529` |
|
|
88
|
+
| `--ui-text-toned` | `#ced4da` | `#343a40` |
|
|
89
|
+
| `--ui-text-muted` | `#adb5bd` | `#6d6e70` |
|
|
90
|
+
| `--ui-text-dimmed` | `#6d6e70` | `#9ca3af` |
|
|
91
|
+
|
|
92
|
+
### Borders
|
|
93
|
+
|
|
94
|
+
`--ui-border` `#404041` / `#dee2e6` · `--ui-border-muted` `#343a40` / `#e9ecef` ·
|
|
95
|
+
`--ui-border-accented` `#404041` / `#ced4da`.
|
|
96
|
+
|
|
97
|
+
### Radius
|
|
98
|
+
|
|
99
|
+
`--ui-radius` is **`0.25rem`**. Nuxt UI derives its whole scale from it:
|
|
100
|
+
|
|
101
|
+
| Class | Multiple | Px | Used for |
|
|
102
|
+
|---|---|---|---|
|
|
103
|
+
| `rounded-sm` | 1× | 4 | inner chips |
|
|
104
|
+
| `rounded-md` | 1.5× | 6 | rows, buttons, inputs, toggles |
|
|
105
|
+
| `rounded-lg` | 2× | 8 | Nuxt UI's own default |
|
|
106
|
+
| `rounded-xl` | 3× | 12 | **cards** |
|
|
107
|
+
|
|
108
|
+
**The 12px card is a rule each app applies, not a layer default.** `UCard` hardcodes
|
|
109
|
+
`rounded-lg`, so a plain card is 8px — write `rounded-xl` on cards you want at 12px. The layer
|
|
110
|
+
does not override `ui.card.slots.root`, for the same reason it does not set `primary`: it is
|
|
111
|
+
global and would reach a scoped consumer's unthemed area. There is no `TspCard`.
|
|
112
|
+
|
|
113
|
+
> Nuxt UI runs **tailwind-merge** over the `class` prop, so `rounded-xl` *replaces*
|
|
114
|
+
> `rounded-lg` rather than sitting beside it. Do not select on the default class in tests.
|
|
115
|
+
|
|
116
|
+
### Fonts
|
|
117
|
+
|
|
118
|
+
Nunito everywhere; Space Grotesk for the wordmark only (`.tsp-wordmark` / `<TspWordmark>`).
|
|
119
|
+
Both from Google via `@nuxt/fonts`, which ships with Nuxt UI.
|
|
120
|
+
|
|
121
|
+
### Icons
|
|
122
|
+
|
|
123
|
+
Tabler, `i-tabler-*`. Install `@iconify-json/tabler` for offline resolution. **One icon family
|
|
124
|
+
across the platform** — do not mix in a second set.
|
|
125
|
+
|
|
126
|
+
### Utilities the layer adds
|
|
127
|
+
|
|
128
|
+
| Class | What |
|
|
129
|
+
|---|---|
|
|
130
|
+
| `.tsp-theme` | the scope class; everything above resolves inside it |
|
|
131
|
+
| `.tsp-text-brand` | mode-correct amber **text** |
|
|
132
|
+
| `.tsp-border-brand` | mode-correct amber **left border** |
|
|
133
|
+
| `.tsp-scroll-quiet` | scrolls, no visible scrollbar track |
|
|
134
|
+
| `.tsp-wordmark` | Space Grotesk |
|
|
135
|
+
|
|
136
|
+
`.tsp-text-brand` is written **unlayered** so it beats Tailwind's `text-*` utilities without
|
|
137
|
+
`!important`.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Layout
|
|
142
|
+
|
|
143
|
+
`<TspContainer>` centres content. `width="app"` (default) is `max-w-6xl` (1152px), `px-6 sm:px-10`;
|
|
144
|
+
`width="site"` is `max-w-[1240px]`, `px-6 md:px-10`. Both add `pt-10 pb-16` unless
|
|
145
|
+
`:padded="false"`. The background stays full-bleed; only the column is constrained.
|
|
146
|
+
|
|
147
|
+
**Components never cap the page width.** A layer component that puts a `max-w-*` on its own root
|
|
148
|
+
overrides the container from the inside, and the app cannot undo it without fighting
|
|
149
|
+
`tailwind-merge`. Width is the container's job; a service that wants a block narrower wraps it.
|
|
150
|
+
|
|
151
|
+
The app measure is **not** a reading measure — it is sized for the tables, card grids and settings
|
|
152
|
+
panels an app page is actually made of. Cap prose where the prose is (`max-w-2xl` on the
|
|
153
|
+
paragraph), not by narrowing the whole page around it.
|
|
154
|
+
|
|
155
|
+
**The scrollbar gutter is always reserved** (`html.tsp-theme, html:has(.tsp-theme)`). The column
|
|
156
|
+
is centred, so without it the whole page slides sideways by half a scrollbar the moment a route
|
|
157
|
+
is tall enough to scroll. Browsers with overlay scrollbars are unaffected, and a consumer's
|
|
158
|
+
unthemed public pages are deliberately not touched.
|
|
159
|
+
|
|
160
|
+
**Responsive.** `sm` (640px) is the shell's breakpoint. At and above it the sidebar is a column,
|
|
161
|
+
sticky at viewport height. Below it the sidebar becomes an off-canvas drawer and `<TspTopBar>`
|
|
162
|
+
appears.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Components
|
|
167
|
+
|
|
168
|
+
All auto-imported. App-specific content arrives as **props**, not slots, for the rows every app
|
|
169
|
+
has — a slot invites each app to restyle its copy, which is the drift the layer exists to stop.
|
|
170
|
+
|
|
171
|
+
### The logged-in shell
|
|
172
|
+
|
|
173
|
+
| Component | Purpose |
|
|
174
|
+
|---|---|
|
|
175
|
+
| `TspSidebar` | The frame. Slots `#brand` `#nav` `#footer`. Sticky on desktop, drawer below `sm`. |
|
|
176
|
+
| `TspTopBar` | Mobile only: burger (left) · brand · page title. |
|
|
177
|
+
| `TspSidebarFooter` | User → Settings → Logout → divider → theme + language → meta icons + version → attribution. |
|
|
178
|
+
| `TspFooterRow` | One footer row; `to` / `href` / neither (button). |
|
|
179
|
+
| `TspNavItem` | Nav row. White at rest, amber when active. `to` or `href`, optional `badge`. |
|
|
180
|
+
| `TspNavGroup` | A nav row with always-visible children on a hairline spine. |
|
|
181
|
+
| `TspNavSubItem` | A child row — muted, own icon. |
|
|
182
|
+
| `TspNavSection` | A rule, optionally labelled. |
|
|
183
|
+
| `TspNavBadge` | Outline pill; `tone` `primary` \| `neutral`. |
|
|
184
|
+
| `TspNavLabel` | Truncates a label, reveals it in full on hover. Used by the rows above. |
|
|
185
|
+
| `TspVersionBadge` | Version pill with a tag icon. A link only with `href`; `unread` marks unseen notes. |
|
|
186
|
+
| `TspLanguageToggle` | DE/EN segmented control. |
|
|
187
|
+
| `TspThemeToggle` | Light/dark. `icon-only` for tight places. |
|
|
188
|
+
| `TspToolOf` | “A tool of tsp.tools”. Shared by both footers. |
|
|
189
|
+
|
|
190
|
+
### The public shell
|
|
191
|
+
|
|
192
|
+
| Component | Purpose |
|
|
193
|
+
|---|---|
|
|
194
|
+
| `TspSiteHeader` | Slots `#brand` `#nav` `#actions`; `sticky`, and `bare` for a landing page (no bar). |
|
|
195
|
+
| `TspSiteFooter` | Compact centred link row; `tool-of` adds the attribution. |
|
|
196
|
+
|
|
197
|
+
### Content
|
|
198
|
+
|
|
199
|
+
| Component | Purpose |
|
|
200
|
+
|---|---|
|
|
201
|
+
| `TspContainer` | The measure. |
|
|
202
|
+
| `TspSectionCard` | Icon + title over content, on `bg-elevated`. |
|
|
203
|
+
| `TspPageTabs` | Area tabs as **routes**, so the back button works. |
|
|
204
|
+
| `TspCopyField` | Read-only value + copy; `secret` masks behind a reveal. |
|
|
205
|
+
| `TspCopyButton` | Copy with a tick confirmation. |
|
|
206
|
+
| `TspCodeBlock` | Config to copy. Separate shown and copied values. |
|
|
207
|
+
| `TspUserCard` | Account card: name, masked id, manage link. |
|
|
208
|
+
| `TspBanner` | Dismissible strip. Emits `dismiss`; remembering it is the app's job. |
|
|
209
|
+
| `TspReleaseNotes` | Release notes. Props mirror the `tsp-release` skill's `RELEASES` shape. |
|
|
210
|
+
|
|
211
|
+
### Brand
|
|
212
|
+
|
|
213
|
+
| Component | Purpose |
|
|
214
|
+
|---|---|
|
|
215
|
+
| `TspWordmark` | `<TspWordmark name="trips" />` → white `trips` + amber dot. No slot. |
|
|
216
|
+
| `TspBrandTile` | The frame around a mark: `sidebar` (24px) or `hero` (64/96px). Paints no fill. |
|
|
217
|
+
| `TspThemeProvider` | Scoped mode: marks a sub-tree as themed. |
|
|
218
|
+
|
|
219
|
+
### Composables
|
|
220
|
+
|
|
221
|
+
All four are auto-imported; signatures and return shapes in [`api.md`](api.md).
|
|
222
|
+
|
|
223
|
+
| Composable | For |
|
|
224
|
+
|---|---|
|
|
225
|
+
| `useTspColorMode()` | the themed area's own light/dark preference, in a cookie so SSR can read it |
|
|
226
|
+
| `useTspNavDrawer()` | the mobile drawer's shared open state (`useState`, not a module ref, which would leak across requests on the server) |
|
|
227
|
+
| `useTspCopy()` | copy + a moment of confirmation, with the fallback for a non-secure context (plain HTTP on a LAN address — how a service gets tested on a phone) |
|
|
228
|
+
| `useTspReveal()` | show/hide a secret and the fixed-length placeholder to render instead of it |
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Rules
|
|
233
|
+
|
|
234
|
+
**The wordmark is the app name, lowercase, terminated by an amber dot.** `trips.` `gage.`
|
|
235
|
+
`stash.` The component owns the dot; there is no slot. `tsp.tools` is the *domain of the tool
|
|
236
|
+
set*, not a tool's name, so it is never a wordmark subject.
|
|
237
|
+
|
|
238
|
+
**Amber fills mean exactly one thing: primary buttons.** Nothing in the sidebar carries an amber
|
|
239
|
+
fill at rest — not the active nav item, not a role badge, not the language toggle. The E2E sweeps
|
|
240
|
+
for it on every run. The brand tile is the one deliberate exception, and it is SVG artwork rather
|
|
241
|
+
than a CSS background.
|
|
242
|
+
|
|
243
|
+
**The nav reads in three tones:** top-level rows white, children muted, the current route amber —
|
|
244
|
+
at either level.
|
|
245
|
+
|
|
246
|
+
**No cards inside cards.** A bordered, filled surface does not contain another. Use spacing, a
|
|
247
|
+
rule, or a heading.
|
|
248
|
+
|
|
249
|
+
**Every visible string is a prop with an English default.** No component hardcodes copy. Where a
|
|
250
|
+
sentence contains a link, only the leading words are translatable and the component owns the
|
|
251
|
+
link — an app must never hand-roll markup to translate a sentence.
|
|
252
|
+
|
|
253
|
+
**Masking is real, not cosmetic.** A hidden secret is *not rendered*. Covering it with CSS leaves
|
|
254
|
+
it in the page source, copyable and readable by anything walking the DOM.
|
|
255
|
+
|
|
256
|
+
**Open source is derived from `github-link`**, not a separate flag: the link is the consequence
|
|
257
|
+
of the fact, and a second source of truth could disagree with the first.
|
|
258
|
+
|
|
259
|
+
**Area tabs** are for one area's own views; a **nav group** is for a service with nested areas.
|
|
260
|
+
Both use `.tsp-text-brand` when active.
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## Gates
|
|
265
|
+
|
|
266
|
+
| Command | Covers |
|
|
267
|
+
|---|---|
|
|
268
|
+
| `npm run lint` | eslint |
|
|
269
|
+
| `npm run docs:api:check` | `docs/api.md` still matches the components |
|
|
270
|
+
| `npm run typecheck` | both demo apps (`nuxi typecheck`, i.e. vue-tsc — `tsc --noEmit` cannot resolve SFCs) |
|
|
271
|
+
| `npm run test:icons` | boots both built apps and fails on one `failed to load icon` |
|
|
272
|
+
| `npm run test:e2e` | Playwright, both consumption modes |
|
|
273
|
+
| `npm test` | icons + e2e |
|
|
274
|
+
| `npm run gate` | all of the above, in the order CI runs them |
|
|
275
|
+
|
|
276
|
+
`.github/workflows/ci.yml` runs the gate on every push and pull request, building both demo apps
|
|
277
|
+
once instead of the four times the scripts would do back to back. A version tag additionally runs
|
|
278
|
+
`release.yml`, which re-runs the gate on a clean checkout and publishes to npm with provenance.
|
|
279
|
+
|
|
280
|
+
**A green pipeline does not prove icons render.** Icons resolve at runtime, per request, so lint,
|
|
281
|
+
typecheck and build all pass with every glyph missing. `test:icons` is part of the gate, not
|
|
282
|
+
optional (ADR 008).
|
|
283
|
+
|
|
284
|
+
The same trap has bitten twice more: `resolveComponent()` in a template expression renders a
|
|
285
|
+
literal `<NuxtLink>` element with the build still green, and a Tailwind class collapse only shows
|
|
286
|
+
in the browser. **Measure the rendered page**, which is what the E2E does.
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## Decisions
|
|
291
|
+
|
|
292
|
+
In the planning project's `decisions/`:
|
|
293
|
+
|
|
294
|
+
- **ADR 008** — why this is a layer; the scoped/global mechanism; the icon trap
|
|
295
|
+
- **ADR 017** — one shell for every service; the layer is the source
|
|
296
|
+
- **ADR 018** (+ its 2026-09-21 and 2026-09-22 amendments) — the quiet shell, what `0.3.0`
|
|
297
|
+
revised, and the content measure
|
package/nuxt.config.ts
CHANGED
|
@@ -9,6 +9,17 @@ const layerDir = dirname(fileURLToPath(import.meta.url))
|
|
|
9
9
|
export default defineNuxtConfig({
|
|
10
10
|
modules: ['@nuxt/ui'],
|
|
11
11
|
|
|
12
|
+
// Register the layer's own type augmentation with the CONSUMER's TS program.
|
|
13
|
+
// Shipping the .d.ts in `files` is not enough: a consumer resolves this layer
|
|
14
|
+
// inside node_modules, and none of Nuxt's generated tsconfig include globs
|
|
15
|
+
// (`../*.d.ts`, `../../*.d.ts`, `../layers/*/*.d.ts`) reach there. It only
|
|
16
|
+
// typechecked here because the demo apps extend '..' by relative path.
|
|
17
|
+
hooks: {
|
|
18
|
+
'prepare:types': ({ references }) => {
|
|
19
|
+
references.push({ path: join(layerDir, 'page-meta.d.ts') })
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
|
|
12
23
|
// The token stylesheet (imports tailwindcss + @nuxt/ui itself, so a consumer
|
|
13
24
|
// does not add its own framework import — see README "CSS ownership").
|
|
14
25
|
css: [join(layerDir, './assets/css/theme.css')],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thespielplatz/tsp-tools-theme",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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",
|
|
@@ -37,23 +37,32 @@
|
|
|
37
37
|
"assets/css",
|
|
38
38
|
"components",
|
|
39
39
|
"composables",
|
|
40
|
+
"page-meta.d.ts",
|
|
40
41
|
"plugins",
|
|
42
|
+
"docs",
|
|
41
43
|
"README.md",
|
|
44
|
+
"CHANGELOG.md",
|
|
42
45
|
"LICENSE"
|
|
43
46
|
],
|
|
44
47
|
"scripts": {
|
|
45
48
|
"lint": "eslint .",
|
|
46
|
-
"
|
|
47
|
-
"
|
|
49
|
+
"docs:api": "node scripts/gen-api.mjs",
|
|
50
|
+
"docs:api:check": "node scripts/gen-api.mjs --check",
|
|
51
|
+
"dev": "node scripts/dev.mjs playground",
|
|
52
|
+
"dev:scoped": "node scripts/dev.mjs scoped",
|
|
48
53
|
"build:playground": "nuxi build playground",
|
|
49
54
|
"build:scoped": "nuxi build scoped",
|
|
50
55
|
"typecheck": "nuxi typecheck playground && nuxi typecheck scoped",
|
|
51
|
-
"test:icons": "npm run build:playground && node scripts/check-icons.mjs",
|
|
52
|
-
"
|
|
56
|
+
"test:icons": "npm run build:playground && npm run build:scoped && node scripts/check-icons.mjs",
|
|
57
|
+
"gate": "npm run lint && npm run docs:api:check && npm run typecheck && npm run test:icons && npm run test:e2e",
|
|
58
|
+
"bump-version": "npm run gate && node scripts/bump-version.mjs && git push --follow-tags",
|
|
59
|
+
"release": "npm run gate && npm publish",
|
|
60
|
+
"test:e2e": "playwright test",
|
|
61
|
+
"test": "npm run test:icons && npm run test:e2e"
|
|
53
62
|
},
|
|
54
63
|
"dependencies": {
|
|
55
|
-
"@nuxt/icon": "~2.
|
|
56
|
-
"@nuxt/ui": "^4.
|
|
64
|
+
"@nuxt/icon": "~2.5.1",
|
|
65
|
+
"@nuxt/ui": "^4.11.1"
|
|
57
66
|
},
|
|
58
67
|
"peerDependencies": {
|
|
59
68
|
"@iconify-json/tabler": "^1.2.0",
|
|
@@ -65,11 +74,13 @@
|
|
|
65
74
|
}
|
|
66
75
|
},
|
|
67
76
|
"devDependencies": {
|
|
68
|
-
"@iconify-json/tabler": "^1.2.
|
|
69
|
-
"@nuxt/eslint-config": "^1.
|
|
70
|
-
"@
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
77
|
+
"@iconify-json/tabler": "^1.2.38",
|
|
78
|
+
"@nuxt/eslint-config": "^1.17.0",
|
|
79
|
+
"@playwright/test": "^1.63.0",
|
|
80
|
+
"@types/node": "^26.6.2",
|
|
81
|
+
"changelogen": "^0.6.2",
|
|
82
|
+
"eslint": "^10.11.0",
|
|
83
|
+
"nuxt": "^4.5.2",
|
|
84
|
+
"vue-tsc": "3.3.11"
|
|
74
85
|
}
|
|
75
86
|
}
|
package/page-meta.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// `definePageMeta({ title })` is how a page tells <TspTopBar> what to show on
|
|
2
|
+
// mobile, where the sidebar is hidden and nothing else says where you are.
|
|
3
|
+
// Declared here so it typechecks in consumers, not only in this repo.
|
|
4
|
+
declare module '#app' {
|
|
5
|
+
interface PageMeta {
|
|
6
|
+
/** Shown by TspTopBar when no `title` prop is passed. */
|
|
7
|
+
title?: string
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export {}
|
|
@@ -26,12 +26,24 @@ export default defineNuxtPlugin((nuxtApp) => {
|
|
|
26
26
|
useHead({ htmlAttrs: { class: 'tsp-theme' } })
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
|
|
29
30
|
const isThemed = (path: string) =>
|
|
30
31
|
options.apply === 'global' || path.startsWith(options.pathPrefix)
|
|
31
32
|
|
|
32
33
|
const modeForPath = (path: string): TspMode =>
|
|
33
34
|
isThemed(path) ? pref.value : options.publicMode
|
|
34
35
|
|
|
36
|
+
// ...and emit the MODE class server-side too. Without this the light/dark
|
|
37
|
+
// class was only ever written under `import.meta.client`, so the first paint
|
|
38
|
+
// was decided by colour-mode's own inline script reading its `nuxt-color-mode`
|
|
39
|
+
// localStorage key (default "dark") — not the `tsp_color_mode` cookie this
|
|
40
|
+
// layer introduced. Measured: `<html class="tsp-theme">` came back with no
|
|
41
|
+
// mode class for either cookie value, in both apps. The result was a
|
|
42
|
+
// wrong-mode flash on every cold load: a light user saw dark, and a scoped
|
|
43
|
+
// consumer's deliberately-light public area flashed dark on a first visit.
|
|
44
|
+
const ssrMode = computed(() => modeForPath(route.path))
|
|
45
|
+
useHead({ htmlAttrs: { class: () => ssrMode.value } })
|
|
46
|
+
|
|
35
47
|
const apply = (path: string) => {
|
|
36
48
|
const mode = modeForPath(path)
|
|
37
49
|
colorMode.preference = mode
|