@thespielplatz/tsp-tools-theme 0.1.3 → 0.2.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/README.md CHANGED
@@ -34,6 +34,25 @@ Peer expectations (provide them in the consuming app):
34
34
  > (A deep relative path such as `extends: ['../..']` can fail to pick up the source dirs;
35
35
  > the package name always works.)
36
36
 
37
+ ### Why `@nuxt/icon` is capped at `~2.3.1`
38
+
39
+ The layer declares `@nuxt/icon: ~2.3.1` as a direct dependency. This is a **temporary cap**
40
+ around an upstream regression, not a preference.
41
+
42
+ `@nuxt/icon` 2.4.1 changed its plugin to take `$fetch` from `useRequestFetch()` and then read
43
+ `.native` off it. During SSR that returns Nitro's request-scoped `event.$fetch`, which has no
44
+ `.native` property — so Iconify's fetch is set to `undefined` and **every server-side icon load
45
+ fails**, logging `[Icon] failed to load icon …` per request. `@nuxt/ui` only requires
46
+ `^2.3.1`, so any re-resolve of the tree silently moves consumers onto the broken version.
47
+
48
+ The cap is in `dependencies` rather than `overrides` on purpose: npm honours `overrides` only
49
+ in the root project, so an `overrides` entry here would not reach consuming apps. `2.3.1`
50
+ satisfies `@nuxt/ui`'s own range, so there is one hoisted copy and no duplication.
51
+
52
+ Remove the cap once a fixed `@nuxt/icon` ships. To check, drop the pin and look for
53
+ `failed to load icon` in the server log of a production build — the build itself stays green
54
+ either way, which is exactly why this is easy to miss.
55
+
37
56
  ---
38
57
 
39
58
  ## Global usage (whole app themed)
@@ -147,14 +166,55 @@ for the themed area.
147
166
  | `TspThemeProvider` | Scoped wrapper — marks a sub-area as themed (`.tsp-theme`). |
148
167
  | `TspSidebar` | App-shell sidebar frame; slots `#brand` `#nav` `#footer`. |
149
168
  | `TspSidebarFooter` | User → Logout → divider → theme toggle → GitHub + version. |
169
+ | `TspSiteHeader` | Public/marketing header; slots `#brand` `#nav` `#actions`, prop `sticky`. |
170
+ | `TspSiteFooter` | Public/marketing footer; slots `#brand` `#links` `#actions`. |
150
171
  | `TspThemeToggle` | Sun/moon light–dark toggle (uses `useTspColorMode`). |
151
172
  | `TspNavItem` | Sidebar nav link (muted → filled-amber when active). |
152
173
  | `TspWordmark` | Space Grotesk + amber brand wordmark. |
153
- | `TspContainer` | Centered content column (`max-w-4xl`, `px-6 sm:px-10`, `pt-10 pb-16`). |
174
+ | `TspContainer` | Centered content column; props `width` (`app` \| `site`) and `padded`. |
154
175
 
155
176
  App-specific content (branding, nav lists, user identity, GitHub/version values) is injected
156
177
  via slots/props — none of it lives in the layer.
157
178
 
179
+ ### App shell vs site shell
180
+
181
+ `TspSidebar` + `TspSidebarFooter` are the **logged-in** shell. For a public/marketing surface
182
+ use `TspSiteHeader` + `TspSiteFooter` with the wide container:
183
+
184
+ ```vue
185
+ <TspSiteHeader>
186
+ <template #brand><NuxtLink to="/"><TspWordmark>tsp<span class="text-primary">.</span>tools</TspWordmark></NuxtLink></template>
187
+ <template #nav><a href="#tools">Tools</a></template>
188
+ <template #actions><TspThemeToggle /></template>
189
+ </TspSiteHeader>
190
+
191
+ <TspContainer width="site"><NuxtPage /></TspContainer>
192
+
193
+ <TspSiteFooter>
194
+ <template #brand><TspWordmark>tsp<span class="text-primary">.</span>tools</TspWordmark></template>
195
+ <template #links><NuxtLink to="/imprint">Imprint</NuxtLink></template>
196
+ </TspSiteFooter>
197
+ ```
198
+
199
+ `TspContainer` defaults to `width="app"` (`max-w-4xl`, `px-6 sm:px-10`) — the app/admin measure.
200
+ `width="site"` is `max-w-[1240px]`, `px-6 md:px-10`, the marketing measure. Both add `pt-10 pb-16`
201
+ unless you pass `:padded="false"` (which is what the header/footer frames do internally).
202
+
203
+ ### Localising the theme toggle
204
+
205
+ `TspThemeToggle` takes all four strings as props, named by destination — `…ToLight` is what the
206
+ button shows and announces while dark:
207
+
208
+ ```vue
209
+ <TspThemeToggle
210
+ :label-to-light="t('theme.light')" :label-to-dark="t('theme.dark')"
211
+ :aria-label-to-light="t('theme.toLight')" :aria-label-to-dark="t('theme.toDark')"
212
+ />
213
+ ```
214
+
215
+ The defaults are English. The default slot receives `{ isDark, label }` if you want different
216
+ markup (an icon-only square button, say) while keeping the mechanism.
217
+
158
218
  ---
159
219
 
160
220
  ## Why `primary` isn't set in `app.config`
@@ -1,9 +1,32 @@
1
1
  <!--
2
- Centered content column (design-system.md → Layout): max-w-4xl with
3
- px-6 sm:px-10 / pt-10 pb-16. The background stays full-bleed behind it.
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="max-w-4xl mx-auto px-6 sm:px-10 pt-10 pb-16">
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 fits
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 ? 'Switch to light mode' : 'Switch to dark mode'"
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 :is-dark="isDark">{{ isDark ? 'Light mode' : 'Dark mode' }}</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.1.3",
3
+ "version": "0.2.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",
@@ -48,9 +48,11 @@
48
48
  "build:playground": "nuxi build playground",
49
49
  "build:scoped": "nuxi build scoped",
50
50
  "typecheck": "nuxi typecheck playground && nuxi typecheck scoped",
51
- "release": "npm run lint && npm run typecheck && npm publish"
51
+ "test:icons": "npm run build:playground && node scripts/check-icons.mjs",
52
+ "release": "npm run lint && npm run typecheck && npm run test:icons && npm publish"
52
53
  },
53
54
  "dependencies": {
55
+ "@nuxt/icon": "~2.3.1",
54
56
  "@nuxt/ui": "^4.10.0"
55
57
  },
56
58
  "peerDependencies": {