@thespielplatz/tsp-tools-theme 0.3.2 โ 0.3.3
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 +14 -0
- package/README.md +58 -31
- package/components/TspAccountPage.vue +81 -0
- package/components/TspAppShell.vue +121 -0
- package/components/TspMcpSettings.vue +278 -0
- package/components/TspPageHeader.vue +53 -0
- package/docs/api.md +103 -3
- package/docs/design-system.md +59 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,20 @@ Consumers pin a caret range and do not auto-update, so a release only reaches a
|
|
|
8
8
|
that service's own spec says so. This package has no users of its own โ every entry below is a
|
|
9
9
|
change to eight apps at once, at the moment each of them chooses to take it.
|
|
10
10
|
|
|
11
|
+
## v0.3.3
|
|
12
|
+
|
|
13
|
+
[compare changes](https://github.com/inf0matics/tsp-tools-theme/compare/v0.3.2...v0.3.3)
|
|
14
|
+
|
|
15
|
+
### ๐ Enhancements
|
|
16
|
+
|
|
17
|
+
- The account page and the MCP page are components (spec 07) ([ec38a8c](https://github.com/inf0matics/tsp-tools-theme/commit/ec38a8c))
|
|
18
|
+
- TspAppShell โ the frame every app was hand-copying (spec 08) ([6713d8e](https://github.com/inf0matics/tsp-tools-theme/commit/6713d8e))
|
|
19
|
+
|
|
20
|
+
### ๐ Documentation
|
|
21
|
+
|
|
22
|
+
- The typography scale, and the pages as pages ([68e54e9](https://github.com/inf0matics/tsp-tools-theme/commit/68e54e9))
|
|
23
|
+
- Anything with one right answer is a component, not a recipe ([75b88c7](https://github.com/inf0matics/tsp-tools-theme/commit/75b88c7))
|
|
24
|
+
|
|
11
25
|
## v0.3.2
|
|
12
26
|
|
|
13
27
|
[compare changes](https://github.com/inf0matics/tsp-tools-theme/compare/v0.3.1...v0.3.2)
|
package/README.md
CHANGED
|
@@ -89,39 +89,41 @@ shell components wherever you like:
|
|
|
89
89
|
|
|
90
90
|
```vue
|
|
91
91
|
<template>
|
|
92
|
-
<
|
|
93
|
-
<
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
<
|
|
99
|
-
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
<
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
<TspContainer><NuxtPage /></TspContainer>
|
|
120
|
-
</main>
|
|
121
|
-
</div>
|
|
92
|
+
<TspAppShell app-name="trips">
|
|
93
|
+
<template #brand>
|
|
94
|
+
<TspBrandTile><img src="/logo.svg" alt="trips"></TspBrandTile>
|
|
95
|
+
<TspWordmark name="trips" />
|
|
96
|
+
</template>
|
|
97
|
+
<template #nav>
|
|
98
|
+
<TspNavItem to="/" icon="i-tabler-car" badge="Owner" badge-tone="primary">Fahrten</TspNavItem>
|
|
99
|
+
<TspNavGroup to="/reports" icon="i-tabler-file-text" label="Berichte">
|
|
100
|
+
<TspNavSubItem to="/reports/tax" icon="i-tabler-receipt">Steuer</TspNavSubItem>
|
|
101
|
+
</TspNavGroup>
|
|
102
|
+
<TspNavSection label="Extern" />
|
|
103
|
+
<TspNavItem to="/handbook" icon="i-tabler-book">Handbuch</TspNavItem>
|
|
104
|
+
</template>
|
|
105
|
+
<template #footer>
|
|
106
|
+
<TspSidebarFooter
|
|
107
|
+
v-model="locale"
|
|
108
|
+
app-name="trips"
|
|
109
|
+
user="fil" user-to="/user"
|
|
110
|
+
settings-to="/settings" logout-href="/auth/logout"
|
|
111
|
+
:locales="['de', 'en']"
|
|
112
|
+
github-link="https://github.com/โฆ" version="v1.2.0" version-link="https://github.com/โฆ/releases/tag/v1.2.0"
|
|
113
|
+
tip-jar tool-of
|
|
114
|
+
/>
|
|
115
|
+
</template>
|
|
116
|
+
|
|
117
|
+
<NuxtPage />
|
|
118
|
+
</TspAppShell>
|
|
122
119
|
</template>
|
|
123
120
|
```
|
|
124
121
|
|
|
122
|
+
**`TspAppShell` is the whole frame** โ root element, sidebar, content column, the mobile top bar
|
|
123
|
+
and the measure. It picks a plain `div` or `<TspThemeProvider>` as its root from your
|
|
124
|
+
`apply` setting, so the same layout serves global and scoped consumption. Don't hand-roll it:
|
|
125
|
+
that frame was written out by hand in all eight services and no two of them agreed (spec 08).
|
|
126
|
+
|
|
125
127
|
## Scoped usage (one sub-area themed)
|
|
126
128
|
|
|
127
129
|
`nuxt.config.ts`:
|
|
@@ -197,7 +199,8 @@ mask }`, show/hide a secret with a fixed-length placeholder.
|
|
|
197
199
|
| Component | Purpose |
|
|
198
200
|
|---|---|
|
|
199
201
|
| `TspThemeProvider` | Scoped wrapper โ marks a sub-area as themed (`.tsp-theme`). |
|
|
200
|
-
| `
|
|
202
|
+
| `TspAppShell` | **The whole logged-in frame** โ root, sidebar, content column, top bar, measure. Start here. |
|
|
203
|
+
| `TspSidebar` | The sidebar alone; slots `#brand` `#nav` `#footer`. `TspAppShell` wraps it for you. |
|
|
201
204
|
| `TspSidebarFooter` | User โ Settings โ Logout โ divider โ theme + language โ meta icons + version โ "A tool of tsp.tools". Rows are **props**, not slots. |
|
|
202
205
|
| `TspSiteHeader` | Public/marketing header; slots `#brand` `#nav` `#actions`, prop `sticky`. |
|
|
203
206
|
| `TspSiteFooter` | Public/marketing footer; slots `#brand` `#links` `#actions`. |
|
|
@@ -210,6 +213,9 @@ mask }`, show/hide a secret with a fixed-length placeholder.
|
|
|
210
213
|
| `TspNavLabel` | Truncates a label and reveals it in full on hover. Used by the rows above. |
|
|
211
214
|
| `TspBrandTile` | The frame around a service's mark โ `sidebar` (24px) or `hero` (64/96px). |
|
|
212
215
|
| `TspUserCard` | Account card for a service's user page; masked id + manage link. |
|
|
216
|
+
| `TspPageHeader` | The page title and its lede. **The heading scale lives here** โ don't hand-roll an `h1`. |
|
|
217
|
+
| `TspAccountPage` | The standalone account page, whole: heading + `TspUserCard`. |
|
|
218
|
+
| `TspMcpSettings` | The whole MCP connection page โ server, AI-processing warning, Claude, generic. |
|
|
213
219
|
| `TspFooterRow` | One sidebar-footer row โ `to` / `href` / `button`. |
|
|
214
220
|
| `TspLanguageToggle` | DE/EN segmented control; all strings are props. |
|
|
215
221
|
| `TspVersionBadge` | Version pill with a tag icon; a link only when given `href`. |
|
|
@@ -235,6 +241,27 @@ version badge links to the release; leave it out and the badge is plain text โ
|
|
|
235
241
|
is then ignored**, because a closed-source project has no release page to point at. There is no
|
|
236
242
|
`variant` prop; a second source of truth could disagree with the first.
|
|
237
243
|
|
|
244
|
+
### Three of them are whole pages
|
|
245
|
+
|
|
246
|
+
`TspPageHeader`, `TspAccountPage` and `TspMcpSettings` are not building blocks โ they are the
|
|
247
|
+
page. Reach for them before assembling one out of `TspSectionCard` and `TspCopyField`:
|
|
248
|
+
|
|
249
|
+
```vue
|
|
250
|
+
<TspMcpSettings
|
|
251
|
+
endpoint="https://todo.tsp.tools/mcp"
|
|
252
|
+
:api-key="key"
|
|
253
|
+
server-name="todo"
|
|
254
|
+
:labels="{ apiKey: t('mcp.key'), โฆ }"
|
|
255
|
+
@regenerate="rotate"
|
|
256
|
+
/>
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
That is the whole page โ the server card, the third-party-processing warning, the Claude
|
|
260
|
+
snippet (redacted on screen, complete on copy) and the generic client card, in that order. It
|
|
261
|
+
exists because the order is what drifted: the page reached four copies in three shapes before
|
|
262
|
+
it was a component. Every visible string is an optional key on `labels`; see
|
|
263
|
+
[`docs/api.md`](docs/api.md) for the full interface.
|
|
264
|
+
|
|
238
265
|
### App shell vs site shell
|
|
239
266
|
|
|
240
267
|
`TspSidebar` + `TspSidebarFooter` are the **logged-in** shell. For a public/marketing surface
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
A service's account page, whole.
|
|
3
|
+
|
|
4
|
+
<TspAccountPage
|
|
5
|
+
:name="user.name"
|
|
6
|
+
:account-id="user.id"
|
|
7
|
+
manage-link="https://logto.tsp.tools/account"
|
|
8
|
+
/>
|
|
9
|
+
|
|
10
|
+
WHY A WRAPPER OVER ONE CARD IS WORTH IT. TspUserCard has existed since 0.3.0
|
|
11
|
+
and tsp-todo used it correctly โ and its account page still came out wrong,
|
|
12
|
+
because the heading above the card was the app's to invent and it invented
|
|
13
|
+
`text-[28px]`. Two correct components a consumer must know to combine is
|
|
14
|
+
precisely the step that failed. This is one named thing that can be found in
|
|
15
|
+
`api.md` and dropped in whole (spec 07).
|
|
16
|
+
|
|
17
|
+
It is the STANDALONE page. Inside a tabbed settings area the heading belongs
|
|
18
|
+
to the parent route, so use <TspPageHeader> there and <TspUserCard> directly
|
|
19
|
+
โ a second <h1> under the tabs is not what this is for.
|
|
20
|
+
|
|
21
|
+
The layer has no auth and no data: the name, the id and the manage URL are
|
|
22
|
+
the app's. Every string is a prop with an English default, and they are
|
|
23
|
+
forwarded rather than redeclared with defaults of their own โ two sets of
|
|
24
|
+
defaults for one card is two places for them to disagree.
|
|
25
|
+
-->
|
|
26
|
+
<template>
|
|
27
|
+
<div data-testid="tsp-account-page">
|
|
28
|
+
<TspPageHeader
|
|
29
|
+
:title="title"
|
|
30
|
+
:description="description"
|
|
31
|
+
/>
|
|
32
|
+
<TspUserCard
|
|
33
|
+
:name="name"
|
|
34
|
+
:account-label="accountLabel"
|
|
35
|
+
:account-id="accountId"
|
|
36
|
+
:id-label="idLabel"
|
|
37
|
+
:manage-link="manageLink"
|
|
38
|
+
:manage-label="manageLabel"
|
|
39
|
+
:reveal-label="revealLabel"
|
|
40
|
+
:hide-label="hideLabel"
|
|
41
|
+
/>
|
|
42
|
+
</div>
|
|
43
|
+
</template>
|
|
44
|
+
|
|
45
|
+
<script setup lang="ts">
|
|
46
|
+
// Only `title` carries a default. The card's strings are passed through as
|
|
47
|
+
// `undefined` when the caller omits them, which is the value Vue resolves
|
|
48
|
+
// against TspUserCard's OWN defaults โ so there is exactly one copy of them.
|
|
49
|
+
withDefaults(defineProps<{
|
|
50
|
+
/** Display name. */
|
|
51
|
+
name: string
|
|
52
|
+
/** The page title. */
|
|
53
|
+
title?: string
|
|
54
|
+
/** One line under the title. Omit for none. */
|
|
55
|
+
description?: string
|
|
56
|
+
/** Which account this is โ shown under the name. */
|
|
57
|
+
accountLabel?: string
|
|
58
|
+
/** The account id. Omit to hide the whole id block. */
|
|
59
|
+
accountId?: string
|
|
60
|
+
/** Label over the account id. */
|
|
61
|
+
idLabel?: string
|
|
62
|
+
/** Where โmanage accountโ goes. Omit to hide the link. */
|
|
63
|
+
manageLink?: string
|
|
64
|
+
/** Text of the manage-account link. */
|
|
65
|
+
manageLabel?: string
|
|
66
|
+
/** Accessible name for the eye while the id is hidden. */
|
|
67
|
+
revealLabel?: string
|
|
68
|
+
/** Accessible name for it while the id is shown. */
|
|
69
|
+
hideLabel?: string
|
|
70
|
+
}>(), {
|
|
71
|
+
title: 'Account',
|
|
72
|
+
description: undefined,
|
|
73
|
+
accountLabel: undefined,
|
|
74
|
+
accountId: undefined,
|
|
75
|
+
idLabel: undefined,
|
|
76
|
+
manageLink: undefined,
|
|
77
|
+
manageLabel: undefined,
|
|
78
|
+
revealLabel: undefined,
|
|
79
|
+
hideLabel: undefined,
|
|
80
|
+
})
|
|
81
|
+
</script>
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
The logged-in frame: root, sidebar, content column, mobile top bar, measure.
|
|
3
|
+
|
|
4
|
+
<TspAppShell app-name="todo">
|
|
5
|
+
<template #brand>โฆ</template>
|
|
6
|
+
<template #nav>โฆ</template>
|
|
7
|
+
<template #footer><TspSidebarFooter โฆ /></template>
|
|
8
|
+
<NuxtPage />
|
|
9
|
+
</TspAppShell>
|
|
10
|
+
|
|
11
|
+
WHY THIS EXISTS. The layer already owned every piece; it did not own how they
|
|
12
|
+
go together, so all eight consumers and both demos hand-wrote the same fifteen
|
|
13
|
+
lines โ and they were not the same fifteen lines. The survey behind spec 08
|
|
14
|
+
found three disagreements nobody had chosen:
|
|
15
|
+
|
|
16
|
+
ยท two scroll models (trips/gage/pogo pin the frame and scroll <main>;
|
|
17
|
+
everything else scrolls the page),
|
|
18
|
+
ยท three viewport units for one intent โ min-h-svh, h-svh, and tsp-todo's
|
|
19
|
+
min-h-screen, which is 100vh: the unit that breaks under a mobile
|
|
20
|
+
browser's collapsing toolbar, and the reason svh exists,
|
|
21
|
+
ยท `bg-default` on the content column, or not โ present in both demos,
|
|
22
|
+
absent in four apps, and only visible when a page is shorter than the
|
|
23
|
+
viewport, which is why nobody had noticed.
|
|
24
|
+
|
|
25
|
+
Otherwise the classes were identical but REORDERED (`flex-1 min-w-0 flex
|
|
26
|
+
flex-col` against `flex min-w-0 flex-1 flex-col`). That is copy-paste, not
|
|
27
|
+
intent, and it is what a component is for.
|
|
28
|
+
|
|
29
|
+
THE ROOT ELEMENT IS DERIVED, NOT DECLARED. `useTspThemeOptions().apply`
|
|
30
|
+
already knows whether this app is themed globally or scoped, so the shell
|
|
31
|
+
picks a plain <div> or <TspThemeProvider> itself. There is no `mode` prop, for
|
|
32
|
+
the same reason TspSidebarFooter has no `variant` prop: a second source of
|
|
33
|
+
truth is one that can disagree with the first.
|
|
34
|
+
|
|
35
|
+
What stays the app's: its mark, its nav list, its footer values, its page.
|
|
36
|
+
-->
|
|
37
|
+
<template>
|
|
38
|
+
<component
|
|
39
|
+
:is="root"
|
|
40
|
+
data-testid="tsp-app-shell"
|
|
41
|
+
:class="rootClass"
|
|
42
|
+
>
|
|
43
|
+
<TspSidebar>
|
|
44
|
+
<template #brand><slot name="brand" /></template>
|
|
45
|
+
<template #nav><slot name="nav" /></template>
|
|
46
|
+
<template #footer><slot name="footer" /></template>
|
|
47
|
+
</TspSidebar>
|
|
48
|
+
|
|
49
|
+
<!-- bg-default here as well as on the root: the root's paint is behind the
|
|
50
|
+
SIDEBAR too, and the column is what shows beside it when a page is
|
|
51
|
+
short. This is the line four apps are missing. -->
|
|
52
|
+
<div
|
|
53
|
+
data-testid="tsp-app-shell-content"
|
|
54
|
+
class="flex min-w-0 flex-1 flex-col bg-default"
|
|
55
|
+
>
|
|
56
|
+
<!-- Below sm the sidebar is a drawer, so this bar is the only thing that
|
|
57
|
+
says where you are. It hides itself above sm. -->
|
|
58
|
+
<TspTopBar :menu-label="menuLabel">
|
|
59
|
+
<template #brand>
|
|
60
|
+
<slot name="top-brand">
|
|
61
|
+
<TspWordmark
|
|
62
|
+
v-if="appName"
|
|
63
|
+
:name="appName"
|
|
64
|
+
/>
|
|
65
|
+
</slot>
|
|
66
|
+
</template>
|
|
67
|
+
</TspTopBar>
|
|
68
|
+
|
|
69
|
+
<main :class="mainClass">
|
|
70
|
+
<TspContainer :width="containerWidth">
|
|
71
|
+
<slot />
|
|
72
|
+
</TspContainer>
|
|
73
|
+
</main>
|
|
74
|
+
</div>
|
|
75
|
+
</component>
|
|
76
|
+
</template>
|
|
77
|
+
|
|
78
|
+
<script setup lang="ts">
|
|
79
|
+
// Imported rather than auto-imported: `<component :is>` needs the component
|
|
80
|
+
// itself, and the auto-import only resolves a tag in a template.
|
|
81
|
+
import TspThemeProvider from './TspThemeProvider.vue'
|
|
82
|
+
|
|
83
|
+
const props = withDefaults(defineProps<{
|
|
84
|
+
/** The service's name. Renders the top bar's wordmark โ override with `#top-brand`. */
|
|
85
|
+
appName?: string
|
|
86
|
+
/** Forwarded to `TspContainer`: `app` (1152px) or `site` (1240px). */
|
|
87
|
+
containerWidth?: 'app' | 'site'
|
|
88
|
+
/**
|
|
89
|
+
* `page` scrolls the document and leaves the sidebar sticky โ the model this
|
|
90
|
+
* layer documents and the one the mobile drawer was built against. `inner`
|
|
91
|
+
* pins the frame to the viewport and scrolls `<main>` instead, which is what
|
|
92
|
+
* trips, gage and pogo run today; it is a real choice for a dense table view,
|
|
93
|
+
* so adopting this shell is not forced to be a redesign as well.
|
|
94
|
+
*/
|
|
95
|
+
scroll?: 'page' | 'inner'
|
|
96
|
+
/** Accessible name for the mobile burger. Forwarded to `TspTopBar`. */
|
|
97
|
+
menuLabel?: string
|
|
98
|
+
}>(), {
|
|
99
|
+
appName: undefined,
|
|
100
|
+
containerWidth: 'app',
|
|
101
|
+
scroll: 'page',
|
|
102
|
+
menuLabel: undefined,
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const { apply } = useTspThemeOptions()
|
|
106
|
+
|
|
107
|
+
// TspThemeProvider paints bg-default and text-default on its own root, so the
|
|
108
|
+
// scoped branch must not repeat them โ two sources for one colour.
|
|
109
|
+
const root = computed(() => (apply === 'scoped' ? TspThemeProvider : 'div'))
|
|
110
|
+
|
|
111
|
+
const rootClass = computed(() => [
|
|
112
|
+
'flex',
|
|
113
|
+
props.scroll === 'inner' ? 'h-svh overflow-hidden' : 'min-h-svh',
|
|
114
|
+
apply === 'scoped' ? '' : 'bg-default text-default',
|
|
115
|
+
])
|
|
116
|
+
|
|
117
|
+
const mainClass = computed(() => [
|
|
118
|
+
'min-w-0 flex-1',
|
|
119
|
+
props.scroll === 'inner' ? 'overflow-y-auto' : '',
|
|
120
|
+
])
|
|
121
|
+
</script>
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
The MCP connection page for a service that exposes one, whole.
|
|
3
|
+
|
|
4
|
+
<TspMcpSettings
|
|
5
|
+
endpoint="https://todo.tsp.tools/mcp"
|
|
6
|
+
:api-key="key"
|
|
7
|
+
server-name="todo"
|
|
8
|
+
@regenerate="rotate"
|
|
9
|
+
/>
|
|
10
|
+
|
|
11
|
+
WHY THIS IS A COMPONENT AND NOT A RECIPE. The page started in trips, was
|
|
12
|
+
generalised into the playground, copied into `scoped/`, and reached tsp-todo
|
|
13
|
+
through an agent that had never seen any of them โ because the playground is
|
|
14
|
+
not in the published package. Four copies, three shapes. The layer's own two
|
|
15
|
+
demos had already drifted from each other: `scoped/` put Generic before
|
|
16
|
+
Claude and dropped a paragraph. Two copies in one repo could not hold still;
|
|
17
|
+
eight services will not (spec 07, ADR 017).
|
|
18
|
+
|
|
19
|
+
THE ORDER IS THE DESIGN. A service that exposes MCP has to tell a user four
|
|
20
|
+
things, and this is the order they need them in:
|
|
21
|
+
|
|
22
|
+
1. where the endpoint is and what their key is
|
|
23
|
+
2. that they are about to hand their data to someone else's model
|
|
24
|
+
3. how to paste it into Claude
|
|
25
|
+
4. how to paste it into anything else
|
|
26
|
+
|
|
27
|
+
The warning is not decoration and not optional by accident: it renders unless
|
|
28
|
+
a service passes :show-warning="false", so leaving it out is a decision
|
|
29
|
+
somebody made rather than a card they forgot to copy.
|
|
30
|
+
|
|
31
|
+
THE REDACTION IS LOAD-BEARING. The Claude snippet is shown with the key
|
|
32
|
+
replaced and copied with it filled in. Printing the real key in the code
|
|
33
|
+
block would defeat the masking one card above, and a service assembling the
|
|
34
|
+
JSON by hand is a service that can get that wrong โ so the component builds
|
|
35
|
+
it.
|
|
36
|
+
|
|
37
|
+
The layer has no auth and no data: the endpoint, the key and the rotation are
|
|
38
|
+
the service's.
|
|
39
|
+
-->
|
|
40
|
+
<template>
|
|
41
|
+
<div
|
|
42
|
+
data-testid="tsp-mcp-settings"
|
|
43
|
+
class="flex flex-col gap-4"
|
|
44
|
+
>
|
|
45
|
+
<!-- Each block is wrapped rather than labelled on the card itself: a
|
|
46
|
+
data-testid on <TspSectionCard> falls through and REPLACES the card's
|
|
47
|
+
own, which the card's tests read. -->
|
|
48
|
+
<div
|
|
49
|
+
data-testid="tsp-mcp-block"
|
|
50
|
+
data-block="server"
|
|
51
|
+
>
|
|
52
|
+
<TspSectionCard
|
|
53
|
+
icon="i-tabler-server-2"
|
|
54
|
+
:title="l.serverTitle"
|
|
55
|
+
>
|
|
56
|
+
<div class="flex flex-col gap-4">
|
|
57
|
+
<TspCopyField
|
|
58
|
+
:label="l.endpoint"
|
|
59
|
+
:value="endpoint"
|
|
60
|
+
:copy-label="l.copy"
|
|
61
|
+
:copied-label="l.copied"
|
|
62
|
+
/>
|
|
63
|
+
<TspCopyField
|
|
64
|
+
:label="l.apiKey"
|
|
65
|
+
:value="apiKey"
|
|
66
|
+
secret
|
|
67
|
+
:help="l.apiKeyHelp"
|
|
68
|
+
:reveal-label="l.reveal"
|
|
69
|
+
:hide-label="l.hide"
|
|
70
|
+
:copy-label="l.copy"
|
|
71
|
+
:copied-label="l.copied"
|
|
72
|
+
>
|
|
73
|
+
<template
|
|
74
|
+
v-if="canRegenerate"
|
|
75
|
+
#help
|
|
76
|
+
>
|
|
77
|
+
<button
|
|
78
|
+
type="button"
|
|
79
|
+
data-testid="tsp-mcp-regenerate"
|
|
80
|
+
class="inline-flex items-center gap-1.5 text-xs text-muted hover:text-highlighted"
|
|
81
|
+
@click="emit('regenerate')"
|
|
82
|
+
>
|
|
83
|
+
<UIcon name="i-tabler-refresh" />
|
|
84
|
+
{{ l.regenerate }}
|
|
85
|
+
</button>
|
|
86
|
+
</template>
|
|
87
|
+
</TspCopyField>
|
|
88
|
+
</div>
|
|
89
|
+
</TspSectionCard>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div
|
|
93
|
+
v-if="showWarning"
|
|
94
|
+
data-testid="tsp-mcp-block"
|
|
95
|
+
data-block="warning"
|
|
96
|
+
>
|
|
97
|
+
<UAlert
|
|
98
|
+
data-testid="tsp-mcp-warning"
|
|
99
|
+
color="warning"
|
|
100
|
+
variant="subtle"
|
|
101
|
+
icon="i-tabler-info-circle"
|
|
102
|
+
:description="l.warning"
|
|
103
|
+
/>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<div
|
|
107
|
+
data-testid="tsp-mcp-block"
|
|
108
|
+
data-block="claude"
|
|
109
|
+
>
|
|
110
|
+
<TspSectionCard
|
|
111
|
+
icon="i-tabler-sparkles"
|
|
112
|
+
:title="l.claudeTitle"
|
|
113
|
+
>
|
|
114
|
+
<p class="mb-3 text-sm text-muted">
|
|
115
|
+
{{ l.claudeHelp }}
|
|
116
|
+
</p>
|
|
117
|
+
<TspCodeBlock
|
|
118
|
+
:code="config(l.keyPlaceholder)"
|
|
119
|
+
:copy-value="config(apiKey)"
|
|
120
|
+
:copy-label="l.copyWithKey"
|
|
121
|
+
:copied-label="l.copiedWithKey"
|
|
122
|
+
:help="l.codeHelp"
|
|
123
|
+
/>
|
|
124
|
+
</TspSectionCard>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<div
|
|
128
|
+
data-testid="tsp-mcp-block"
|
|
129
|
+
data-block="generic"
|
|
130
|
+
>
|
|
131
|
+
<TspSectionCard
|
|
132
|
+
icon="i-tabler-plug-connected"
|
|
133
|
+
:title="l.genericTitle"
|
|
134
|
+
>
|
|
135
|
+
<p class="mb-4 text-sm text-muted">
|
|
136
|
+
{{ l.genericHelp }}
|
|
137
|
+
</p>
|
|
138
|
+
<div class="flex flex-col gap-4">
|
|
139
|
+
<TspCopyField
|
|
140
|
+
:label="l.serverUrl"
|
|
141
|
+
:value="endpoint"
|
|
142
|
+
:copy-label="l.copy"
|
|
143
|
+
:copied-label="l.copied"
|
|
144
|
+
/>
|
|
145
|
+
<!-- A second secret, and a much longer one than the bare key. The
|
|
146
|
+
fixed-length mask can only be proven with two of different
|
|
147
|
+
lengths to compare. -->
|
|
148
|
+
<TspCopyField
|
|
149
|
+
:label="l.authHeader"
|
|
150
|
+
:value="`Authorization: Bearer ${apiKey}`"
|
|
151
|
+
secret
|
|
152
|
+
:reveal-label="l.reveal"
|
|
153
|
+
:hide-label="l.hide"
|
|
154
|
+
:copy-label="l.copy"
|
|
155
|
+
:copied-label="l.copied"
|
|
156
|
+
/>
|
|
157
|
+
</div>
|
|
158
|
+
</TspSectionCard>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</template>
|
|
162
|
+
|
|
163
|
+
<script setup lang="ts">
|
|
164
|
+
/**
|
|
165
|
+
* Every user-visible string in the page. One object rather than fifteen flat
|
|
166
|
+
* props: the layer's rule is that nothing is hardcoded beyond a default, not
|
|
167
|
+
* that each string must be its own attribute, and fifteen attributes is where
|
|
168
|
+
* the flat style stops paying. Partial โ anything omitted keeps the English
|
|
169
|
+
* default below.
|
|
170
|
+
*/
|
|
171
|
+
export interface TspMcpLabels {
|
|
172
|
+
/** Title of the first card. */
|
|
173
|
+
serverTitle?: string
|
|
174
|
+
/** Label on the endpoint field. */
|
|
175
|
+
endpoint?: string
|
|
176
|
+
/** Label on the key field. */
|
|
177
|
+
apiKey?: string
|
|
178
|
+
/** The line under the key โ say it is a password. */
|
|
179
|
+
apiKeyHelp?: string
|
|
180
|
+
/** The rotate-my-key control. Only rendered when `@regenerate` is bound. */
|
|
181
|
+
regenerate?: string
|
|
182
|
+
/** The third-party-processing warning. */
|
|
183
|
+
warning?: string
|
|
184
|
+
/** Title of the Claude card. */
|
|
185
|
+
claudeTitle?: string
|
|
186
|
+
/** How to install the snippet in Claude. */
|
|
187
|
+
claudeHelp?: string
|
|
188
|
+
/** Stands in for the key in the SHOWN snippet. The copy has the real one. */
|
|
189
|
+
keyPlaceholder?: string
|
|
190
|
+
/** Snippet copy button at rest. */
|
|
191
|
+
copyWithKey?: string
|
|
192
|
+
/** Snippet copy button just after a copy. */
|
|
193
|
+
copiedWithKey?: string
|
|
194
|
+
/** The line under the snippet, explaining the placeholder. */
|
|
195
|
+
codeHelp?: string
|
|
196
|
+
/** Title of the any-other-client card. */
|
|
197
|
+
genericTitle?: string
|
|
198
|
+
/** What to do with the two values below it. */
|
|
199
|
+
genericHelp?: string
|
|
200
|
+
/** Label on the server-URL field. */
|
|
201
|
+
serverUrl?: string
|
|
202
|
+
/** Label on the Authorization-header field. */
|
|
203
|
+
authHeader?: string
|
|
204
|
+
/** Accessible name for the eye while a secret is hidden. */
|
|
205
|
+
reveal?: string
|
|
206
|
+
/** Accessible name for it while the secret is shown. */
|
|
207
|
+
hide?: string
|
|
208
|
+
/** Accessible name for every field's copy button at rest. */
|
|
209
|
+
copy?: string
|
|
210
|
+
/** And just after a successful copy. */
|
|
211
|
+
copied?: string
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const props = withDefaults(defineProps<{
|
|
215
|
+
/** The MCP endpoint URL. Shown, copied, and written into the config snippet. */
|
|
216
|
+
endpoint: string
|
|
217
|
+
/** The user's key. Never rendered while masked โ only the copy carries it. */
|
|
218
|
+
apiKey: string
|
|
219
|
+
/** Key under `mcpServers` in the snippet. Use the service's short name. */
|
|
220
|
+
serverName?: string
|
|
221
|
+
/** The third-party-processing warning. On unless a service decides otherwise. */
|
|
222
|
+
showWarning?: boolean
|
|
223
|
+
/** Overrides for any visible string. Partial; English defaults for the rest. */
|
|
224
|
+
labels?: TspMcpLabels
|
|
225
|
+
}>(), {
|
|
226
|
+
serverName: 'tsp',
|
|
227
|
+
showWarning: true,
|
|
228
|
+
labels: () => ({}),
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
const emit = defineEmits<{
|
|
232
|
+
/** The user asked for a new key. Rotating it is the service's job. */
|
|
233
|
+
regenerate: []
|
|
234
|
+
}>()
|
|
235
|
+
|
|
236
|
+
const DEFAULTS: Required<TspMcpLabels> = {
|
|
237
|
+
serverTitle: 'MCP Server',
|
|
238
|
+
endpoint: 'Endpoint',
|
|
239
|
+
apiKey: 'API key',
|
|
240
|
+
apiKeyHelp: 'Treat this key like a password โ anyone with it can access your data.',
|
|
241
|
+
regenerate: 'Regenerate',
|
|
242
|
+
warning: 'Anything you send to your connected AI assistant is processed by that assistant\'s AI provider (e.g. Anthropic), under your own agreement with them โ not by this service.',
|
|
243
|
+
claudeTitle: 'Claude',
|
|
244
|
+
claudeHelp: 'Add this to your Claude MCP configuration (Settings โ Connectors, or claude_desktop_config.json), then restart Claude.',
|
|
245
|
+
keyPlaceholder: '<your API key>',
|
|
246
|
+
copyWithKey: 'Copy with API key',
|
|
247
|
+
copiedWithKey: 'Copied with key',
|
|
248
|
+
codeHelp: 'The key above is hidden โ โCopy with API keyโ copies the config with your real key filled in.',
|
|
249
|
+
genericTitle: 'Generic / OpenAI',
|
|
250
|
+
genericHelp: 'For any other MCP client, add a remote (streamable HTTP) server with these details:',
|
|
251
|
+
serverUrl: 'Server URL',
|
|
252
|
+
authHeader: 'Auth header',
|
|
253
|
+
reveal: 'Show',
|
|
254
|
+
hide: 'Hide',
|
|
255
|
+
copy: 'Copy',
|
|
256
|
+
copied: 'Copied',
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const l = computed<Required<TspMcpLabels>>(() => ({ ...DEFAULTS, ...props.labels }))
|
|
260
|
+
|
|
261
|
+
// Built here, not by the caller. It is the same JSON for every service bar the
|
|
262
|
+
// name and the URL, and JSON.stringify escapes what hand-written template
|
|
263
|
+
// literals do not.
|
|
264
|
+
const config = (key: string) => JSON.stringify({
|
|
265
|
+
mcpServers: {
|
|
266
|
+
[props.serverName]: {
|
|
267
|
+
url: props.endpoint,
|
|
268
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
}, null, 2)
|
|
272
|
+
|
|
273
|
+
// A regenerate control with nothing behind it is a dead link, so it appears
|
|
274
|
+
// only when the service actually listens. `defineEmits` strips the handler out
|
|
275
|
+
// of `useAttrs`, so the vnode is where it can still be seen.
|
|
276
|
+
const instance = getCurrentInstance()
|
|
277
|
+
const canRegenerate = computed(() => !!instance?.vnode.props?.onRegenerate)
|
|
278
|
+
</script>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
The page title, and optionally the line under it.
|
|
3
|
+
|
|
4
|
+
<TspPageHeader title="Settings" description="AI client (MCP) connection." />
|
|
5
|
+
|
|
6
|
+
The smallest component in the layer and the one that stops the most drift.
|
|
7
|
+
Every page in every service opens with this block, and until now it was
|
|
8
|
+
twelve hand-written copies across the two demo apps alone โ with three
|
|
9
|
+
different bottom margins between them. Nothing in `design-system.md` ever
|
|
10
|
+
stated the scale, so a consuming service had nothing to copy and guessed:
|
|
11
|
+
tsp-todo shipped `text-[28px]` under a correctly-used TspUserCard (spec 07).
|
|
12
|
+
|
|
13
|
+
THE SCALE IS FIXED HERE. `text-2xl` (24px), bold, `text-highlighted`. A
|
|
14
|
+
service that wants a different size on one page is a service whose pages no
|
|
15
|
+
longer match the others', which is the entire thing the layer exists to stop.
|
|
16
|
+
|
|
17
|
+
Spacing belongs to the component, not the caller: the gap below a title with
|
|
18
|
+
a lede is not the gap below a bare title, and leaving that to each page is
|
|
19
|
+
how three margins happened.
|
|
20
|
+
|
|
21
|
+
No slots. A slot over the title invites exactly the restyling this prevents.
|
|
22
|
+
-->
|
|
23
|
+
<template>
|
|
24
|
+
<header
|
|
25
|
+
data-testid="tsp-page-header"
|
|
26
|
+
:class="description ? 'mb-8' : 'mb-6'"
|
|
27
|
+
>
|
|
28
|
+
<h1
|
|
29
|
+
data-testid="tsp-page-header-title"
|
|
30
|
+
class="text-2xl font-bold text-highlighted"
|
|
31
|
+
>
|
|
32
|
+
{{ title }}
|
|
33
|
+
</h1>
|
|
34
|
+
<!-- Gated, not just empty: an unconditional <p> keeps its margin and
|
|
35
|
+
pushes the page's first card down by a line that is not there. -->
|
|
36
|
+
<p
|
|
37
|
+
v-if="description"
|
|
38
|
+
data-testid="tsp-page-header-description"
|
|
39
|
+
class="mt-2 max-w-2xl text-muted"
|
|
40
|
+
>
|
|
41
|
+
{{ description }}
|
|
42
|
+
</p>
|
|
43
|
+
</header>
|
|
44
|
+
</template>
|
|
45
|
+
|
|
46
|
+
<script setup lang="ts">
|
|
47
|
+
defineProps<{
|
|
48
|
+
/** The page title. Rendered as the page's one `<h1>`. */
|
|
49
|
+
title: string
|
|
50
|
+
/** One line under the title. Capped at `max-w-2xl` โ it is prose, the page is not. */
|
|
51
|
+
description?: string
|
|
52
|
+
}>()
|
|
53
|
+
</script>
|
package/docs/api.md
CHANGED
|
@@ -5,13 +5,43 @@
|
|
|
5
5
|
drift from the components. For what each component is *for*, and the rules that govern
|
|
6
6
|
it, read [`design-system.md`](design-system.md) โ this file is the signature only.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
32 components ยท 137 props ยท 4 composables.
|
|
9
9
|
|
|
10
10
|
Every component is auto-imported; nothing needs an explicit import in a consuming app.
|
|
11
11
|
A prop marked required has no default and the component will not render correctly without it.
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
+
## TspAccountPage
|
|
16
|
+
|
|
17
|
+
A service's account page, whole.
|
|
18
|
+
|
|
19
|
+
| Prop | Type | Default | |
|
|
20
|
+
|---|---|---|---|
|
|
21
|
+
| `name` | `string` | **required** | Display name. |
|
|
22
|
+
| `title` | `string` | `'Account'` | The page title. |
|
|
23
|
+
| `description` | `string` | `undefined` | One line under the title. Omit for none. |
|
|
24
|
+
| `accountLabel` | `string` | `undefined` | Which account this is โ shown under the name. |
|
|
25
|
+
| `accountId` | `string` | `undefined` | The account id. Omit to hide the whole id block. |
|
|
26
|
+
| `idLabel` | `string` | `undefined` | Label over the account id. |
|
|
27
|
+
| `manageLink` | `string` | `undefined` | Where โmanage accountโ goes. Omit to hide the link. |
|
|
28
|
+
| `manageLabel` | `string` | `undefined` | Text of the manage-account link. |
|
|
29
|
+
| `revealLabel` | `string` | `undefined` | Accessible name for the eye while the id is hidden. |
|
|
30
|
+
| `hideLabel` | `string` | `undefined` | Accessible name for it while the id is shown. |
|
|
31
|
+
|
|
32
|
+
## TspAppShell
|
|
33
|
+
|
|
34
|
+
The logged-in frame: root, sidebar, content column, mobile top bar, measure.
|
|
35
|
+
|
|
36
|
+
| Prop | Type | Default | |
|
|
37
|
+
|---|---|---|---|
|
|
38
|
+
| `appName` | `string` | `undefined` | The service's name. Renders the top bar's wordmark โ override with `#top-brand`. |
|
|
39
|
+
| `containerWidth` | `'app' \| 'site'` | `'app'` | Forwarded to `TspContainer`: `app` (1152px) or `site` (1240px). |
|
|
40
|
+
| `scroll` | `'page' \| 'inner'` | `'page'` | `page` scrolls the document and leaves the sidebar sticky โ the model this layer documents and the one the mobile drawer was built against. `inner` pins the frame to the viewport and scrolls `<main>` instead, which is what trips, gage and pogo run today; it is a real choice for a dense table view, so adopting this shell is not forced to be a redesign as well. |
|
|
41
|
+
| `menuLabel` | `string` | `undefined` | Accessible name for the mobile burger. Forwarded to `TspTopBar`. |
|
|
42
|
+
|
|
43
|
+
**Slots** โ default ยท `#brand` ยท `#footer` ยท `#nav` ยท `#top-brand`
|
|
44
|
+
|
|
15
45
|
## TspBanner
|
|
16
46
|
|
|
17
47
|
A dismissible strip across the top of the content area. Its first job is the release announcement the tsp-release skill's `showBanner` flag turns on, but nothing here is release-specific: it is a banner.
|
|
@@ -111,7 +141,46 @@ DE/EN segmented control โ the component gage, trips, pogo and tsp-tools each b
|
|
|
111
141
|
| `groupLabel` | `string` | `'Language'` | aria-label for the group. |
|
|
112
142
|
| `labels` | `Record<string, string>` | `() => ({ de: 'German', en: 'English' })` | Accessible name per code (aria-label + title). Falls back to the code. |
|
|
113
143
|
|
|
114
|
-
**Emits** โ `
|
|
144
|
+
**Emits** โ `update:modelValue: [code: string]`
|
|
145
|
+
|
|
146
|
+
## TspMcpSettings
|
|
147
|
+
|
|
148
|
+
The MCP connection page for a service that exposes one, whole.
|
|
149
|
+
|
|
150
|
+
| Prop | Type | Default | |
|
|
151
|
+
|---|---|---|---|
|
|
152
|
+
| `endpoint` | `string` | **required** | The MCP endpoint URL. Shown, copied, and written into the config snippet. |
|
|
153
|
+
| `apiKey` | `string` | **required** | The user's key. Never rendered while masked โ only the copy carries it. |
|
|
154
|
+
| `serverName` | `string` | `'tsp'` | Key under `mcpServers` in the snippet. Use the service's short name. |
|
|
155
|
+
| `showWarning` | `boolean` | `true` | The third-party-processing warning. On unless a service decides otherwise. |
|
|
156
|
+
| `labels` | `TspMcpLabels` | `() => ({})` | Overrides for any visible string. Partial; English defaults for the rest. |
|
|
157
|
+
|
|
158
|
+
**Emits** โ `regenerate: []` โ The user asked for a new key. Rotating it is the service's job.
|
|
159
|
+
|
|
160
|
+
**`TspMcpLabels`**
|
|
161
|
+
|
|
162
|
+
| Field | Type | |
|
|
163
|
+
|---|---|---|
|
|
164
|
+
| `serverTitle?` | `string` | Title of the first card. |
|
|
165
|
+
| `endpoint?` | `string` | Label on the endpoint field. |
|
|
166
|
+
| `apiKey?` | `string` | Label on the key field. |
|
|
167
|
+
| `apiKeyHelp?` | `string` | The line under the key โ say it is a password. |
|
|
168
|
+
| `regenerate?` | `string` | The rotate-my-key control. Only rendered when `@regenerate` is bound. |
|
|
169
|
+
| `warning?` | `string` | The third-party-processing warning. |
|
|
170
|
+
| `claudeTitle?` | `string` | Title of the Claude card. |
|
|
171
|
+
| `claudeHelp?` | `string` | How to install the snippet in Claude. |
|
|
172
|
+
| `keyPlaceholder?` | `string` | Stands in for the key in the SHOWN snippet. The copy has the real one. |
|
|
173
|
+
| `copyWithKey?` | `string` | Snippet copy button at rest. |
|
|
174
|
+
| `copiedWithKey?` | `string` | Snippet copy button just after a copy. |
|
|
175
|
+
| `codeHelp?` | `string` | The line under the snippet, explaining the placeholder. |
|
|
176
|
+
| `genericTitle?` | `string` | Title of the any-other-client card. |
|
|
177
|
+
| `genericHelp?` | `string` | What to do with the two values below it. |
|
|
178
|
+
| `serverUrl?` | `string` | Label on the server-URL field. |
|
|
179
|
+
| `authHeader?` | `string` | Label on the Authorization-header field. |
|
|
180
|
+
| `reveal?` | `string` | Accessible name for the eye while a secret is hidden. |
|
|
181
|
+
| `hide?` | `string` | Accessible name for it while the secret is shown. |
|
|
182
|
+
| `copy?` | `string` | Accessible name for every field's copy button at rest. |
|
|
183
|
+
| `copied?` | `string` | And just after a successful copy. |
|
|
115
184
|
|
|
116
185
|
## TspNavBadge
|
|
117
186
|
|
|
@@ -179,6 +248,15 @@ A child row inside a TspNavGroup. Carries its own icon, in its own indented colu
|
|
|
179
248
|
|
|
180
249
|
**Slots** โ default
|
|
181
250
|
|
|
251
|
+
## TspPageHeader
|
|
252
|
+
|
|
253
|
+
The page title, and optionally the line under it.
|
|
254
|
+
|
|
255
|
+
| Prop | Type | Default | |
|
|
256
|
+
|---|---|---|---|
|
|
257
|
+
| `title` | `string` | **required** | The page title. Rendered as the page's one `<h1>`. |
|
|
258
|
+
| `description` | `string` | `undefined` | One line under the title. Capped at `max-w-2xl` โ it is prose, the page is not. |
|
|
259
|
+
|
|
182
260
|
## TspPageTabs
|
|
183
261
|
|
|
184
262
|
Tabs across the top of an area, above the page title โ the shape trips uses for Settings / MCP Server.
|
|
@@ -199,6 +277,28 @@ Release notes for one or more versions โ the overview the tsp-release skill wr
|
|
|
199
277
|
| `beforeLabel` | `string` | `'Before'` | Caption on the left side of a before/after pair. |
|
|
200
278
|
| `afterLabel` | `string` | `'After'` | Caption on its right side. |
|
|
201
279
|
|
|
280
|
+
**`TspReleaseItemObject`**
|
|
281
|
+
|
|
282
|
+
| Field | Type | |
|
|
283
|
+
|---|---|---|
|
|
284
|
+
| `text` | `string` | |
|
|
285
|
+
| `compare?` | `{ before: string, after: string }` | |
|
|
286
|
+
| `beforeImage?` | `string` | |
|
|
287
|
+
| `afterImage?` | `string` | |
|
|
288
|
+
|
|
289
|
+
**`TspRelease`**
|
|
290
|
+
|
|
291
|
+
| Field | Type | |
|
|
292
|
+
|---|---|---|
|
|
293
|
+
| `version` | `string` | |
|
|
294
|
+
| `date?` | `string` | |
|
|
295
|
+
| `showBanner?` | `boolean` | |
|
|
296
|
+
| `showPopup?` | `boolean` | |
|
|
297
|
+
| `important?` | `TspReleaseItem[]` | |
|
|
298
|
+
| `other?` | `TspReleaseItem[]` | |
|
|
299
|
+
|
|
300
|
+
**`TspReleaseItem`** โ `string \| TspReleaseItemObject`
|
|
301
|
+
|
|
202
302
|
## TspSectionCard
|
|
203
303
|
|
|
204
304
|
A titled content card: icon + title in a header row, content below.
|
|
@@ -261,7 +361,7 @@ The sidebar bottom block, in the order spec 03 fixes:
|
|
|
261
361
|
|
|
262
362
|
**Slots** โ `#logout` ยท `#user`
|
|
263
363
|
|
|
264
|
-
**Emits** โ `
|
|
364
|
+
**Emits** โ `update:modelValue: [code: string]`
|
|
265
365
|
|
|
266
366
|
## TspSiteFooter
|
|
267
367
|
|
package/docs/design-system.md
CHANGED
|
@@ -118,6 +118,25 @@ global and would reach a scoped consumer's unthemed area. There is no `TspCard`.
|
|
|
118
118
|
Nunito everywhere; Space Grotesk for the wordmark only (`.tsp-wordmark` / `<TspWordmark>`).
|
|
119
119
|
Both from Google via `@nuxt/fonts`, which ships with Nuxt UI.
|
|
120
120
|
|
|
121
|
+
### Typography
|
|
122
|
+
|
|
123
|
+
The scale a page is built from. It went unwritten until 0.4.0 and a consuming service duly
|
|
124
|
+
guessed `text-[28px]` for its page title โ so it is stated here and, more usefully, **carried by
|
|
125
|
+
`<TspPageHeader>`**, which is what a page should use rather than reproducing the classes.
|
|
126
|
+
|
|
127
|
+
| Role | Classes |
|
|
128
|
+
|---|---|
|
|
129
|
+
| Page title (`h1`) | `text-2xl font-bold text-highlighted` |
|
|
130
|
+
| Page lede | `text-muted max-w-2xl` |
|
|
131
|
+
| Card title (`h2`, inside `TspSectionCard`) | `text-sm font-bold text-highlighted` |
|
|
132
|
+
| Section label above a group | `text-xs font-bold uppercase tracking-wide text-muted` |
|
|
133
|
+
| Field label | `text-xs font-bold text-toned` |
|
|
134
|
+
| Body | inherited โ `text-default`, or `text-muted` for supporting prose |
|
|
135
|
+
|
|
136
|
+
**Cap prose on the paragraph, not the page** (`max-w-2xl`), for the reason under *Layout*.
|
|
137
|
+
There is no `h1` larger than `text-2xl` in an app shell: the marketing surface has its own
|
|
138
|
+
hero scale and is not this.
|
|
139
|
+
|
|
121
140
|
### Icons
|
|
122
141
|
|
|
123
142
|
Tabler, `i-tabler-*`. Install `@iconify-json/tabler` for offline resolution. **One icon family
|
|
@@ -157,6 +176,15 @@ is centred, so without it the whole page slides sideways by half a scrollbar the
|
|
|
157
176
|
is tall enough to scroll. Browsers with overlay scrollbars are unaffected, and a consumer's
|
|
158
177
|
unthemed public pages are deliberately not touched.
|
|
159
178
|
|
|
179
|
+
**The frame is `TspAppShell`, not fifteen lines in your layout.** It renders the root, the
|
|
180
|
+
sidebar, the content column, the mobile top bar and the container, and it derives its root
|
|
181
|
+
element โ plain `div` globally, `TspThemeProvider` when scoped โ from `apply` rather than from a
|
|
182
|
+
prop. Before it existed, all eight services had written that frame out by hand and disagreed
|
|
183
|
+
three ways nobody had chosen: two scroll models, three viewport units for one intent
|
|
184
|
+
(`min-h-svh` vs `h-svh` vs `min-h-screen`, which is `100vh` and breaks under a mobile toolbar),
|
|
185
|
+
and `bg-default` on the content column or not. `scroll="inner"` keeps the pinned-frame model for
|
|
186
|
+
a dense table view; the default scrolls the page and leaves the sidebar sticky.
|
|
187
|
+
|
|
160
188
|
**Responsive.** `sm` (640px) is the shell's breakpoint. At and above it the sidebar is a column,
|
|
161
189
|
sticky at viewport height. Below it the sidebar becomes an off-canvas drawer and `<TspTopBar>`
|
|
162
190
|
appears.
|
|
@@ -172,7 +200,8 @@ has โ a slot invites each app to restyle its copy, which is the drift the laye
|
|
|
172
200
|
|
|
173
201
|
| Component | Purpose |
|
|
174
202
|
|---|---|
|
|
175
|
-
| `
|
|
203
|
+
| `TspAppShell` | **The frame, whole** โ root, sidebar, content column, top bar, container. Start here. |
|
|
204
|
+
| `TspSidebar` | The sidebar alone. Slots `#brand` `#nav` `#footer`. Sticky on desktop, drawer below `sm`. |
|
|
176
205
|
| `TspTopBar` | Mobile only: burger (left) ยท brand ยท page title. |
|
|
177
206
|
| `TspSidebarFooter` | User โ Settings โ Logout โ divider โ theme + language โ meta icons + version โ attribution. |
|
|
178
207
|
| `TspFooterRow` | One footer row; `to` / `href` / neither (button). |
|
|
@@ -194,6 +223,35 @@ has โ a slot invites each app to restyle its copy, which is the drift the laye
|
|
|
194
223
|
| `TspSiteHeader` | Slots `#brand` `#nav` `#actions`; `sticky`, and `bare` for a landing page (no bar). |
|
|
195
224
|
| `TspSiteFooter` | Compact centred link row; `tool-of` adds the attribution. |
|
|
196
225
|
|
|
226
|
+
### Pages
|
|
227
|
+
|
|
228
|
+
Three components are whole pages, or the top of one. They exist because the layer being the
|
|
229
|
+
source of the *shell* was never enough: a service still had to assemble each page from parts,
|
|
230
|
+
and the parts went together differently every time. The MCP page reached four copies in three
|
|
231
|
+
shapes โ including two inside this repo that had drifted apart from each other โ before it
|
|
232
|
+
became a component (spec 07).
|
|
233
|
+
|
|
234
|
+
| Component | Purpose |
|
|
235
|
+
|---|---|
|
|
236
|
+
| `TspPageHeader` | The page title and its lede. Every page starts with it; the scale above is its. |
|
|
237
|
+
| `TspAccountPage` | The standalone account page โ heading + `TspUserCard`. |
|
|
238
|
+
| `TspMcpSettings` | The whole MCP connection page: server, the AI-processing warning, Claude, generic. |
|
|
239
|
+
|
|
240
|
+
**Use the page, not the recipe.** If you find yourself rebuilding one of these out of
|
|
241
|
+
`TspSectionCard` and `TspCopyField`, that is the drift these exist to stop โ and if the page
|
|
242
|
+
genuinely cannot do what your service needs, the fix belongs here, not in your app (ADR 017).
|
|
243
|
+
|
|
244
|
+
Two rules they encode, worth knowing before overriding either:
|
|
245
|
+
|
|
246
|
+
- **`TspMcpSettings` renders the third-party-processing warning by default.** The user is about
|
|
247
|
+
to hand their data to someone else's model. `:show-warning="false"` exists, so leaving it out
|
|
248
|
+
is a decision somebody made rather than a card nobody copied.
|
|
249
|
+
- **The Claude snippet is redacted on screen and complete on copy.** Printing the real key
|
|
250
|
+
there would defeat the masking one card above it.
|
|
251
|
+
|
|
252
|
+
Inside a tabbed settings area the heading belongs to the parent route โ use `TspPageHeader`
|
|
253
|
+
there and `TspUserCard` directly, rather than `TspAccountPage` under the tabs.
|
|
254
|
+
|
|
197
255
|
### Content
|
|
198
256
|
|
|
199
257
|
| Component | Purpose |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thespielplatz/tsp-tools-theme",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
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",
|