@thespielplatz/tsp-tools-theme 0.2.0 → 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.
Files changed (39) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/README.md +147 -32
  3. package/assets/css/theme.css +86 -6
  4. package/components/TspBanner.vue +97 -0
  5. package/components/TspBrandTile.vue +46 -0
  6. package/components/TspCodeBlock.vue +68 -0
  7. package/components/TspContainer.vue +10 -2
  8. package/components/TspCopyButton.vue +40 -0
  9. package/components/TspCopyField.vue +102 -0
  10. package/components/TspFooterRow.vue +56 -0
  11. package/components/TspLanguageToggle.vue +75 -0
  12. package/components/TspNavBadge.vue +36 -0
  13. package/components/TspNavGroup.vue +58 -0
  14. package/components/TspNavItem.vue +68 -12
  15. package/components/TspNavLabel.vue +56 -0
  16. package/components/TspNavSection.vue +33 -0
  17. package/components/TspNavSubItem.vue +35 -0
  18. package/components/TspPageTabs.vue +46 -0
  19. package/components/TspReleaseNotes.vue +143 -0
  20. package/components/TspSectionCard.vue +52 -0
  21. package/components/TspSidebar.vue +96 -3
  22. package/components/TspSidebarFooter.vue +332 -37
  23. package/components/TspSiteFooter.vue +47 -16
  24. package/components/TspSiteHeader.vue +18 -2
  25. package/components/TspThemeToggle.vue +24 -5
  26. package/components/TspToolOf.vue +42 -0
  27. package/components/TspTopBar.vue +69 -0
  28. package/components/TspUserCard.vue +127 -0
  29. package/components/TspVersionBadge.vue +49 -0
  30. package/components/TspWordmark.vue +36 -4
  31. package/composables/useTspCopy.ts +72 -0
  32. package/composables/useTspNavDrawer.ts +14 -0
  33. package/composables/useTspReveal.ts +18 -0
  34. package/docs/api.md +394 -0
  35. package/docs/design-system.md +297 -0
  36. package/nuxt.config.ts +11 -0
  37. package/package.json +24 -11
  38. package/page-meta.d.ts +11 -0
  39. package/plugins/01.tspColorMode.ts +12 -0
