@leavepulse/ui 0.86.0 → 0.86.2

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 (38) hide show
  1. package/README.md +367 -72
  2. package/dist/assets/{LpSkeleton-H3Z5dEIk.css → LpSkeleton-D6GSatJ-.css} +15 -0
  3. package/dist/assets/{LpTable-ChCcmmtT.css → LpTable-CEFsRz5J.css} +19 -19
  4. package/dist/chunks/{LpAppShell-DW8JmY1o.js → LpAppShell-DpsQtXvG.js} +1 -1
  5. package/dist/chunks/{LpCard-BPtYXwnT.js → LpCard-xXBqLVGo.js} +1 -1
  6. package/dist/chunks/{LpFileTree-wiXExcZI.js → LpFileTree-B365yOLv.js} +1 -1
  7. package/dist/chunks/{LpModalNav-BLvDVl2-.js → LpModalNav-Er99S_AP.js} +1 -1
  8. package/dist/chunks/{LpNotificationBell-Bf0u3t5q.js → LpNotificationBell-DmR1xDd-.js} +1 -1
  9. package/dist/chunks/{LpNotificationFeed-BPZM3zkS.js → LpNotificationFeed-DK7D1f-v.js} +1 -1
  10. package/dist/chunks/{LpPie-Bzv8mnVR.js → LpPie-CAsGO9jE.js} +1 -1
  11. package/dist/chunks/{LpSidebar-DnqbKULj.js → LpSidebar-CjE90o3s.js} +1 -1
  12. package/dist/chunks/{LpSidebarNav-B3Eo_VIS.js → LpSidebarNav-Bgz2tXSq.js} +1 -1
  13. package/dist/chunks/{LpSkeleton-BB6XIV-w.js → LpSkeleton-leTl53V9.js} +1 -1
  14. package/dist/chunks/{LpSparkline-CExIRDb3.js → LpSparkline-11ryFdPX.js} +1 -1
  15. package/dist/chunks/{LpStat-BtxSto7K.js → LpStat-DM5Wfb6K.js} +1 -1
  16. package/dist/chunks/{LpUptimeBar-BoxcCDVY.js → LpUptimeBar-G7PfNJUH.js} +1 -1
  17. package/dist/components/LpAppShell.vue.js +1 -1
  18. package/dist/components/LpCard.vue.js +1 -1
  19. package/dist/components/LpChart.vue.js +1 -1
  20. package/dist/components/LpFileTree.vue.js +1 -1
  21. package/dist/components/LpModalNav.vue.js +1 -1
  22. package/dist/components/LpNotificationBell.vue.js +1 -1
  23. package/dist/components/LpNotificationFeed.vue.js +1 -1
  24. package/dist/components/LpPaletteFacts.vue.js +1 -1
  25. package/dist/components/LpPaletteStats.vue.js +1 -1
  26. package/dist/components/LpPie.vue.js +1 -1
  27. package/dist/components/LpSidebar.vue.js +1 -1
  28. package/dist/components/LpSidebarNav.vue.js +1 -1
  29. package/dist/components/LpSkeleton.vue.js +1 -1
  30. package/dist/components/LpSparkline.vue.js +1 -1
  31. package/dist/components/LpStat.vue.js +1 -1
  32. package/dist/components/LpTable.vue.js +344 -336
  33. package/dist/components/LpTabs.vue.js +1 -1
  34. package/dist/components/LpUptimeBar.vue.js +1 -1
  35. package/dist/index.js +12 -12
  36. package/package.json +3 -2
  37. package/src/components/LpSkeleton.vue +15 -0
  38. package/src/components/LpTable.vue +12 -0
