@thespielplatz/tsp-tools-theme 0.2.1 → 0.3.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/CHANGELOG.md +114 -0
- package/README.md +140 -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
|
@@ -1,60 +1,355 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
The sidebar bottom block, in
|
|
3
|
-
User label → #user → #logout → divider → theme toggle → GitHub + version.
|
|
2
|
+
The sidebar bottom block, in the order spec 03 fixes:
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
User label → user → settings → logout
|
|
5
|
+
───────────
|
|
6
|
+
[theme ……… DE|EN] → [github · support · tip jar ……… version] → “A tool of…”
|
|
7
|
+
|
|
8
|
+
The divider separates ACCOUNT from PREFERENCES: above it is who you are, your
|
|
9
|
+
settings and leaving; below it is how the app looks and speaks, plus the meta
|
|
10
|
+
links. All four rows share one left edge and one 32px height. Before 0.3.0 the
|
|
11
|
+
language toggle was positioned by each app (`class="mt-1 ml-3"`) and the theme
|
|
12
|
+
toggle row was text-xs/px-1 against TspFooterRow's text-sm/px-3 — spec 01
|
|
13
|
+
shipped that mismatch knowingly and left it open; this closes it.
|
|
14
|
+
|
|
15
|
+
PROPS, NOT SLOTS, for the rows every app has. A slot invites each app to
|
|
16
|
+
re-style its own copy, which is the drift TspFooterRow was created to stop.
|
|
17
|
+
`#user` survives only for apps that need more than a name.
|
|
18
|
+
|
|
19
|
+
OPEN SOURCE IS DERIVED FROM `githubLink`. Fil's framing: "I just define in a
|
|
20
|
+
project if it's open source, and then there will be a GitHub link or not." So
|
|
21
|
+
the link is the consequence of the fact, and a separate `variant` prop would
|
|
22
|
+
be a second source of truth that can disagree with it.
|
|
23
|
+
|
|
24
|
+
githubLink set → theme-toggle row, then a GitHub row whose version badge
|
|
25
|
+
links to the release.
|
|
26
|
+
githubLink unset → one row: theme toggle with a PLAIN-TEXT version badge.
|
|
27
|
+
`versionLink` is then a no-op, silently (a warning would
|
|
28
|
+
scold someone for passing both and toggling githubLink
|
|
29
|
+
per environment, which is reasonable).
|
|
30
|
+
|
|
31
|
+
The language row renders only when `locales` is passed — that is what makes it
|
|
32
|
+
absent for the services that are not bilingual.
|
|
7
33
|
-->
|
|
8
34
|
<template>
|
|
9
35
|
<div>
|
|
10
36
|
<p
|
|
11
|
-
v-if="$slots.user"
|
|
12
|
-
|
|
37
|
+
v-if="user || $slots.user"
|
|
38
|
+
data-testid="tsp-user-label"
|
|
39
|
+
class="px-3 mb-1 text-[10px] uppercase tracking-wide text-muted"
|
|
13
40
|
>
|
|
14
|
-
|
|
41
|
+
{{ userLabel }}
|
|
15
42
|
</p>
|
|
16
|
-
<slot name="user" />
|
|
17
|
-
<slot name="logout" />
|
|
18
43
|
|
|
19
|
-
<
|
|
44
|
+
<slot name="user">
|
|
45
|
+
<!-- A LINK when `userTo` is given — the user page is a real destination,
|
|
46
|
+
and the row then goes amber on it like any other active row. Without
|
|
47
|
+
it the row is plain text. The icon inherits the row colour either
|
|
48
|
+
way: the original complaint was an amber icon on a row that was
|
|
49
|
+
neither selected nor clickable. -->
|
|
50
|
+
<TspFooterRow
|
|
51
|
+
v-if="user && userTo"
|
|
52
|
+
data-footer-row="user"
|
|
53
|
+
data-testid="tsp-user"
|
|
54
|
+
:to="userTo"
|
|
55
|
+
:icon="userIcon"
|
|
56
|
+
>
|
|
57
|
+
<span class="truncate">{{ user }}</span>
|
|
58
|
+
</TspFooterRow>
|
|
59
|
+
<p
|
|
60
|
+
v-else-if="user"
|
|
61
|
+
data-footer-row="user"
|
|
62
|
+
data-testid="tsp-user"
|
|
63
|
+
:class="[ROW, 'text-muted']"
|
|
64
|
+
>
|
|
65
|
+
<UIcon
|
|
66
|
+
v-if="userIcon"
|
|
67
|
+
:name="userIcon"
|
|
68
|
+
class="text-lg shrink-0"
|
|
69
|
+
/>
|
|
70
|
+
<span class="truncate">{{ user }}</span>
|
|
71
|
+
</p>
|
|
72
|
+
</slot>
|
|
20
73
|
|
|
21
|
-
<
|
|
22
|
-
v-if="
|
|
23
|
-
|
|
74
|
+
<TspFooterRow
|
|
75
|
+
v-if="settingsTo"
|
|
76
|
+
data-footer-row="settings"
|
|
77
|
+
:to="settingsTo"
|
|
78
|
+
icon="i-tabler-settings"
|
|
24
79
|
>
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
80
|
+
{{ settingsLabel }}
|
|
81
|
+
</TspFooterRow>
|
|
82
|
+
|
|
83
|
+
<!-- The slot is kept as an escape hatch, exactly like #user. Removing it
|
|
84
|
+
was a SILENT break: Vue discards content for a slot that no longer
|
|
85
|
+
exists, so every consumer still passing #logout would have lost its
|
|
86
|
+
logout control with no error, no warning and no build failure. -->
|
|
87
|
+
<slot name="logout">
|
|
88
|
+
<TspFooterRow
|
|
89
|
+
v-if="logoutTo || logoutHref"
|
|
90
|
+
data-footer-row="logout"
|
|
91
|
+
:to="logoutTo"
|
|
92
|
+
:href="logoutHref"
|
|
93
|
+
icon="i-tabler-logout"
|
|
31
94
|
>
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
95
|
+
{{ logoutLabel }}
|
|
96
|
+
</TspFooterRow>
|
|
97
|
+
</slot>
|
|
98
|
+
|
|
99
|
+
<div class="mt-2 pt-2 border-t border-default">
|
|
100
|
+
<!-- Language sits BELOW the divider with the theme toggle: both are
|
|
101
|
+
display preferences, while everything above the line is the account
|
|
102
|
+
(who you are, your settings, leaving). -->
|
|
103
|
+
<!-- Preferences row: theme on the left with its label, language on the
|
|
104
|
+
right. The old "Sprache" text is gone — a DE/EN segmented control
|
|
105
|
+
says what it is, so the label was carrying nothing, and the space it
|
|
106
|
+
freed is what lets the theme toggle keep ITS label instead of being
|
|
107
|
+
an unexplained sun. The row itself has no padding: the toggle brings
|
|
108
|
+
its own px-3, so the icon still lands on the 12px column. -->
|
|
109
|
+
<div
|
|
110
|
+
data-footer-row="preferences"
|
|
111
|
+
class="flex items-center h-8"
|
|
41
112
|
>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
113
|
+
<TspThemeToggle
|
|
114
|
+
:label-to-light="labelToLight"
|
|
115
|
+
:label-to-dark="labelToDark"
|
|
116
|
+
:aria-label-to-light="ariaLabelToLight"
|
|
117
|
+
:aria-label-to-dark="ariaLabelToDark"
|
|
118
|
+
/>
|
|
119
|
+
<TspLanguageToggle
|
|
120
|
+
v-if="locales?.length"
|
|
121
|
+
class="ml-auto mr-3"
|
|
122
|
+
:model-value="modelValue"
|
|
123
|
+
:locales="locales"
|
|
124
|
+
:group-label="languageGroupLabel"
|
|
125
|
+
:labels="languageLabels"
|
|
126
|
+
@update:model-value="emit('update:modelValue', $event)"
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<div
|
|
131
|
+
data-testid="tsp-meta-row"
|
|
132
|
+
class="flex items-center gap-1 h-8 px-2 text-muted"
|
|
47
133
|
>
|
|
48
|
-
|
|
49
|
-
|
|
134
|
+
<a
|
|
135
|
+
v-if="githubLink"
|
|
136
|
+
data-testid="tsp-github-link"
|
|
137
|
+
:href="githubLink"
|
|
138
|
+
:aria-label="githubLabel"
|
|
139
|
+
:title="githubLabel"
|
|
140
|
+
target="_blank"
|
|
141
|
+
rel="noopener noreferrer"
|
|
142
|
+
:class="ICON"
|
|
143
|
+
>
|
|
144
|
+
<UIcon
|
|
145
|
+
name="i-tabler-brand-github"
|
|
146
|
+
class="text-lg block"
|
|
147
|
+
/>
|
|
148
|
+
</a>
|
|
149
|
+
<a
|
|
150
|
+
v-if="supportEmail"
|
|
151
|
+
data-testid="tsp-support-link"
|
|
152
|
+
:href="supportHref"
|
|
153
|
+
:aria-label="supportLabel"
|
|
154
|
+
:title="supportLabel"
|
|
155
|
+
:class="ICON"
|
|
156
|
+
>
|
|
157
|
+
<UIcon
|
|
158
|
+
name="i-tabler-mail"
|
|
159
|
+
class="text-lg block"
|
|
160
|
+
/>
|
|
161
|
+
</a>
|
|
162
|
+
<a
|
|
163
|
+
v-if="tipJar"
|
|
164
|
+
data-testid="tsp-tip-jar-link"
|
|
165
|
+
:href="tipJarLink"
|
|
166
|
+
:aria-label="tipJarLabel"
|
|
167
|
+
:title="tipJarLabel"
|
|
168
|
+
target="_blank"
|
|
169
|
+
rel="noopener noreferrer"
|
|
170
|
+
:class="ICON"
|
|
171
|
+
>
|
|
172
|
+
<UIcon
|
|
173
|
+
name="i-tabler-coin"
|
|
174
|
+
class="text-lg block"
|
|
175
|
+
/>
|
|
176
|
+
</a>
|
|
177
|
+
|
|
178
|
+
<TspVersionBadge
|
|
179
|
+
v-if="version"
|
|
180
|
+
class="ml-auto"
|
|
181
|
+
:version="version"
|
|
182
|
+
:href="githubLink ? versionLink : undefined"
|
|
183
|
+
:unread="versionUnread"
|
|
184
|
+
/>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<TspToolOf
|
|
188
|
+
v-if="toolOf"
|
|
189
|
+
class="px-3 pt-2 pb-0.5"
|
|
190
|
+
:label="toolOfLabel"
|
|
191
|
+
:name="toolOfName"
|
|
192
|
+
:link="toolOfLink"
|
|
193
|
+
/>
|
|
50
194
|
</div>
|
|
51
195
|
</div>
|
|
52
196
|
</template>
|
|
53
197
|
|
|
54
198
|
<script setup lang="ts">
|
|
55
|
-
|
|
199
|
+
// One row shape for every row in this block: 32px tall, 12px left edge, 14px
|
|
200
|
+
// text. Fixed height rather than vertical padding, because the language row
|
|
201
|
+
// holds a control and would otherwise come out taller than its neighbours.
|
|
202
|
+
const ROW = 'flex items-center gap-2.5 h-8 px-3 rounded-md text-sm font-normal'
|
|
203
|
+
// Icon control in the meta row. p-1 on top of the row's px-2 puts the glyph on
|
|
204
|
+
// the same 12px column as the icons in the rows above.
|
|
205
|
+
const ICON = 'flex items-center p-1 rounded-md hover:text-highlighted hover:bg-default'
|
|
206
|
+
|
|
207
|
+
const props = withDefaults(defineProps<{
|
|
208
|
+
/** The username. Plain text, no icon. Use #user only if you need more. */
|
|
209
|
+
user?: string
|
|
210
|
+
/** Accessible name for the user row. */
|
|
211
|
+
userLabel?: string
|
|
212
|
+
/** Icon for the user row. Pass `undefined` for a bare name. */
|
|
213
|
+
userIcon?: string
|
|
214
|
+
/** Route to the user page. Given, the row becomes a link and can go active. */
|
|
215
|
+
userTo?: string
|
|
216
|
+
|
|
217
|
+
/** Route to the settings page. Omit to drop the row. */
|
|
218
|
+
|
|
219
|
+
settingsTo?: string
|
|
220
|
+
/** Visible text of the settings row. */
|
|
221
|
+
settingsLabel?: string
|
|
222
|
+
|
|
223
|
+
/** Internal logout route. Use this or `logoutHref`, not both. */
|
|
224
|
+
|
|
225
|
+
logoutTo?: string
|
|
226
|
+
/** External logout URL — the usual case with a hosted identity provider. */
|
|
227
|
+
logoutHref?: string
|
|
228
|
+
/** Visible text of the logout row. */
|
|
229
|
+
logoutLabel?: string
|
|
230
|
+
|
|
231
|
+
/** Active locale (v-model). The language toggle renders only with `locales`. */
|
|
232
|
+
modelValue?: string
|
|
233
|
+
/** Locale codes, e.g. `['de', 'en']`. The language row appears only with this — not every
|
|
234
|
+
* service is translated.
|
|
235
|
+
*/
|
|
236
|
+
locales?: string[]
|
|
237
|
+
/** Accessible name for the locale group. There is no visible label. */
|
|
238
|
+
languageGroupLabel?: string
|
|
239
|
+
/** Accessible names per code, e.g. `{ de: 'Deutsch' }`. The button itself shows the code. */
|
|
240
|
+
languageLabels?: Record<string, string>
|
|
241
|
+
|
|
242
|
+
/** Presence of this is what makes the project open source. */
|
|
56
243
|
githubLink?: string
|
|
244
|
+
/** Accessible name for the GitHub icon. */
|
|
245
|
+
githubLabel?: string
|
|
246
|
+
/** Support address. One platform mailbox, so it has a default. */
|
|
247
|
+
supportEmail?: string
|
|
248
|
+
/** Accessible name for the support icon. */
|
|
249
|
+
supportLabel?: string
|
|
250
|
+
/** Subject line. Defaults to one naming this app, so support can tell which. */
|
|
251
|
+
supportSubject?: string
|
|
252
|
+
/** First line of the body. Defaults to app + version, for the same reason. */
|
|
253
|
+
supportBody?: string
|
|
254
|
+
/** This app's name, used in the support subject/body and nothing else. */
|
|
255
|
+
appName?: string
|
|
256
|
+
/** The tip jar icon. Same destination for every service, so only a boolean. */
|
|
257
|
+
tipJar?: boolean
|
|
258
|
+
/** Accessible name for the tip-jar icon. */
|
|
259
|
+
tipJarLabel?: string
|
|
260
|
+
/** Where it goes. One destination for the whole platform, so services do not set this. */
|
|
261
|
+
tipJarLink?: string
|
|
262
|
+
/** Version string for the badge, e.g. `v0.3.0`. Omit to drop the badge. */
|
|
57
263
|
version?: string
|
|
264
|
+
/** Release link for the badge. Ignored without `githubLink`. */
|
|
58
265
|
versionLink?: string
|
|
59
|
-
|
|
266
|
+
/** Mark the version badge as having unseen release notes. */
|
|
267
|
+
versionUnread?: boolean
|
|
268
|
+
|
|
269
|
+
/** The “A tool of tsp.tools” line. Off by default; only some services carry it. */
|
|
270
|
+
toolOf?: boolean
|
|
271
|
+
/** Leading words of the attribution. Left undefined so TspToolOf owns the default. */
|
|
272
|
+
toolOfLabel?: string
|
|
273
|
+
/** The platform name in it. Same reason. */
|
|
274
|
+
toolOfName?: string
|
|
275
|
+
/** Where that name points. Same reason. */
|
|
276
|
+
toolOfLink?: string
|
|
277
|
+
|
|
278
|
+
/** Theme-toggle text while dark — it names the DESTINATION. */
|
|
279
|
+
|
|
280
|
+
labelToLight?: string
|
|
281
|
+
/** Theme-toggle text while light. */
|
|
282
|
+
labelToDark?: string
|
|
283
|
+
/** Its accessible name while dark. */
|
|
284
|
+
ariaLabelToLight?: string
|
|
285
|
+
/** Its accessible name while light. */
|
|
286
|
+
ariaLabelToDark?: string
|
|
287
|
+
}>(), {
|
|
288
|
+
user: undefined,
|
|
289
|
+
userLabel: 'User',
|
|
290
|
+
userIcon: 'i-tabler-user-circle',
|
|
291
|
+
userTo: undefined,
|
|
292
|
+
settingsTo: undefined,
|
|
293
|
+
settingsLabel: 'Settings',
|
|
294
|
+
logoutTo: undefined,
|
|
295
|
+
logoutHref: undefined,
|
|
296
|
+
logoutLabel: 'Logout',
|
|
297
|
+
modelValue: undefined,
|
|
298
|
+
locales: undefined,
|
|
299
|
+
languageGroupLabel: 'Language',
|
|
300
|
+
languageLabels: undefined,
|
|
301
|
+
githubLink: undefined,
|
|
302
|
+
githubLabel: 'GitHub',
|
|
303
|
+
// The platform mailbox, not a per-service one — so it is a default rather
|
|
304
|
+
// than something eight apps each retype.
|
|
305
|
+
supportEmail: 'support@tsp.tools',
|
|
306
|
+
supportLabel: 'Support',
|
|
307
|
+
supportSubject: undefined,
|
|
308
|
+
supportBody: undefined,
|
|
309
|
+
appName: undefined,
|
|
310
|
+
// Opt-in, not on by default: switching it on would put a money link into
|
|
311
|
+
// every consuming app on upgrade, which is not something a version bump
|
|
312
|
+
// should decide.
|
|
313
|
+
tipJar: false,
|
|
314
|
+
tipJarLabel: 'Tip jar',
|
|
315
|
+
tipJarLink: 'https://thespielplatz.com/tip-jar',
|
|
316
|
+
version: undefined,
|
|
317
|
+
versionLink: undefined,
|
|
318
|
+
versionUnread: false,
|
|
319
|
+
toolOf: false,
|
|
320
|
+
// Left undefined so TspToolOf's own defaults apply — the same pattern as the
|
|
321
|
+
// theme-toggle labels below. Restating them here would be a second source of
|
|
322
|
+
// truth for one string set, which is what this component's header warns
|
|
323
|
+
// against for githubLink.
|
|
324
|
+
toolOfLabel: undefined,
|
|
325
|
+
toolOfName: undefined,
|
|
326
|
+
toolOfLink: undefined,
|
|
327
|
+
labelToLight: undefined,
|
|
328
|
+
labelToDark: undefined,
|
|
329
|
+
ariaLabelToLight: undefined,
|
|
330
|
+
ariaLabelToDark: undefined,
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
const emit = defineEmits<{ 'update:modelValue': [code: string] }>()
|
|
334
|
+
|
|
335
|
+
// A support mail should arrive already saying which service and which version
|
|
336
|
+
// it came from — otherwise that is the first round-trip of every support
|
|
337
|
+
// thread. Subject and body are overridable, but never empty.
|
|
338
|
+
// A bare mailbox only. The address used to be interpolated raw while the
|
|
339
|
+
// subject and body were encoded — so a value carrying `?`, `&` or a comma could
|
|
340
|
+
// add recipients or headers to the mail the user is about to send. It is a
|
|
341
|
+
// prop, and a prop is exactly what a service wires to config.
|
|
342
|
+
const MAILBOX = /^[^\s@,?&]+@[^\s@,?&]+\.[^\s@,?&]+$/
|
|
343
|
+
|
|
344
|
+
const supportHref = computed(() => {
|
|
345
|
+
if (!props.supportEmail || !MAILBOX.test(props.supportEmail)) return undefined
|
|
346
|
+
const app = props.appName ?? 'tsp.tools'
|
|
347
|
+
const subject = props.supportSubject ?? `${app} — Support`
|
|
348
|
+
const body = props.supportBody
|
|
349
|
+
?? `\n\n—\n${app}${props.version ? ` ${props.version}` : ''}`
|
|
350
|
+
// encodeURIComponent, not URLSearchParams: the latter encodes spaces as "+",
|
|
351
|
+
// which mail clients show literally in the subject line.
|
|
352
|
+
const query = `subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`
|
|
353
|
+
return `mailto:${props.supportEmail}?${query}`
|
|
354
|
+
})
|
|
60
355
|
</script>
|
|
@@ -1,40 +1,71 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
Marketing/public site footer (ADR 008, finding 2). Frame only: top border
|
|
3
|
-
|
|
4
|
-
content; the legal routes and their labels stay with the app.
|
|
2
|
+
Marketing/public site footer (ADR 008, finding 2). Frame only: a top border
|
|
3
|
+
and one centred row of links, with the platform attribution under it.
|
|
5
4
|
|
|
6
|
-
#brand —
|
|
7
|
-
#links — inline links, muted
|
|
8
|
-
|
|
5
|
+
#brand — optional mark/wordmark above the row
|
|
6
|
+
#links — inline links, muted. Give them icons; the row is compact and an
|
|
7
|
+
icon carries a short label better than a word alone at this size.
|
|
8
|
+
#actions — trailing items on the same row, e.g. <TspVersionBadge>
|
|
9
|
+
|
|
10
|
+
COMPACT AND CENTRED. It used to be a tall left-aligned block with the brand
|
|
11
|
+
stacked over a link row — a lot of vertical space at the end of a page that
|
|
12
|
+
has already said everything. One centred row reads as a footer rather than as
|
|
13
|
+
a final section, and matches what stash runs in production.
|
|
9
14
|
|
|
10
15
|
The link row renders only when #links or #actions is filled.
|
|
16
|
+
|
|
17
|
+
`tool-of` adds the “A tool of tsp.tools” line below it — the same component
|
|
18
|
+
the sidebar footer uses, so the two attributions cannot drift.
|
|
11
19
|
-->
|
|
12
20
|
<template>
|
|
13
|
-
<footer class="border-t border-muted pt-
|
|
21
|
+
<footer class="border-t border-muted pt-6 pb-8">
|
|
14
22
|
<TspContainer
|
|
15
23
|
width="site"
|
|
16
24
|
:padded="false"
|
|
17
25
|
>
|
|
18
26
|
<div
|
|
19
27
|
v-if="$slots.brand"
|
|
20
|
-
class="flex items-center gap-3"
|
|
28
|
+
class="flex items-center justify-center gap-3 mb-5"
|
|
21
29
|
>
|
|
22
30
|
<slot name="brand" />
|
|
23
31
|
</div>
|
|
24
32
|
|
|
25
33
|
<div
|
|
26
34
|
v-if="$slots.links || $slots.actions"
|
|
27
|
-
class="
|
|
35
|
+
class="flex flex-wrap items-center justify-center gap-x-6 gap-y-3 text-sm text-muted"
|
|
28
36
|
>
|
|
29
37
|
<slot name="links" />
|
|
30
|
-
|
|
31
|
-
<div
|
|
32
|
-
v-if="$slots.actions"
|
|
33
|
-
class="sm:ml-auto"
|
|
34
|
-
>
|
|
35
|
-
<slot name="actions" />
|
|
36
|
-
</div>
|
|
38
|
+
<slot name="actions" />
|
|
37
39
|
</div>
|
|
40
|
+
|
|
41
|
+
<TspToolOf
|
|
42
|
+
v-if="toolOf"
|
|
43
|
+
class="mt-4 text-center"
|
|
44
|
+
:label="toolOfLabel"
|
|
45
|
+
:name="toolOfName"
|
|
46
|
+
:link="toolOfLink"
|
|
47
|
+
/>
|
|
38
48
|
</TspContainer>
|
|
39
49
|
</footer>
|
|
40
50
|
</template>
|
|
51
|
+
|
|
52
|
+
<script setup lang="ts">
|
|
53
|
+
withDefaults(defineProps<{
|
|
54
|
+
/** The “A tool of tsp.tools” line. Off by default; only some services carry it. */
|
|
55
|
+
toolOf?: boolean
|
|
56
|
+
/** Leading words of the attribution. Left undefined so TspToolOf owns the default. */
|
|
57
|
+
toolOfLabel?: string
|
|
58
|
+
/** The platform name in it. Same reason. */
|
|
59
|
+
toolOfName?: string
|
|
60
|
+
/** Where that name points. Same reason. */
|
|
61
|
+
toolOfLink?: string
|
|
62
|
+
}>(), {
|
|
63
|
+
toolOf: false,
|
|
64
|
+
// Left undefined so TspToolOf's own defaults apply. Restating them here would
|
|
65
|
+
// make this a second source of truth for one string set, which is how the two
|
|
66
|
+
// footers' attributions would quietly drift apart.
|
|
67
|
+
toolOfLabel: undefined,
|
|
68
|
+
toolOfName: undefined,
|
|
69
|
+
toolOfLink: undefined,
|
|
70
|
+
})
|
|
71
|
+
</script>
|
|
@@ -10,13 +10,25 @@
|
|
|
10
10
|
#actions — right-hand cluster, e.g. <TspThemeToggle> + a CTA
|
|
11
11
|
|
|
12
12
|
Pass `:sticky="false"` for a header that scrolls away.
|
|
13
|
+
|
|
14
|
+
`bare` drops the bar entirely — no border, no background, not sticky — leaving
|
|
15
|
+
just the controls floating over the page. That is what a LANDING page wants:
|
|
16
|
+
the brand is already the hero, so repeating it in a header bar says the same
|
|
17
|
+
thing twice and puts a line across the top of an otherwise open page. A
|
|
18
|
+
logged-out *content* page still wants the real bar, because there the brand is
|
|
19
|
+
the only way back.
|
|
13
20
|
-->
|
|
14
21
|
<template>
|
|
15
|
-
<header
|
|
22
|
+
<header
|
|
23
|
+
:class="[
|
|
24
|
+
bare ? '' : 'border-b border-muted bg-default/90 backdrop-blur',
|
|
25
|
+
sticky && !bare ? 'sticky top-0 z-50' : '',
|
|
26
|
+
]"
|
|
27
|
+
>
|
|
16
28
|
<TspContainer
|
|
17
29
|
width="site"
|
|
18
30
|
:padded="false"
|
|
19
|
-
class="flex h-
|
|
31
|
+
:class="['flex items-center', bare ? 'h-16' : 'h-18']"
|
|
20
32
|
>
|
|
21
33
|
<slot name="brand" />
|
|
22
34
|
|
|
@@ -39,8 +51,12 @@
|
|
|
39
51
|
|
|
40
52
|
<script setup lang="ts">
|
|
41
53
|
withDefaults(defineProps<{
|
|
54
|
+
/** Keep the bar at the top of the viewport. Ignored when `bare`. */
|
|
42
55
|
sticky?: boolean
|
|
56
|
+
/** No bar: no border, no background, not sticky. For landing pages. */
|
|
57
|
+
bare?: boolean
|
|
43
58
|
}>(), {
|
|
44
59
|
sticky: true,
|
|
60
|
+
bare: false,
|
|
45
61
|
})
|
|
46
62
|
</script>
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
Light/dark toggle.
|
|
2
|
+
Light/dark toggle. Shares the 32px row shape of everything else in the
|
|
3
|
+
sidebar footer (spec 03 — it used to be text-xs/px-1 against the rows'
|
|
4
|
+
text-sm/px-3, which is the mismatch spec 01 shipped knowingly).
|
|
5
|
+
|
|
6
|
+
Flips the remembered preference (useTspColorMode); the
|
|
3
7
|
colour-mode plugin applies it. Default content is an icon + label; override via
|
|
4
8
|
the default slot (receives `{ isDark, label }`). The button is unstyled-ish so it
|
|
5
9
|
fits the sidebar footer or a top bar — pass classes to taste.
|
|
6
10
|
|
|
11
|
+
`icon-only` drops the label for tight places — the sidebar's meta row, and the
|
|
12
|
+
landing-page header, which wants just a sun/moon. The aria-label stays either
|
|
13
|
+
way: an icon-only control is only as good as its label.
|
|
14
|
+
|
|
7
15
|
All four strings are props so a bilingual site can pass its own (ADR 008,
|
|
8
16
|
finding 4). Naming is by *destination*: `…ToLight` is what the button shows and
|
|
9
17
|
announces while dark, because pressing it switches to light.
|
|
@@ -18,14 +26,17 @@
|
|
|
18
26
|
type="button"
|
|
19
27
|
data-testid="tsp-theme-toggle"
|
|
20
28
|
:aria-label="isDark ? ariaLabelToLight : ariaLabelToDark"
|
|
21
|
-
class="
|
|
29
|
+
:class="iconOnly
|
|
30
|
+
? 'flex items-center p-1 rounded-md text-muted hover:text-highlighted hover:bg-default'
|
|
31
|
+
: 'flex items-center gap-2.5 h-8 px-3 rounded-md text-sm text-muted hover:text-highlighted hover:bg-default'"
|
|
22
32
|
@click="toggle"
|
|
23
33
|
>
|
|
24
34
|
<UIcon
|
|
25
35
|
:name="isDark ? 'i-tabler-sun' : 'i-tabler-moon'"
|
|
26
|
-
class="text-base"
|
|
36
|
+
:class="iconOnly ? 'text-lg block' : 'text-base'"
|
|
27
37
|
/>
|
|
28
38
|
<slot
|
|
39
|
+
v-if="!iconOnly"
|
|
29
40
|
:is-dark="isDark"
|
|
30
41
|
:label="label"
|
|
31
42
|
>{{ label }}</slot>
|
|
@@ -42,11 +53,19 @@ const props = withDefaults(defineProps<{
|
|
|
42
53
|
ariaLabelToLight?: string
|
|
43
54
|
/** aria-label while light. */
|
|
44
55
|
ariaLabelToDark?: string
|
|
56
|
+
/** Drop the visible label and render a compact icon button. */
|
|
57
|
+
iconOnly?: boolean
|
|
45
58
|
}>(), {
|
|
46
|
-
|
|
47
|
-
|
|
59
|
+
// Just "Light" / "Dark": the word "mode" earns nothing next to a sun icon,
|
|
60
|
+
// and the label shares a 224px row with the language control — at "Light
|
|
61
|
+
// mode" the two sat flush with a pixel to spare, and a longer translation
|
|
62
|
+
// overflowed. The ARIA labels keep the full phrasing, where the extra words
|
|
63
|
+
// do work.
|
|
64
|
+
labelToLight: 'Light',
|
|
65
|
+
labelToDark: 'Dark',
|
|
48
66
|
ariaLabelToLight: 'Switch to light mode',
|
|
49
67
|
ariaLabelToDark: 'Switch to dark mode',
|
|
68
|
+
iconOnly: false,
|
|
50
69
|
})
|
|
51
70
|
|
|
52
71
|
const { pref, toggle } = useTspColorMode()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
“A tool of tsp.tools” — the platform attribution line.
|
|
3
|
+
|
|
4
|
+
One component because it appears in BOTH footers: the sidebar's and the
|
|
5
|
+
landing page's (spec 03). Writing it twice is how the two drift apart.
|
|
6
|
+
|
|
7
|
+
Only the leading words translate (`label`, German "Ein Tool von"). The domain
|
|
8
|
+
never does, and the link is always ours — so an app never hand-rolls markup to
|
|
9
|
+
translate a sentence that contains a link, which is the thing this layer
|
|
10
|
+
exists to prevent. This works because DE and EN both put the domain last; a
|
|
11
|
+
language that needs it mid-sentence would need a different shape.
|
|
12
|
+
|
|
13
|
+
Not a flex row: it is a sentence and has to stay one inline flow.
|
|
14
|
+
-->
|
|
15
|
+
<template>
|
|
16
|
+
<p
|
|
17
|
+
data-testid="tsp-tool-of"
|
|
18
|
+
class="text-xs text-dimmed"
|
|
19
|
+
>
|
|
20
|
+
{{ label }} <a
|
|
21
|
+
:href="link"
|
|
22
|
+
target="_blank"
|
|
23
|
+
rel="noopener noreferrer"
|
|
24
|
+
class="underline underline-offset-2 hover:text-highlighted"
|
|
25
|
+
>{{ name }}</a>
|
|
26
|
+
</p>
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
<script setup lang="ts">
|
|
30
|
+
withDefaults(defineProps<{
|
|
31
|
+
/** The leading words. */
|
|
32
|
+
label?: string
|
|
33
|
+
/** The platform, not the service. A service never names itself here. */
|
|
34
|
+
name?: string
|
|
35
|
+
/** Where the name points. */
|
|
36
|
+
link?: string
|
|
37
|
+
}>(), {
|
|
38
|
+
label: 'A tool of',
|
|
39
|
+
name: 'tsp.tools',
|
|
40
|
+
link: 'https://tsp.tools',
|
|
41
|
+
})
|
|
42
|
+
</script>
|