package/docs/api.md ADDED
@@ -0,0 +1,394 @@
1
+ # API reference
2
+
3
+ **Generated from the source by `scripts/gen-api.mjs` — do not edit.**
4
+ `npm run docs:api` rewrites it; `npm run docs:api:check` is in the gate, so it cannot
5
+ drift from the components. For what each component is *for*, and the rules that govern
6
+ it, read [`design-system.md`](design-system.md) — this file is the signature only.
7
+
8
+ 28 components · 116 props · 4 composables.
9
+
10
+ Every component is auto-imported; nothing needs an explicit import in a consuming app.
11
+ A prop marked required has no default and the component will not render correctly without it.
12
+
13
+ ---
14
+
15
+ ## TspBanner
16
+
17
+ 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.
18
+
19
+ | Prop | Type | Default | |
20
+ |---|---|---|---|
21
+ | `title` | `string` | `undefined` | Headline. Without it the banner is only its default slot. |
22
+ | `icon` | `string` | `undefined` | Leading icon, `i-tabler-*`. |
23
+ | `tone` | `'brand' \| 'neutral'` | `'brand'` | `brand` = amber accent edge, `neutral` = quiet. Neither is a fill. |
24
+ | `dismissible` | `boolean` | `true` | Show the close button. The app decides whether it STAYS closed. |
25
+ | `dismissLabel` | `string` | `'Dismiss'` | Accessible name for the close button. |
26
+
27
+ **Slots** — default · `#action`
28
+
29
+ **Emits** — `dismiss: []`
30
+
31
+ ## TspBrandTile
32
+
33
+ The frame a service's mark sits in — size, radius and clipping. The artwork goes in the default slot; the layer ships no logo of its own and must not grow one (spec 02).
34
+
35
+ | Prop | Type | Default | |
36
+ |---|---|---|---|
37
+ | `size` | `'sidebar' \| 'hero'` | `'sidebar'` | `sidebar` = 24px (TspSidebar #brand), `hero` = 64px / 96px from sm up. |
38
+
39
+ **Slots** — default
40
+
41
+ ## TspCodeBlock
42
+
43
+ A block of configuration to copy — a JSON snippet, a shell line.
44
+
45
+ | Prop | Type | Default | |
46
+ |---|---|---|---|
47
+ | `code` | `string` | **required** | What is SHOWN. |
48
+ | `copyValue` | `string` | `undefined` | What is COPIED, when that differs — e.g. the same config with the real key. |
49
+ | `copyLabel` | `string` | `'Copy'` | Button label at rest. |
50
+ | `copiedLabel` | `string` | `'Copied'` | Button label for the moment after a successful copy. |
51
+ | `help` | `string` | `undefined` | One muted line under the block — e.g. what the placeholder stands for. |
52
+
53
+ ## TspContainer
54
+
55
+ Centered content column.
56
+
57
+ | Prop | Type | Default | |
58
+ |---|---|---|---|
59
+ | `width` | `'app' \| 'site'` | `'app'` | `app` = 1152px, the logged-in measure. `site` = 1240px, the marketing one. |
60
+ | `padded` | `boolean` | `true` | Vertical rhythm (`pt-10 pb-16`). Off for header/footer frames. |
61
+
62
+ **Slots** — default
63
+
64
+ ## TspCopyButton
65
+
66
+ Copy-to-clipboard button. Confirms by swapping its own icon for a tick for a moment — a copy that gives no feedback leaves you pressing it twice.
67
+
68
+ | Prop | Type | Default | |
69
+ |---|---|---|---|
70
+ | `value` | `string` | **required** | The string put on the clipboard. |
71
+ | `label` | `string` | `'Copy'` | Button label at rest. |
72
+ | `copiedLabel` | `string` | `'Copied'` | Button label for the moment after a successful copy. |
73
+
74
+ ## TspCopyField
75
+
76
+ A read-only value with a copy button — an endpoint, a server URL, an API key.
77
+
78
+ | Prop | Type | Default | |
79
+ |---|---|---|---|
80
+ | `value` | `string` | **required** | What is shown and copied. Not rendered at all while masked. |
81
+ | `label` | `string` | `undefined` | Field label above the value. |
82
+ | `help` | `string` | `undefined` | One muted line under the field. |
83
+ | `secret` | `boolean` | `false` | Mask the value behind a reveal. |
84
+ | `revealLabel` | `string` | `'Show'` | Accessible name for the eye while the value is hidden. |
85
+ | `hideLabel` | `string` | `'Hide'` | Accessible name for the eye while the value is shown. |
86
+ | `copyLabel` | `string` | `'Copy'` | Accessible name for the copy button at rest. |
87
+ | `copiedLabel` | `string` | `'Copied'` | Accessible name for it just after a successful copy. |
88
+
89
+ **Slots** — `#help`
90
+
91
+ ## TspFooterRow
92
+
93
+ One row of the sidebar footer nav — icon + label, styled exactly like a TspNavItem at rest. This exists because gage, pogo and trips each carried the same `footerRowClass` string in their own layout; no consumer should write it again (theme 0.3.0).
94
+
95
+ | Prop | Type | Default | |
96
+ |---|---|---|---|
97
+ | `to` | `string` | `undefined` | Internal route — renders a NuxtLink. |
98
+ | `href` | `string` | `undefined` | External or non-router href — renders an <a>. |
99
+ | `icon` | `string` | `undefined` | Tabler icon name. Every footer row carries one. |
100
+
101
+ **Slots** — default
102
+
103
+ ## TspLanguageToggle
104
+
105
+ DE/EN segmented control — the component gage, trips, pogo and tsp-tools each built by hand (theme 0.3.0).
106
+
107
+ | Prop | Type | Default | |
108
+ |---|---|---|---|
109
+ | `modelValue` | `string` | `undefined` | The active locale code. |
110
+ | `locales` | `string[]` | `() => ['de', 'en']` | The codes to offer, in display order. |
111
+ | `groupLabel` | `string` | `'Language'` | aria-label for the group. |
112
+ | `labels` | `Record<string, string>` | `() => ({ de: 'German', en: 'English' })` | Accessible name per code (aria-label + title). Falls back to the code. |
113
+
114
+ **Emits** — `'update:modelValue': [code: string]`
115
+
116
+ ## TspNavBadge
117
+
118
+ The small pill on a nav row — stash's OWNER / MEMBER (spec 05).
119
+
120
+ | Prop | Type | Default | |
121
+ |---|---|---|---|
122
+ | `tone` | `'primary' \| 'neutral'` | `'neutral'` | `primary` = amber outline, `neutral` = grey. Outline only — a fill in the sidebar would read as a primary button (ADR 018). |
123
+
124
+ **Slots** — default
125
+
126
+ ## TspNavGroup
127
+
128
+ A nav item that owns sub-items (spec 04, variant A).
129
+
130
+ | Prop | Type | Default | |
131
+ |---|---|---|---|
132
+ | `to` | `string` | **required** | Route of the parent row itself — the group is a destination, not just a heading. |
133
+ | `icon` | `string` | `undefined` | Tabler icon name for the parent row. |
134
+ | `label` | `string` | **required** | Text of the parent row. |
135
+ | `badge` | `string` | `undefined` | Optional pill on the right, e.g. a role. |
136
+ | `badgeTone` | `'primary' \| 'neutral'` | `'neutral'` | Tone of that pill; see TspNavBadge. |
137
+
138
+ **Slots** — default
139
+
140
+ ## TspNavItem
141
+
142
+ Sidebar nav link: WHITE at rest, amber TEXT on a transparent background when active. Pass a Tabler icon name (`i-tabler-*`) and the route.
143
+
144
+ | Prop | Type | Default | |
145
+ |---|---|---|---|
146
+ | `to` | `string` | `undefined` | Internal route — renders a NuxtLink. |
147
+ | `href` | `string` | `undefined` | External or non-router destination — renders an <a>. A section labelled "External" whose rows cannot leave the app is not much of a section. |
148
+ | `target` | `string` | `undefined` | Only with `href`. Any target that leaves this context also gets `rel="noopener noreferrer"`. |
149
+ | `icon` | `string` | `undefined` | Tabler icon name. Every nav row carries one. |
150
+ | `badge` | `string` | `undefined` | Optional pill on the right of the row, e.g. a role. |
151
+ | `badgeTone` | `'primary' \| 'neutral'` | `'neutral'` | Tone of the badge; see TspNavBadge. |
152
+
153
+ **Slots** — default
154
+
155
+ ## TspNavLabel
156
+
157
+ A nav row's label: truncated to the sidebar, with the full text revealed on hover after a short pause.
158
+
159
+ No props.
160
+
161
+ **Slots** — default
162
+
163
+ ## TspNavSection
164
+
165
+ A break in the nav: a rule, optionally followed by a small label.
166
+
167
+ | Prop | Type | Default | |
168
+ |---|---|---|---|
169
+ | `label` | `string` | `undefined` | Omit for a bare divider. |
170
+
171
+ ## TspNavSubItem
172
+
173
+ A child row inside a TspNavGroup. Carries its own icon, in its own indented column — the level is already carried by the indent, the spine and the muting, so the icon does not have to do that work too.
174
+
175
+ | Prop | Type | Default | |
176
+ |---|---|---|---|
177
+ | `to` | `string` | **required** | Internal route. A child row is always internal. |
178
+ | `icon` | `string` | `undefined` | Tabler icon name, e.g. `i-tabler-list-details`. |
179
+
180
+ **Slots** — default
181
+
182
+ ## TspPageTabs
183
+
184
+ Tabs across the top of an area, above the page title — the shape trips uses for Settings / MCP Server.
185
+
186
+ | Prop | Type | Default | |
187
+ |---|---|---|---|
188
+ | `items` | `{ label: string, to: string, icon?: string }[]` | **required** | One entry per view: `{ label, to, icon? }`. `to` is a ROUTE, so the back button works. |
189
+
190
+ ## TspReleaseNotes
191
+
192
+ Release notes for one or more versions — the overview the tsp-release skill writes, rendered.
193
+
194
+ | Prop | Type | Default | |
195
+ |---|---|---|---|
196
+ | `releases` | `TspRelease[]` | **required** | Newest first. Mirrors the `tsp-release` skill's RELEASES shape, so a service can pass its own array straight through. |
197
+ | `importantLabel` | `string` | `'Highlights'` | Heading over the `important` items. |
198
+ | `otherLabel` | `string` | `'Also in this release'` | Heading over the `other` items. |
199
+ | `beforeLabel` | `string` | `'Before'` | Caption on the left side of a before/after pair. |
200
+ | `afterLabel` | `string` | `'After'` | Caption on its right side. |
201
+
202
+ ## TspSectionCard
203
+
204
+ A titled content card: icon + title in a header row, content below.
205
+
206
+ | Prop | Type | Default | |
207
+ |---|---|---|---|
208
+ | `title` | `string` | `undefined` | Short card title. Omit for an untitled card — no empty heading is emitted. |
209
+ | `icon` | `string` | `undefined` | Tabler icon name. Amber, because it labels rather than decorates. |
210
+
211
+ **Slots** — default
212
+
213
+ ## TspSidebar
214
+
215
+ App-shell sidebar frame (the generic structure from the piggybank admin). The app supplies content via slots; no app-specific branding/nav lives here.
216
+
217
+ No props.
218
+
219
+ **Slots** — `#brand` · `#footer` · `#nav`
220
+
221
+ ## TspSidebarFooter
222
+
223
+ The sidebar bottom block, in the order spec 03 fixes:
224
+
225
+ | Prop | Type | Default | |
226
+ |---|---|---|---|
227
+ | `user` | `string` | `undefined` | The username. Plain text, no icon. Use #user only if you need more. |
228
+ | `userLabel` | `string` | `'User'` | Accessible name for the user row. |
229
+ | `userIcon` | `string` | `'i-tabler-user-circle'` | Icon for the user row. Pass `undefined` for a bare name. |
230
+ | `userTo` | `string` | `undefined` | Route to the user page. Given, the row becomes a link and can go active. |
231
+ | `settingsTo` | `string` | `undefined` | Route to the settings page. Omit to drop the row. |
232
+ | `settingsLabel` | `string` | `'Settings'` | Visible text of the settings row. |
233
+ | `logoutTo` | `string` | `undefined` | Internal logout route. Use this or `logoutHref`, not both. |
234
+ | `logoutHref` | `string` | `undefined` | External logout URL — the usual case with a hosted identity provider. |
235
+ | `logoutLabel` | `string` | `'Logout'` | Visible text of the logout row. |
236
+ | `modelValue` | `string` | `undefined` | Active locale (v-model). The language toggle renders only with `locales`. |
237
+ | `locales` | `string[]` | `undefined` | Locale codes, e.g. `['de', 'en']`. The language row appears only with this — not every service is translated. |
238
+ | `languageGroupLabel` | `string` | `'Language'` | Accessible name for the locale group. There is no visible label. |
239
+ | `languageLabels` | `Record<string, string>` | `undefined` | Accessible names per code, e.g. `{ de: 'Deutsch' }`. The button itself shows the code. |
240
+ | `githubLink` | `string` | `undefined` | Presence of this is what makes the project open source. |
241
+ | `githubLabel` | `string` | `'GitHub'` | Accessible name for the GitHub icon. |
242
+ | `supportEmail` | `string` | `'support@tsp.tools'` | Support address. One platform mailbox, so it has a default. |
243
+ | `supportLabel` | `string` | `'Support'` | Accessible name for the support icon. |
244
+ | `supportSubject` | `string` | `undefined` | Subject line. Defaults to one naming this app, so support can tell which. |
245
+ | `supportBody` | `string` | `undefined` | First line of the body. Defaults to app + version, for the same reason. |
246
+ | `appName` | `string` | `undefined` | This app's name, used in the support subject/body and nothing else. |
247
+ | `tipJar` | `boolean` | `false` | The tip jar icon. Same destination for every service, so only a boolean. |
248
+ | `tipJarLabel` | `string` | `'Tip jar'` | Accessible name for the tip-jar icon. |
249
+ | `tipJarLink` | `string` | `'https://thespielplatz.com/tip-jar'` | Where it goes. One destination for the whole platform, so services do not set this. |
250
+ | `version` | `string` | `undefined` | Version string for the badge, e.g. `v0.3.0`. Omit to drop the badge. |
251
+ | `versionLink` | `string` | `undefined` | Release link for the badge. Ignored without `githubLink`. |
252
+ | `versionUnread` | `boolean` | `false` | Mark the version badge as having unseen release notes. |
253
+ | `toolOf` | `boolean` | `false` | The “A tool of tsp.tools” line. Off by default; only some services carry it. |
254
+ | `toolOfLabel` | `string` | `undefined` | Leading words of the attribution. Left undefined so TspToolOf owns the default. |
255
+ | `toolOfName` | `string` | `undefined` | The platform name in it. Same reason. |
256
+ | `toolOfLink` | `string` | `undefined` | Where that name points. Same reason. |
257
+ | `labelToLight` | `string` | `undefined` | Theme-toggle text while dark — it names the DESTINATION. |
258
+ | `labelToDark` | `string` | `undefined` | Theme-toggle text while light. |
259
+ | `ariaLabelToLight` | `string` | `undefined` | Its accessible name while dark. |
260
+ | `ariaLabelToDark` | `string` | `undefined` | Its accessible name while light. |
261
+
262
+ **Slots** — `#logout` · `#user`
263
+
264
+ **Emits** — `'update:modelValue': [code: string]`
265
+
266
+ ## TspSiteFooter
267
+
268
+ Marketing/public site footer (ADR 008, finding 2). Frame only: a top border and one centred row of links, with the platform attribution under it.
269
+
270
+ | Prop | Type | Default | |
271
+ |---|---|---|---|
272
+ | `toolOf` | `boolean` | `false` | The “A tool of tsp.tools” line. Off by default; only some services carry it. |
273
+ | `toolOfLabel` | `string` | `undefined` | Leading words of the attribution. Left undefined so TspToolOf owns the default. |
274
+ | `toolOfName` | `string` | `undefined` | The platform name in it. Same reason. |
275
+ | `toolOfLink` | `string` | `undefined` | Where that name points. Same reason. |
276
+
277
+ **Slots** — `#actions` · `#brand` · `#links`
278
+
279
+ ## TspSiteHeader
280
+
281
+ Marketing/public site header (ADR 008, finding 2 — the counterpart to TspSidebar, which only fits the logged-in shell). Frame only: sticky bar, bottom border, translucent backdrop, site-width column. The app supplies the content via slots; no branding, nav or copy lives here.
282
+
283
+ | Prop | Type | Default | |
284
+ |---|---|---|---|
285
+ | `sticky` | `boolean` | `true` | Keep the bar at the top of the viewport. Ignored when `bare`. |
286
+ | `bare` | `boolean` | `false` | No bar: no border, no background, not sticky. For landing pages. |
287
+
288
+ **Slots** — `#actions` · `#brand` · `#nav`
289
+
290
+ ## TspThemeProvider
291
+
292
+ Scoped-mode wrapper: marks a sub-area as themed. Everything inside resolves the tsp tokens (amber primary, anthracite/light surfaces); everything outside keeps default Nuxt UI. In global mode you don't need this — the colour-mode plugin puts `.tsp-theme` on <html> for you.
293
+
294
+ No props.
295
+
296
+ **Slots** — default
297
+
298
+ ## TspThemeToggle
299
+
300
+ Light/dark toggle. Shares the 32px row shape of everything else in the sidebar footer (spec 03 — it used to be text-xs/px-1 against the rows' text-sm/px-3, which is the mismatch spec 01 shipped knowingly).
301
+
302
+ | Prop | Type | Default | |
303
+ |---|---|---|---|
304
+ | `labelToLight` | `string` | `'Light'` | Visible label while dark (pressing switches to light). |
305
+ | `labelToDark` | `string` | `'Dark'` | Visible label while light. |
306
+ | `ariaLabelToLight` | `string` | `'Switch to light mode'` | aria-label while dark. |
307
+ | `ariaLabelToDark` | `string` | `'Switch to dark mode'` | aria-label while light. |
308
+ | `iconOnly` | `boolean` | `false` | Drop the visible label and render a compact icon button. |
309
+
310
+ ## TspToolOf
311
+
312
+ “A tool of tsp.tools” — the platform attribution line.
313
+
314
+ | Prop | Type | Default | |
315
+ |---|---|---|---|
316
+ | `label` | `string` | `'A tool of'` | The leading words. |
317
+ | `name` | `string` | `'tsp.tools'` | The platform, not the service. A service never names itself here. |
318
+ | `link` | `string` | `'https://tsp.tools'` | Where the name points. |
319
+
320
+ ## TspTopBar
321
+
322
+ Mobile top bar — only below `sm`, where the sidebar has become a drawer.
323
+
324
+ | Prop | Type | Default | |
325
+ |---|---|---|---|
326
+ | `title` | `string` | `undefined` | The current page's name. Nothing else on a mobile screen says where you are. Omitted, it is derived from the route. |
327
+ | `menuLabel` | `string` | `'Open menu'` | Accessible name for the burger button. |
328
+
329
+ **Slots** — `#brand`
330
+
331
+ ## TspUserCard
332
+
333
+ The account card on a service's user page — rebuilt from stash, which already runs this layout, so the layer now owns it instead of each service copying it (ADR 017: the shell lives here).
334
+
335
+ | Prop | Type | Default | |
336
+ |---|---|---|---|
337
+ | `name` | `string` | **required** | Display name. |
338
+ | `accountLabel` | `string` | `'tsp.tools account'` | Which account this is — shown under the name. |
339
+ | `accountId` | `string` | `undefined` | The account id. Omit to hide the whole id block. |
340
+ | `idLabel` | `string` | `'tsp.tools ID'` | Label over the account id. |
341
+ | `manageLink` | `string` | `undefined` | Where “manage account” goes. Omit to hide the link. |
342
+ | `manageLabel` | `string` | `'Manage account in Logto'` | Text of the manage-account link. |
343
+ | `revealLabel` | `string` | `'Show ID'` | Accessible name for the eye while the id is hidden. |
344
+ | `hideLabel` | `string` | `'Hide ID'` | Accessible name for it while the id is shown. |
345
+
346
+ ## TspVersionBadge
347
+
348
+ The version pill in the sidebar footer. A link when `href` is given, plain text otherwise (spec 03) — a closed-source project has no release page to point at, so its badge must not look clickable.
349
+
350
+ | Prop | Type | Default | |
351
+ |---|---|---|---|
352
+ | `version` | `string` | **required** | The version string, shown as-is, e.g. `v0.3.0`. |
353
+ | `href` | `string` | `undefined` | Release link. Omitted for closed-source projects, which renders a span. |
354
+ | `unread` | `boolean` | `undefined` | There are release notes the user has not seen. |
355
+
356
+ ## TspWordmark
357
+
358
+ Brand wordmark: the app's name in white, terminated by an amber dot.
359
+
360
+ | Prop | Type | Default | |
361
+ |---|---|---|---|
362
+ | `name` | `string` | **required** | The app name, lowercase. The amber dot is appended for you. |
363
+
364
+ ---
365
+
366
+ ## Composables
367
+
368
+ Auto-imported like the components.
369
+
370
+ ### useTspColorMode
371
+
372
+ The themed area's own colour-mode preference, remembered in a cookie so it is readable during SSR and survives reloads. Kept separate from @nuxtjs/color-mode's global state so that forcing the public area to `publicMode` (scoped mode) can never erase the user's choice.
373
+
374
+ Returns `{ pref, toggle }`.
375
+
376
+ Also exports: `useTspThemeOptions`.
377
+
378
+ ### useTspCopy
379
+
380
+ Copy text to the clipboard and confirm it for a moment. `navigator.clipboard` needs a SECURE CONTEXT. It is absent over plain HTTP on a LAN address, which is exactly how a service gets tested on a phone — so the fallback keeps the button working there instead of failing silently. `copied` returns to false on its own; a copy that gives no feedback gets pressed twice.
381
+
382
+ Returns `{ copied, copy }`.
383
+
384
+ ### useTspNavDrawer
385
+
386
+ Shared open/closed state for the mobile nav drawer, so TspTopBar's burger and TspSidebar do not have to be wired together by every app. useState, not a module-level ref: on the server a module-level ref is shared across requests, which leaks one visitor's open drawer into another's page.
387
+
388
+ Returns `{ isOpen, open, close, toggle }`.
389
+
390
+ ### useTspReveal
391
+
392
+ Show/hide a secret, with the placeholder to render while it is hidden. MASKING IS REAL, NOT COSMETIC: callers render `mask` INSTEAD of the value, never the value under a CSS cover. A covered value is still in the page source, still copyable, and still readable by anything walking the DOM — that is not masking, it only looks like it. The mask is a FIXED length. Deriving it from the value would leak how long the secret is, which is the sort of detail masking exists to withhold.
393
+
394
+ Returns `{ revealed, toggle, mask }`.