package/README.md CHANGED
@@ -4,6 +4,14 @@ Token-driven Vue 3 component kit for LeavePulse — components on [reka-ui](http
4
4
  and [Tailwind CSS v4](https://tailwindcss.com), a runtime theme engine (themes are
5
5
  JSON token sets), and a composable block-layout canvas.
6
6
 
7
+ ## Contents
8
+
9
+ - [Install](#install) and [Usage](#usage)
10
+ - [Configuration](#configuration): theme, labels and languages, icons, Nuxt, overlays and Back, prop check, WebKitGTK glass, scrollbars
11
+ - [Components](#components): every component by group, with what it is for
12
+ - [Guides](#guides): linked views, workspaces, forms, tables, clipboard, video, command palette
13
+ - [Develop](#develop) and [Releasing](#releasing)
14
+
7
15
  ## Install
8
16
 
9
17
  Published to the public npm registry:
@@ -44,6 +52,15 @@ Apps using the topology canvas (`LpTopologyCanvas`) also import its stylesheet:
44
52
  @import "@leavepulse/ui/canvas.css";
45
53
  ```
46
54
 
55
+ ### Module format
56
+
57
+ The package is ESM only: `require("@leavepulse/ui")` is not supported. Import
58
+ from the package root; `@leavepulse/ui/dist/*.js` also resolves, for a tool
59
+ that needs one file (the Nuxt module does), and covers `components`,
60
+ `composables`, `layout` and `theme`.
61
+
62
+ ## Configuration
63
+
47
64
  ### Theme before mount (plain Vue)
48
65
 
49
66
  Themes are JSON token sets applied at runtime. Paint the saved one before the
@@ -65,7 +82,7 @@ app, a live preview). With `transition`, changing its `theme` reveals the new on
65
82
  with the same circular transition as the page switch, from where the user last
66
83
  pressed. `revealTransition(update, origin)` runs any repaint behind that reveal.
67
84
 
68
- ### App-wide configuration
85
+ ### Labels and languages
69
86
 
70
87
  `provideLpConfig` (or `app.use(lpConfig({...}))`) sets the link component that
71
88
  internal links render (`LpLink`, `LpButton` with `to`/`href`, `LpBreadcrumbs`,
@@ -105,6 +122,54 @@ new label that lacks a translation fails the type check. A test also fails when
105
122
  a `useLabels("scope", ...)` call has no entry in the pack. Writing a pack for
106
123
  another language against `KitLabels` gets the same checks.
107
124
 
125
+ #### Another language
126
+
127
+ Every piece of text a component renders belongs to a label scope (`table`,
128
+ `videoTrim`, `copyButton`…), and `KitLabels` lists them all with every key.
129
+ A pack written as `const de: KitLabels = { ... }` is checked the same way as
130
+ `uk`: a scope or a key it misses fails the type check, and a new label in a
131
+ later version shows up as an error instead of a stray English word. Partial
132
+ overrides need no type: pass only the scopes and keys you change.
133
+
134
+ ### Icons
135
+
136
+ `<LpIcon name="lucide:check">` renders offline, never through the iconify API.
137
+ The kit registers only the icons its own components use (about 20 KB instead of
138
+ the whole ~540 KB lucide set). Any other lucide name still renders: the full
139
+ collection loads as a separate chunk of your bundle the first time one is
140
+ needed, and an empty box of the same size holds the place until then.
141
+
142
+ Register icons up front when they must be in the server render or must not wait
143
+ for a chunk:
144
+
145
+ ```ts
146
+ import { registerIcons, registerIconCollection } from "@leavepulse/ui"
147
+
148
+ await registerIcons(["lucide:rocket", "lucide:server"]) // from the bundled lucide set
149
+ registerIconCollection(myIconsJson) // any iconify collection
150
+ import "@leavepulse/ui/icons-all" // the whole lucide set, as before
151
+ ```
152
+
153
+ In Nuxt the module does it for you: `icons: "scan"` (default) registers every
154
+ `lucide:*` literal found in the app's source, so they are in the SSR markup;
155
+ `"all"` registers the whole set, `"kit"` only the kit's own, or pass a list.
156
+
157
+ ### Nuxt
158
+
159
+ ```ts
160
+ export default defineNuxtConfig({
161
+ modules: ["@leavepulse/ui/nuxt"],
162
+ leavepulseUi: {
163
+ defaultTheme: "Nord", // fallback when nothing is saved
164
+ themes: "~/themes", // optional: module whose default export is your TokenSet[]
165
+ },
166
+ })
167
+ ```
168
+
169
+ The module registers every exported component, paints the saved theme during
170
+ SSR (from the `lp-theme` cookie, no flash), and provides `NuxtLink` as the link
171
+ component. `theme: false`, `link: false` and `exclude: ["Name"]` opt out.
172
+
108
173
  ### Back and Forward for overlays
109
174
 
110
175
  `provideOverlayHistory()` (at the app root, like `provideGlass()`) makes the
@@ -152,13 +217,6 @@ again, nothing remounts). A Forward onto a kit entry still reaches it that
152
217
  way, because the entry it starts from belongs to the router. Plain
153
218
  `pushState` and `popstate` only, so WebKit (Safari, WebKitGTK) works too.
154
219
 
155
- ### Module format
156
-
157
- The package is ESM only: `require("@leavepulse/ui")` is not supported. Import
158
- from the package root; `@leavepulse/ui/dist/*.js` also resolves, for a tool
159
- that needs one file (the Nuxt module does), and covers `components`,
160
- `composables`, `layout` and `theme`.
161
-
162
220
  ### Catching a misspelt prop
163
221
 
164
222
  A prop a component does not declare falls through as an attribute with no
@@ -176,45 +234,6 @@ in the app's tsconfig turns that on, but it also reports attributes passed
176
234
  through on purpose (`id`, `title`, `aria-*`), so it suits a one-off sweep better
177
235
  than a standing gate.
178
236
 
179
- ### Nuxt
180
-
181
- ```ts
182
- export default defineNuxtConfig({
183
- modules: ["@leavepulse/ui/nuxt"],
184
- leavepulseUi: {
185
- defaultTheme: "Nord", // fallback when nothing is saved
186
- themes: "~/themes", // optional: module whose default export is your TokenSet[]
187
- },
188
- })
189
- ```
190
-
191
- The module registers every exported component, paints the saved theme during
192
- SSR (from the `lp-theme` cookie, no flash), and provides `NuxtLink` as the link
193
- component. `theme: false`, `link: false` and `exclude: ["Name"]` opt out.
194
-
195
- ### Icons
196
-
197
- `<LpIcon name="lucide:check">` renders offline, never through the iconify API.
198
- The kit registers only the icons its own components use (about 20 KB instead of
199
- the whole ~540 KB lucide set). Any other lucide name still renders: the full
200
- collection loads as a separate chunk of your bundle the first time one is
201
- needed, and an empty box of the same size holds the place until then.
202
-
203
- Register icons up front when they must be in the server render or must not wait
204
- for a chunk:
205
-
206
- ```ts
207
- import { registerIcons, registerIconCollection } from "@leavepulse/ui"
208
-
209
- await registerIcons(["lucide:rocket", "lucide:server"]) // from the bundled lucide set
210
- registerIconCollection(myIconsJson) // any iconify collection
211
- import "@leavepulse/ui/icons-all" // the whole lucide set, as before
212
- ```
213
-
214
- In Nuxt the module does it for you: `icons: "scan"` (default) registers every
215
- `lucide:*` literal found in the app's source, so they are in the SSR markup;
216
- `"all"` registers the whole set, `"kit"` only the kit's own, or pass a list.
217
-
218
237
  ### Glass on WebKitGTK (Tauri on Linux)
219
238
 
220
239
  WebKitGTK reports `backdrop-filter` as supported and then draws nothing, and on
@@ -262,20 +281,215 @@ palette). Opt into the theme's colour the same way:
262
281
  @import "@leavepulse/ui/selection.css"; /* after the tokens — reads --color-brand */
263
282
  ```
264
283
 
265
- ## Develop
266
-
267
- ```bash
268
- bun install
269
- bun run dev # live preview/playground on http://127.0.0.1:4500
270
- bun test # unit tests
271
- bun run build # type-check + build the library and declarations to dist/
272
- npm run check:pack # the tarball has its declarations and every export target
273
- bun run dev:dist # rebuild dist on change (for a linked consumer)
274
- ```
275
-
276
- `dist/` is generated, not committed. Releases: `npm run release` (or
277
- `release:minor`) bumps the version and pushes the tag; CI tests, builds and
278
- publishes it.
284
+ ## Components
285
+
286
+ The list below is generated from the sources (`bun run gen:readme`),
287
+ so it always matches what the package exports. The first line of each comes
288
+ from the comment that opens the component.
289
+
290
+ <!-- catalogue:start -->
291
+
292
+ 142 components. The preview (`bun run dev`) shows each one live, with its props, events and slots.
293
+
294
+ <details>
295
+ <summary><b>Basics</b> (24)</summary>
296
+
297
+ - `LpAlert`: A notice in the page's flow: info, success, warning or danger, easing between them when it changes.
298
+ - `LpAvatar`: A person's picture, or their initials on a colour of their own when there is none.
299
+ - `LpBadge`: A short label or count on a tone: a status, a tag, a number of new items.
300
+ - `LpButton`: The button, in solid, soft, outline, ghost and danger looks, with loading, an icon-only square and a link mode.
301
+ - `LpCard`: The surface most content sits on, with an optional header, footer and right-click menu.
302
+ - `LpCodeBlock`: Code block with lightweight, theme-aware syntax highlighting (codeHighlight.ts — zero deps, synchronous).
303
+ - `LpCopyButton`: The one copy button: an icon that turns into a tick for a moment, a tip that says "Copy" and then "Copied", and nothing thrown when the clipboard refuses.
304
+ - `LpDivider`: A rule between blocks, optionally with a label in the middle.
305
+ - `LpDock`: A row (or column) of icon buttons that swell under the pointer, their neighbours less with distance: the launcher's quick bar of apps, instances or tools.
306
+ - `LpEmptyState`: What a list or page shows when there is nothing in it yet: an icon, a line and an action.
307
+ - `LpIcon`: Icon — wrapper over @iconify/vue.
308
+ - `LpIdenticon`: A face for something that has no picture: a mirrored pixel pattern and a colour, both derived from `seed` (an id, a username, a key's fingerprint).
309
+ - `LpLink`: A link that renders the app's router link for internal targets and a plain anchor for external ones.
310
+ - `LpLoader`: The kit's loading indicator: a spinner, three dots or an indeterminate bar.
311
+ - `LpMasonry`: Items of different heights packed into columns without the ragged gaps a grid leaves: memo cards, screenshots, mod tiles.
312
+ - `LpPopover`: A floating panel anchored to a trigger, for content that is more than a tip.
313
+ - `LpRelativeTime`: A date said the way people say it ("3 min ago", "in 2 h"), kept true as time passes, with the exact date one hover away.
314
+ - `LpScrollArea`: Drawn overlay scrollbar (reka ScrollArea).
315
+ - `LpSection`: A section heading: a title, an optional line under it, and actions on the right.
316
+ - `LpSkeleton`: Loading placeholder, in two shapes.
317
+ - `LpStatus`: What state something is in, as a light and a word: a server online, a bot starting, a region degraded.
318
+ - `LpTooltip`: Tooltip for controls whose meaning is not written on them — an icon button, a truncated value, a disabled action that owes an explanation.
319
+ - `LpTooltipProvider`: Shared timing for every LpTooltip below it: once one tip is showing, moving to the next opens it without waiting the delay again.
320
+ - `LpTruncate`: Text that may not fit: cut with an ellipsis, and the whole of it in a tip — but only when something was actually cut.
321
+
322
+ </details>
323
+
324
+ <details>
325
+ <summary><b>Forms</b> (34)</summary>
326
+
327
+ - `LpAddressInput`: Postal address form — street, optional second line, city, postcode and country — with live suggestions on the text fields and a button that opens a map to drop a pin instead of typing.
328
+ - `LpAutocomplete`: Free-text autocomplete — a text field that suggests as you type but never forces a choice.
329
+ - `LpCalendar`: Inline month calendar (reka Calendar).
330
+ - `LpCheckbox`: A checkbox with a label, including the indeterminate state.
331
+ - `LpCodeDisplay`: A short code a person reads off the screen, types somewhere else or writes down: a one-time sign-in code, a pairing code for a device, a recovery key, a licence key.
332
+ - `LpColorPicker`: Colour picker — a trigger showing the current colour, and a popover holding a saturation/value field, a hue rail, an optional alpha rail, a hex field and a row of presets.
333
+ - `LpDatePicker`: Date field + calendar popover.
334
+ - `LpDateRangePicker`: Range field + popover: presets on one side, a range calendar on the other.
335
+ - `LpDropzone`: A place to put files: drop them, click to browse, or paste from the clipboard.
336
+ - `LpEditable`: Text that is edited where it stands: a server's name in its header, a channel's topic, a note on a row.
337
+ - `LpFormField`: Form-control wrapper: label + the control (default slot) + hint/error.
338
+ - `LpImageEditor`: Straighten-and-crop for a picture on its way to being uploaded.
339
+ - `LpInput`: A text field with a label, hint, error, icons, a clear button and leading or trailing slots.
340
+ - `LpMapPicker`: Point picker on a raster tile map — drag to pan, wheel or the buttons to zoom, click to drop the pin, arrows and +/−/0 from the keyboard.
341
+ - `LpNumberField`: A number field with steppers, limits and locale-aware formatting.
342
+ - `LpOtpInput`: One-time-code input (TOTP / 2FA).
343
+ - `LpPasswordInput`: A password field with a show/hide button.
344
+ - `LpPhoneInput`: Phone number field — a country picker (flag + dial code) fused with a free-text number input.
345
+ - `LpRadio`: One radio button; LpRadioGroup holds a set of them.
346
+ - `LpRadioGroup`: A set of radio buttons for one choice among a few.
347
+ - `LpRangeCalendar`: Inline range calendar (reka RangeCalendar).
348
+ - `LpRangeSlider`: Two thumbs on one track: a range rather than a value.
349
+ - `LpRepeater`: Repeatable form rows with add and remove buttons and a stable identity per row.
350
+ - `LpSecretField`: A value someone must be able to read and copy but should not have on screen by default: an API key, a bot token, a webhook secret, a recovery code.
351
+ - `LpSegmented`: Segmented control — a compact bar of mutually-exclusive options with a sliding pill marking the selection (iOS-style).
352
+ - `LpSelect`: One select to rule them all — built on reka-ui Combobox.
353
+ - `LpSlider`: One value on a track, dragged or stepped with the keys.
354
+ - `LpSwitch`: An on/off switch with a label.
355
+ - `LpTagsInput`: Free-form list of short values: emails, labels, hostnames.
356
+ - `LpTextarea`: A multi-line text field that can grow with its content.
357
+ - `LpTimeField`: Time-of-day field: hour and minute segments, typed or stepped with the arrow keys (reka TimeField).
358
+ - `LpVideoCompress`: Trim and shrink a video on the person's own machine before it is uploaded.
359
+ - `LpVideoPlayer`: A video with the kit's own controls instead of each browser's: the same look in Chromium, Firefox and the launcher's WebKitGTK, in both themes.
360
+ - `LpVideoTrim`: Pick a stretch of a video, and optionally a rectangle on its picture, for a server to cut (ffmpeg) or blur.
361
+
362
+ </details>
363
+
364
+ <details>
365
+ <summary><b>Data</b> (21)</summary>
366
+
367
+ - `LpActivityLine`: A hairline along the edge of a card, table or page that says "something is happening here": a sweep while the length of the work is unknown, a fill once it is, a quick run to the end and a fade whe…
368
+ - `LpChart`: Token-driven SVG chart: line, area and bar off one data shape.
369
+ - `LpCountdown`: Time left until something: "resend the code in 0:42", "maintenance in 2d 3h", "the lobby closes in 4:59".
370
+ - `LpDataList`: Facts about one thing, as label and value pairs: a server's address, region, uptime and version; an account's id, plan and creation date.
371
+ - `LpFileTree`: File/folder tree — the kit convention for browsing a hierarchy of files so apps stop re-implementing indent math, chevron state and tree keyboard rules.
372
+ - `LpGauge`: One reading within a range, drawn as a dial: CPU load, disk use, a score.
373
+ - `LpLightbox`: Full-screen image viewer — the Discord-style lightbox: click a thumbnail, the image fills the screen, arrows step through the set, the wheel zooms toward the cursor over the picture and pages throu…
374
+ - `LpLogViewer`: Terminal-flavoured log stream.
375
+ - `LpMediaGallery`: A grid of thumbnails that opens into the lightbox.
376
+ - `LpNumberFlow`: A number whose DIGITS roll, like an odometer.
377
+ - `LpPagination`: Page navigator.
378
+ - `LpPie`: Shares of a whole, as a pie or a donut.
379
+ - `LpPipeline`: A run of jobs and the state each one is in: a deploy, a CI workflow, a server being provisioned.
380
+ - `LpProgress`: One determinate value, drawn as a bar or as a ring.
381
+ - `LpQuota`: "Used of limit": storage, seats, API calls.
382
+ - `LpRollingText`: Text that CHANGES rather than being replaced.
383
+ - `LpSparkline`: A trend, inline.
384
+ - `LpStat`: One headline figure with its label, a change against before and an optional sparkline.
385
+ - `LpStepMark`: The state of one job as a round glyph: hollow while it waits, spinning while it runs, a tick, a cross, a dash for skipped, a slash for cancelled.
386
+ - `LpTable`: A data table: sorting, column filters, selection, row menus, a toolbar with the page's controls, export and paging.
387
+ - `LpUptimeBar`: Uptime over a period as a row of slices, each coloured by the state it was in.
388
+
389
+ </details>
390
+
391
+ <details>
392
+ <summary><b>Navigation</b> (15)</summary>
393
+
394
+ - `LpActionBar`: A bar that rises from the bottom of the window to ask for one decision, then leaves.
395
+ - `LpBreadcrumbs`: Where the page sits in the app, as a trail of links.
396
+ - `LpCommandPalette`: Command palette (⌘K).
397
+ - `LpContextMenu`: Right-click context menu (reka ContextMenu).
398
+ - `LpDropdownMenu`: A menu opened from a button (usually a row's "…").
399
+ - `LpKeys`: A key combination drawn as keycaps: "Ctrl+K" is two caps, "G I" is G then I.
400
+ - `LpPaletteFacts`: A short list of facts in a palette preview: a label, and what it is set to now.
401
+ - `LpPaletteHeader`: The head of a palette preview: what kind of thing this is (eyebrow), its name, a line under the name, and a lead paragraph.
402
+ - `LpPalettePreview`: The body of LpCommandPalette's `#preview` slot.
403
+ - `LpPaletteStats`: A few figures in a palette preview, as tiles: mods and worlds in an instance, downloads and follows of a project, members online.
404
+ - `LpPaletteSwatches`: A theme's colours as a strip of swatches on its own surface, for a palette preview of a theme picker: the row says the name, this shows what picking it would look like.
405
+ - `LpSidebar`: App-shell side navigation.
406
+ - `LpStepper`: The steps of a flow, with where the person is and which are done.
407
+ - `LpTableOfContents`: On-this-page table of contents with scroll-spy.
408
+ - `LpTabs`: Tabs with a sliding indicator, counts and dots, and keyboard movement.
409
+
410
+ </details>
411
+
412
+ <details>
413
+ <summary><b>Overlays and feedback</b> (7)</summary>
414
+
415
+ - `LpConfirmDialog`: A dialog that asks to confirm an action, with a danger look for destructive ones.
416
+ - `LpDrawer`: Side / bottom panel built on vaul-vue (drag-driven drawer over reka's Dialog primitive — same base as the rest of the kit, so accessibility and the token theming carry over).
417
+ - `LpHoverCard`: A preview card that opens when the pointer rests on something: a username that shows the profile, a server name that shows its status, a link that shows where it goes.
418
+ - `LpModal`: A dialog over the page, stacking above the one that opened it, with a guard against losing unsaved input.
419
+ - `LpModalNav`: A dialog whose sections stand BESIDE it rather than inside it — settings, a profile with tabs' worth of detail, a wizard you can skip around in.
420
+ - `LpNotificationBell`: Notification bell: an icon button with an unread badge that opens the notification feed (LpNotificationFeed) in a popover on left-click and a quick-action menu on right-click.
421
+ - `LpShortcutsDialog`: A keyboard reference, conventionally opened with F1.
422
+
423
+ </details>
424
+
425
+ <details>
426
+ <summary><b>Motion</b> (15)</summary>
427
+
428
+ - `LpCollapse`: Shows and hides content by folding it open and shut: the box grows from nothing to the content's own height (or width), and back.
429
+ - `LpDisclosure`: Collapsible "show more" section built on reka Collapsible.
430
+ - `LpGlowBorder`: A gradient that runs round the edge of whatever it wraps.
431
+ - `LpMagnetic`: Leans what it wraps toward a nearby pointer and lets it spring back when the pointer leaves.
432
+ - `LpRevealOnScroll`: Content that arrives when it is first scrolled into view.
433
+ - `LpScramble`: A value that "decrypts" into place: each character churns through random glyphs and settles, left to right or all at once.
434
+ - `LpShaderBackdrop`: A slowly moving decorative background, drawn on the GPU: behind a sign-in screen, a hero, an empty state, a launcher's home.
435
+ - `LpShift`: Wraps content whose size is derived from what's inside it, so that everything around it shifts smoothly instead of jumping.
436
+ - `LpShinyText`: Accent text with light moving through it.
437
+ - `LpSpotlight`: A soft glow that follows the pointer inside whatever it wraps.
438
+ - `LpSwap`: One piece of content standing in for another: the section picked in a side nav, the step of a wizard, the view chosen in a segmented control.
439
+ - `LpTextReveal`: Text that arrives word by word (or letter by letter): each piece fades in, rises a few pixels and comes into focus, one after another.
440
+ - `LpTilt`: A tactile 3D-tilt wrapper.
441
+ - `LpTypewriter`: Text typed out a character at a time, optionally cycling through several strings: type, hold, erase, type the next.
442
+ - `LpVideoBackdrop`: A muted, looping video behind something: a page's hero, a panel, an app's wallpaper.
443
+
444
+ </details>
445
+
446
+ <details>
447
+ <summary><b>Tours and hints</b> (4)</summary>
448
+
449
+ - `LpHintExample`: A sample of something that is absent right now, for the tour and "what changed" to point at: nobody left the server, no flag to show.
450
+ - `LpHintLayer`: Quiet "new" markers over `v-hint` elements, with a short note when the user reaches for one.
451
+ - `LpHints`: Drop-in onboarding: put it once next to the app's root, hand it a hint system, and the tour offer, the "new" dots, "what changed" and nudges all work.
452
+ - `LpTour`: A guided walk over the real interface: the target is lit through a dim scrim, a card beside it says what it is, and the user steps through with the buttons or the arrow keys.
453
+
454
+ </details>
455
+
456
+ <details>
457
+ <summary><b>Theming</b> (3)</summary>
458
+
459
+ - `LpThemeProvider`: Applies a theme to a subtree instead of the whole page: a dark sidebar in a light app, a live preview of a theme being edited, an embedded widget that keeps its own look.
460
+ - `LpThemeStudio`: The theme, tuned in place: a base theme, any brand colour, corners, control size and motion speed, all applied live, and the result ready to take away as code, JSON or CSS.
461
+ - `LpThemeSwitcher`: Theme switcher.
462
+
463
+ </details>
464
+
465
+ <details>
466
+ <summary><b>Shell, layout and other</b> (19)</summary>
467
+
468
+ - `LpAppShell`: Full application shell: the side navigation rail, a top header bar and the page, which is what scrolls.
469
+ - `LpBottomNav`: A phone app's bottom navigation: the main pages along the bottom edge, each a real link (or a button that emits `select`), the current one marked by a bar that slides between them, and "More" last…
470
+ - `LpCommandScope`: A registry of commands for LpCommandPalette: what the page and its fields offer, nested scopes first.
471
+ - `LpConfirmHost`: Renders the requests made through `confirm()` / `useConfirm()`.
472
+ - `LpContextSwitcher`: Which of an app's contexts the rail is showing — a server, a workspace, a machine, a part of the product — and the menu to change it.
473
+ - `LpFieldNotice`: The floating note from useFieldNotice: anchored under the nearest `relative` box, over whatever sits below it (usually the hint), so it never moves the layout.
474
+ - `LpFrame`: A window of its own inside the page, as LpThemeProvider is a theme of its own: what is rendered in it behaves as if it were the whole screen.
475
+ - `LpInfraNode`: A host or service card on LpTopologyCanvas.
476
+ - `LpInspector`: The panel beside a table, graph or canvas that shows the selected thing and lets it be edited: one place and one shape for "what is this, and change it".
477
+ - `LpLogInspector`: One log entry read closely: a structured payload as a tree, a stack trace with its frames set apart from the error above them.
478
+ - `LpNotificationFeed`: A notification feed: the list behind LpNotificationBell, usable on its own for a full "All notifications" page.
479
+ - `LpPageScrollbar`: The kit's overlay scrollbar for the document itself, for a page that scrolls the window (LpAppShell's default): the same bar as LpScrollArea's, drawn by the kit over the content instead of the brow…
480
+ - `LpPaletteTheme`: The theme editor inside LpCommandPalette's preview.
481
+ - `LpServiceNode`: Custom Vue Flow node for a SERVICE inside a host frame on LpTopologyCanvas.
482
+ - `LpThemeStudioSection`: One row of LpThemeStudio: a label and its controls.
483
+ - `LpToaster`: Where toasts appear; mount it once near the app root.
484
+ - `LpTopologyCanvas`: A declarative infra topology graph on Vue Flow (the engine n8n & NetBird use), themed with kit tokens.
485
+ - `LpWindow`: A floating panel over the page that the page keeps working under: a log tail beside a dashboard, a console, a reference next to a form.
486
+ - `LpWorkspaceSwitcher`: The menu for useWorkspaces: which layout is showing, the others, and making, copying, renaming and deleting them.
487
+
488
+ </details>
489
+
490
+ <!-- catalogue:end -->
491
+
492
+ ## Guides
279
493
 
280
494
  ### Linked views
281
495
 
@@ -317,7 +531,7 @@ on a screen edge, snap to other windows, `persist` for its place. It stays out
317
531
  of the modal stack and under dropdowns; it is not a window manager (no
318
532
  minimising, no taskbar).
319
533
 
320
- ### Repeatable form rows
534
+ ### Forms: repeatable rows and clearable inputs
321
535
 
322
536
  `LpRepeater` supplies add/remove controls and stable row identity. Its slot owns
323
537
  the fields and validation; internal keys never become part of the saved model.
@@ -354,21 +568,58 @@ objects are edited, reordered or removed. The slot also provides `index`,
354
568
  disabled/read-only state, and can be combined with the `trailing` slot. Its
355
569
  button text comes from `labels.clear` or the app-wide `input` label scope.
356
570
 
357
- ### Staying in the right window
571
+ ### Tables: the page's own controls in the toolbar
358
572
 
359
- `tests/realmGuard.test.ts` fails on `instanceof Element`, a bare `new ResizeObserver`, `getComputedStyle` or `document.activeElement`: inside an LpFrame those are another realm's. Use `composables/realm.ts`.
573
+ `LpTable` keeps one bar above the rows: the filter chips, the row count and
574
+ Export (`exportable`) sit there, and the `#toolbar` slot puts the page's own
575
+ controls (a search box, segmented filters, selects) on the same line, instead
576
+ of a second bar of the page's above the table's.
360
577
 
361
- ### Browser regression tests
578
+ ```vue
579
+ <LpTable :columns="columns" :rows="rows" exportable>
580
+ <template #toolbar>
581
+ <LpInput v-model="query" placeholder="Search" />
582
+ <LpSegmented v-model="status" :options="statuses" />
583
+ </template>
584
+ </LpTable>
585
+ ```
362
586
 
363
- ```sh
364
- bunx playwright install chromium
365
- bun run test:browser
587
+ ### Clipboard feedback
588
+
589
+ `useClipboard().copy(text, { toast: true, errorToast: true })` says the kit's
590
+ own "Copied" and "Couldn't copy" through the toaster, taken from the
591
+ `copyButton` label scope, so a translated app (or the `uk` pack) covers them
592
+ without every call passing its own strings. A string instead of `true` says
593
+ that string. `COPY_LABELS` is the shared English default.
594
+
595
+ ### Video: playing, trimming and shrinking before upload
596
+
597
+ - `LpVideoPlayer` is a video with the kit's own controls, the same in Chromium,
598
+ Firefox and WebKitGTK. Its full screen takes the whole player, so the controls
599
+ and menus come along. With `fit` it sizes itself to a box (a lightbox stage, a
600
+ card of fixed height) without upscaling past the video's own size; a height
601
+ cap belongs on that box, not on the player.
602
+ - `LpVideoTrim` picks a stretch (`v-model:range`, seconds) and optionally an area
603
+ of the picture (`v-model:region`, fractions of the frame) for a server to cut
604
+ or blur. Nothing is cut in the browser.
605
+ - `LpVideoCompress` trims and shrinks a video on the person's own device before
606
+ it is uploaded, so the upload costs only the part that matters. A preset picks
607
+ the height and bitrate, or a target size, and the estimate shows the ceiling
608
+ against the original. `done` hands back an MP4; `skip` keeps the original.
609
+ Encoding uses WebCodecs (hardware encoders where the device has them) through
610
+ `mediabunny`, which is loaded only when a video is first encoded. A browser
611
+ without a video encoder only offers the original.
612
+
613
+ ```ts
614
+ import { canTranscodeVideo, estimateTranscodedBytes, probeVideo, transcodeVideo } from "@leavepulse/ui"
615
+
616
+ if (await canTranscodeVideo()) {
617
+ const out = await transcodeVideo(file, { range: [12, 40], maxHeight: 720, onProgress: (f) => (progress.value = f) })
618
+ }
366
619
  ```
367
620
 
368
- Playwright starts an isolated Vite server on port 4501. The suite covers field
369
- locking, secret reveal controls, date/select focus boundaries and a mobile
370
- form inside a guarded drawer. These checks run in CI and before npm publishing.
371
- Use `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` to run an existing Chromium installation.
621
+ Re-encoding never raises the bitrate above the source's, and the sound is kept
622
+ (AAC, or Opus where the browser has no AAC encoder).
372
623
 
373
624
  ### Contextual command palette
374
625
 
@@ -460,3 +711,47 @@ well as the Tab menu, including an Actions button on phones.
460
711
  `LpPaletteFacts` and `LpPaletteStats` accept `loading`. Known labels retain their
461
712
  place, values become skeletons; empty item lists receive placeholder rows.
462
713
  Try the complete example at `/component/context-palette` in the preview.
714
+
715
+ ## Develop
716
+
717
+ ```bash
718
+ bun install
719
+ bun run dev # live preview/playground on http://127.0.0.1:4500
720
+ bun test # unit tests
721
+ bun run build # type-check + build the library and declarations to dist/
722
+ npm run check:pack # the tarball has its declarations and every export target
723
+ bun run dev:dist # rebuild dist on change (for a linked consumer)
724
+ ```
725
+
726
+ `dist/` is generated, not committed.
727
+
728
+ After adding, renaming or exporting a component, run
729
+ `bun run gen:readme`: a test fails while the catalogue above is
730
+ out of date.
731
+
732
+ ### Staying in the right window
733
+
734
+ `tests/realmGuard.test.ts` fails on `instanceof Element`, a bare `new ResizeObserver`, `getComputedStyle` or `document.activeElement`: inside an LpFrame those are another realm's. Use `composables/realm.ts`.
735
+
736
+ ### Browser regression tests
737
+
738
+ ```sh
739
+ bunx playwright install chromium
740
+ bun run test:browser
741
+ ```
742
+
743
+ Playwright starts an isolated Vite server on port 4501. The suite covers field
744
+ locking, secret reveal controls, date/select focus boundaries and a mobile
745
+ form inside a guarded drawer. These checks run in CI and before npm publishing.
746
+ Use `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` to run an existing Chromium installation.
747
+
748
+ ## Releasing
749
+
750
+ `npm run release` (patch) or `npm run release:minor` bumps the version, commits
751
+ it, tags it and pushes; CI tests, builds and publishes the tag to npm. Run it on
752
+ a clean tree whose code has a green CI run. CI on `main` cancels a run in
753
+ progress when a new commit arrives, so nothing else should be pushed to `main`
754
+ while a release commit's run is going. When more than one person or agent works
755
+ on the kit at once, agree on who releases before running the script, so two
756
+ versions don't race for `latest`. npm can take a few minutes after the publish
757
+ job before the new version shows as `latest`.
@@ -12,6 +12,21 @@
12
12
  animation: lp-skeleton-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
13
13
  }
14
14
 
15
+ /*
16
+ * A block to pulse has to be visible first. The pulse only animates opacity,
17
+ * so an item written as the docs above show it (a size, a radius, no colour)
18
+ * was transparent and the loading state was blank. The default sits in
19
+ * `@layer base`, as the kit's other defaults do (tokens.css, selection.css):
20
+ * an unlayered rule outranks every Tailwind utility whatever its specificity,
21
+ * so a consumer's own background (bg-surface-raised on a soft card) would
22
+ * have lost to it.
23
+ */
24
+ @layer base {
25
+ .lp-skeleton-item {
26
+ background-color: var(--color-surface-soft);
27
+ }
28
+ }
29
+
15
30
  /* Items nested anywhere inside a wrapper, at any depth. */
16
31
  .lp-skeleton .lp-skeleton-item {
17
32
  animation: lp-skeleton-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
@@ -11,17 +11,17 @@
11
11
  * fade before closing the gap and the whole list lurches at the end instead of
12
12
  * gliding.
13
13
  */
14
- .lp-row-enter-active[data-v-f6e4fb45],
15
- .lp-row-leave-active[data-v-f6e4fb45] {
14
+ .lp-row-enter-active[data-v-3fb24315],
15
+ .lp-row-leave-active[data-v-3fb24315] {
16
16
  transition:
17
17
  opacity var(--duration-fast) var(--ease-emphasized),
18
18
  transform var(--duration-fast) var(--ease-emphasized);
19
19
  }
20
- .lp-row-move[data-v-f6e4fb45] {
20
+ .lp-row-move[data-v-3fb24315] {
21
21
  transition: transform var(--duration-medium) var(--ease-emphasized);
22
22
  }
23
- .lp-row-enter-from[data-v-f6e4fb45],
24
- .lp-row-leave-to[data-v-f6e4fb45] {
23
+ .lp-row-enter-from[data-v-3fb24315],
24
+ .lp-row-leave-to[data-v-3fb24315] {
25
25
  opacity: 0;
26
26
  transform: translateX(-0.5rem);
27
27
  }
@@ -35,7 +35,7 @@
35
35
  * drops out of the table layout entirely. Nothing sits below this row to be
36
36
  * dragged around, so simply not animating its exit costs nothing.
37
37
  */
38
- .lp-row-empty.lp-row-leave-active[data-v-f6e4fb45] {
38
+ .lp-row-empty.lp-row-leave-active[data-v-3fb24315] {
39
39
  display: none;
40
40
  }
41
41
 
@@ -54,7 +54,7 @@
54
54
  * once anything is selected. An earlier version collapsed the column to nothing
55
55
  * and floated the hover box over the first cell, where it sat on the text.
56
56
  */
57
- .lp-tick[data-v-f6e4fb45] {
57
+ .lp-tick[data-v-3fb24315] {
58
58
  display: flex;
59
59
  align-items: center;
60
60
  max-width: 0;
@@ -66,17 +66,17 @@
66
66
  padding var(--duration-medium) var(--ease-emphasized),
67
67
  opacity var(--duration-fast) ease;
68
68
  }
69
- .lp-tick-open[data-v-f6e4fb45] {
69
+ .lp-tick-open[data-v-3fb24315] {
70
70
  max-width: 3.25rem;
71
71
  padding-inline: 1rem;
72
72
  opacity: 1;
73
73
  }
74
- .lp-tick-hidden[data-v-f6e4fb45],
75
- .lp-tick-cue[data-v-f6e4fb45] {
74
+ .lp-tick-hidden[data-v-3fb24315],
75
+ .lp-tick-cue[data-v-3fb24315] {
76
76
  opacity: 0;
77
77
  }
78
- tr:hover .lp-tick-cue[data-v-f6e4fb45],
79
- .lp-tick-cue[data-v-f6e4fb45]:focus-within {
78
+ tr:hover .lp-tick-cue[data-v-3fb24315],
79
+ .lp-tick-cue[data-v-3fb24315]:focus-within {
80
80
  opacity: 1;
81
81
  }
82
82
 
@@ -84,24 +84,24 @@ tr:hover .lp-tick-cue[data-v-f6e4fb45],
84
84
  turned down the rows change without animating rather than not changing. */
85
85
  /* Only where there is a hover to reveal it; a finger long-presses instead. */
86
86
  @media (hover: none) {
87
- tr:hover .lp-tick-cue[data-v-f6e4fb45] {
87
+ tr:hover .lp-tick-cue[data-v-3fb24315] {
88
88
  opacity: 0;
89
89
  }
90
90
 
91
91
  /* Nor is there anything for the column's width to hold until then: on a
92
92
  touch screen it stays closed until a long press starts selecting, and
93
93
  eases open like the rest of the column. */
94
- .lp-tick-hidden[data-v-f6e4fb45],
95
- .lp-tick-cue[data-v-f6e4fb45] {
94
+ .lp-tick-hidden[data-v-3fb24315],
95
+ .lp-tick-cue[data-v-3fb24315] {
96
96
  max-width: 0;
97
97
  padding-inline: 0;
98
98
  }
99
99
  }
100
100
  @media (prefers-reduced-motion: reduce) {
101
- .lp-row-enter-active[data-v-f6e4fb45],
102
- .lp-row-leave-active[data-v-f6e4fb45],
103
- .lp-row-move[data-v-f6e4fb45],
104
- .lp-tick[data-v-f6e4fb45] {
101
+ .lp-row-enter-active[data-v-3fb24315],
102
+ .lp-row-leave-active[data-v-3fb24315],
103
+ .lp-row-move[data-v-3fb24315],
104
+ .lp-tick[data-v-3fb24315] {
105
105
  transition: none;
106
106
  }
107
107
  }
@@ -13,7 +13,7 @@ import { _ as _sfc_main$4 } from "./LpIcon-DCGhNsCU.js";
13
13
  import { _ as _sfc_main$8 } from "./LpPageScrollbar-C9sTeU-2.js";
14
14
  import { _ as _sfc_main$7 } from "./LpScrollArea-CqvwfZs2.js";
15
15
  import { _ as _sfc_main$5 } from "./LpShift-DAgS_d2x.js";
16
- import { _ as _sfc_main$1 } from "./LpSidebar-DnqbKULj.js";
16
+ import { _ as _sfc_main$1 } from "./LpSidebar-CjE90o3s.js";
17
17
  import { mergeClass, useMergedAttrs } from "../composables/useMergedClass.js";
18
18
  import { nodeWindow, isHTMLElement } from "../composables/realm.js";
19
19
  const _hoisted_1 = ["href"];
@@ -2,7 +2,7 @@ import { defineComponent, ref, watch, computed, openBlock, createBlock, unref, w
2
2
  import { tv } from "tailwind-variants";
3
3
  import { useMergedAttrs } from "../composables/useMergedClass.js";
4
4
  import { MenuWhen } from "../components/menuWhen.js";
5
- import { _ as _sfc_main$2 } from "./LpSkeleton-BB6XIV-w.js";
5
+ import { _ as _sfc_main$2 } from "./LpSkeleton-leTl53V9.js";
6
6
  import { _ as _sfc_main$1 } from "./LpSwap-M7nFeW3C.js";
7
7
  import { useSpotlight } from "../composables/useSpotlight.js";
8
8
  const __default__ = { inheritAttrs: false };
@@ -8,7 +8,7 @@ import { _ as _sfc_main$2 } from "./LpEmptyState-DT8U4R8L.js";
8
8
  import { _ as _sfc_main$6 } from "./LpFileTreeNode-DfvIGmSz.js";
9
9
  import { _ as _sfc_main$3 } from "./LpInput-CKMblRtp.js";
10
10
  import { _ as _sfc_main$5 } from "./LpScrollArea-CqvwfZs2.js";
11
- import { _ as _sfc_main$1 } from "./LpSkeleton-BB6XIV-w.js";
11
+ import { _ as _sfc_main$1 } from "./LpSkeleton-leTl53V9.js";
12
12
  import { usePersisted } from "../composables/usePersisted.js";
13
13
  import { useMergedAttrs } from "../composables/useMergedClass.js";
14
14
  import { useLabels } from "../composables/useLpConfig.js";