@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 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
- <div class="flex min-h-svh bg-default text-default">
93
- <TspSidebar>
94
- <template #brand>
95
- <TspBrandTile><img src="/logo.svg" alt="trips"></TspBrandTile>
96
- <TspWordmark name="trips" />
97
- </template>
98
- <template #nav>
99
- <TspNavItem to="/" icon="i-tabler-car" badge="Owner" badge-tone="primary">Fahrten</TspNavItem>
100
- <TspNavGroup to="/reports" icon="i-tabler-file-text" label="Berichte">
101
- <TspNavSubItem to="/reports/tax" icon="i-tabler-receipt">Steuer</TspNavSubItem>
102
- </TspNavGroup>
103
- <TspNavSection label="Extern" />
104
- <TspNavItem to="/handbook" icon="i-tabler-book">Handbuch</TspNavItem>
105
- </template>
106
- <template #footer>
107
- <TspSidebarFooter
108
- v-model="locale"
109
- app-name="trips"
110
- user="fil" user-to="/user"
111
- settings-to="/settings" logout-href="/auth/logout"
112
- :locales="['de', 'en']"
113
- github-link="https://github.com/โ€ฆ" version="v1.2.0" version-link="https://github.com/โ€ฆ/releases/tag/v1.2.0"
114
- tip-jar tool-of
115
- />
116
- </template>
117
- </TspSidebar>
118
- <main class="flex-1 min-w-0 bg-default">
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
- | `TspSidebar` | App-shell sidebar frame; slots `#brand` `#nav` `#footer`. |
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
- 28 components ยท 116 props ยท 4 composables.
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 `&lt;main&gt;` 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&lt;string, string&gt;` | `() =&gt; ({ de: 'German', en: 'English' })` | Accessible name per code (aria-label + title). Falls back to the code. |
113
143
 
114
- **Emits** โ€” `'update:modelValue': [code: string]`
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` | `() =&gt; ({})` | 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 `&lt;h1&gt;`. |
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** โ€” `'update:modelValue': [code: string]`
364
+ **Emits** โ€” `update:modelValue: [code: string]`
265
365
 
266
366
  ## TspSiteFooter
267
367
 
@@ -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
- | `TspSidebar` | The frame. Slots `#brand` `#nav` `#footer`. Sticky on desktop, drawer below `sm`. |
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.2",
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",