@leavepulse/ui 0.60.0 → 0.62.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 (74) hide show
  1. package/README.md +8 -0
  2. package/dist/assets/{LpTable-BZZf_rrt.css → LpTable-DpXyPzs8.css} +19 -19
  3. package/dist/chunks/{LpAddressInput-DD4zTN4P.js → LpAddressInput-CzdZguBo.js} +1 -1
  4. package/dist/chunks/{LpAlert--KLL4NLk.js → LpAlert-BAOujnXy.js} +8 -6
  5. package/dist/chunks/{LpAppShell-Cv-Y9qNA.js → LpAppShell-BXMNDEpF.js} +1 -1
  6. package/dist/chunks/{LpAutocomplete-PnL2ntKl.js → LpAutocomplete-D9mY5Tqk.js} +101 -36
  7. package/dist/chunks/{LpAvatar-CwGs1JCQ.js → LpAvatar-K6MOMfgn.js} +1 -1
  8. package/dist/chunks/{LpCard-B2D0GIUU.js → LpCard-D9HVgX8Y.js} +1 -1
  9. package/dist/chunks/{LpColorPicker-CvBF8TeL.js → LpColorPicker-BO1ZTY51.js} +1 -1
  10. package/dist/chunks/{LpContextMenu-BnN1k7ga.js → LpContextMenu-Cv_F2iCG.js} +6 -3
  11. package/dist/chunks/{LpFileTree-DEqh2ySW.js → LpFileTree-DTwUj0L-.js} +1 -1
  12. package/dist/chunks/{LpFileTreeNode-DCZA7sQ-.js → LpFileTreeNode-D-XLPZ6T.js} +1 -1
  13. package/dist/chunks/{LpHints-BTpvisgP.js → LpHints-B_85eSmm.js} +1 -1
  14. package/dist/chunks/{LpLogViewer-BuPe23sZ.js → LpLogViewer-D9UgNz2m.js} +1 -1
  15. package/dist/chunks/{LpModalNav-Cw7m-eks.js → LpModalNav-D4rmflQn.js} +1 -1
  16. package/dist/chunks/{LpNotificationBell-xH_JphpS.js → LpNotificationBell-CyylOlZR.js} +1 -1
  17. package/dist/chunks/{LpPie-BD5Lsh-2.js → LpPie-B2deu8CI.js} +1 -1
  18. package/dist/chunks/{LpSidebar-B3E_iViL.js → LpSidebar-D8tub3bt.js} +1 -1
  19. package/dist/chunks/{LpSidebarNav-32sAXNM-.js → LpSidebarNav-BBSRg34G.js} +1 -1
  20. package/dist/chunks/LpSwap-D82jiecV.js +38 -0
  21. package/dist/chunks/{LpThemeSwitcher-CeBG0e_D.js → LpThemeSwitcher-ZFFftBQn.js} +1 -1
  22. package/dist/chunks/{LpToaster-BcKs7vX7.js → LpToaster-D3OisETH.js} +1 -1
  23. package/dist/chunks/{LpTour-C29-1wha.js → LpTour-ByaL36fN.js} +75 -53
  24. package/dist/component-names.d.ts +1 -1
  25. package/dist/components/LpAddressInput.vue.js +1 -1
  26. package/dist/components/LpAlert.vue.d.ts +7 -0
  27. package/dist/components/LpAlert.vue.js +1 -1
  28. package/dist/components/LpAppShell.vue.js +1 -1
  29. package/dist/components/LpAutocomplete.vue.d.ts +24 -0
  30. package/dist/components/LpAutocomplete.vue.js +1 -1
  31. package/dist/components/LpAvatar.vue.js +1 -1
  32. package/dist/components/LpCard.vue.js +1 -1
  33. package/dist/components/LpChart.vue.js +1 -1
  34. package/dist/components/LpColorPicker.vue.js +1 -1
  35. package/dist/components/LpCommandPalette.vue.js +1 -1
  36. package/dist/components/LpContextMenu.vue.js +1 -1
  37. package/dist/components/LpFileTree.vue.js +1 -1
  38. package/dist/components/LpFileTreeNode.vue.js +1 -1
  39. package/dist/components/LpHints.vue.js +1 -1
  40. package/dist/components/LpLogViewer.vue.js +1 -1
  41. package/dist/components/LpModalNav.vue.js +1 -1
  42. package/dist/components/LpNotificationBell.vue.js +1 -1
  43. package/dist/components/LpPaletteHeader.vue.js +1 -1
  44. package/dist/components/LpPie.vue.js +1 -1
  45. package/dist/components/LpSidebar.vue.js +1 -1
  46. package/dist/components/LpSidebarNav.vue.js +1 -1
  47. package/dist/components/LpSwap.vue.d.ts +19 -0
  48. package/dist/components/LpSwap.vue.js +4 -0
  49. package/dist/components/LpTable.vue.js +5 -4
  50. package/dist/components/LpThemeSwitcher.vue.js +1 -1
  51. package/dist/components/LpToaster.vue.js +1 -1
  52. package/dist/components/LpTour.vue.js +1 -1
  53. package/dist/components/tour.d.ts +9 -0
  54. package/dist/components/tour.js +22 -0
  55. package/dist/composables/useHintSystem.d.ts +2 -0
  56. package/dist/composables/useHintSystem.js +16 -4
  57. package/dist/composables/useToast.d.ts +13 -4
  58. package/dist/composables/useToast.js +13 -2
  59. package/dist/index.d.ts +3 -2
  60. package/dist/index.js +63 -61
  61. package/package.json +2 -1
  62. package/src/component-names.ts +1 -0
  63. package/src/components/LpAlert.vue +16 -5
  64. package/src/components/LpAutocomplete.vue +110 -24
  65. package/src/components/LpContextMenu.vue +6 -3
  66. package/src/components/LpSwap.vue +45 -0
  67. package/src/components/LpTable.vue +1 -0
  68. package/src/components/LpToaster.vue +1 -1
  69. package/src/components/LpTour.vue +26 -8
  70. package/src/components/tour.ts +31 -0
  71. package/src/composables/useHintSystem.ts +26 -4
  72. package/src/composables/useToast.ts +32 -5
  73. package/src/index.ts +3 -1
  74. package/src/tokens/selection.css +20 -0
@@ -19,6 +19,8 @@ export interface ToastItem {
19
19
  actions?: ToastAction[];
20
20
  /** Whole-toast click handler (UAProject-style clickable toast). */
21
21
  onClick?: () => void | Promise<void>;
22
+ /** Copied with the message, not shown: what a bug report needs (request, status, ids). */
23
+ details?: string;
22
24
  }
23
25
  export interface ToastOptions {
24
26
  /**
@@ -31,12 +33,19 @@ export interface ToastOptions {
31
33
  duration?: number;
32
34
  actions?: ToastAction[];
33
35
  onClick?: () => void | Promise<void>;
36
+ /** Copied with the message, not shown. An Error passed to `error()` brings its own `details`. */
37
+ details?: string;
38
+ /** The error behind a hand-written message ("Not saved: " + e.message): its `details` ride along. */
39
+ cause?: unknown;
34
40
  }
35
- declare function push(variant: ToastVariant, message: string, options?: ToastOptions): number;
41
+ export type ToastMessage = string | (Error & {
42
+ details?: string;
43
+ });
44
+ declare function push(variant: ToastVariant, input: ToastMessage, given?: ToastOptions): number;
36
45
  export interface ToastPromiseMessages<T> {
37
46
  loading: string;
38
47
  success: string | ((value: T) => string);
39
- error: string | ((error: unknown) => string);
48
+ error: string | ((error: unknown) => ToastMessage);
40
49
  }
41
50
  /**
42
51
  * One toast that follows a promise: a persistent "loading" toast that turns
@@ -50,8 +59,8 @@ export declare function useToast(): {
50
59
  toast: (message: string, o?: ToastOptions) => number;
51
60
  info: (message: string, o?: ToastOptions) => number;
52
61
  success: (message: string, o?: ToastOptions) => number;
53
- warning: (message: string, o?: ToastOptions) => number;
54
- error: (message: string, o?: ToastOptions) => number;
62
+ warning: (message: ToastMessage, o?: ToastOptions) => number;
63
+ error: (message: ToastMessage, o?: ToastOptions) => number;
55
64
  promise: typeof promise;
56
65
  /** Low-level push for bridges (e.g. @leavepulse/ui-sdk apiError). */
57
66
  push: typeof push;
@@ -1,11 +1,21 @@
1
1
  import { reactive } from "vue";
2
+ function detailsOf(source) {
3
+ const details = source?.details;
4
+ return typeof details === "string" && details ? details : void 0;
5
+ }
6
+ function split(message, options) {
7
+ const text = typeof message === "string" ? message : message.message;
8
+ const details = options.details ?? (typeof message === "string" ? void 0 : detailsOf(message)) ?? detailsOf(options.cause);
9
+ return [text, { ...options, details }];
10
+ }
2
11
  const DEFAULT_DURATION = 4500;
3
12
  const MAX_ACTIVE = 4;
4
13
  const STATE_KEY = /* @__PURE__ */ Symbol.for("@leavepulse/ui:toast-queue");
5
14
  const g = globalThis;
6
15
  const state = g[STATE_KEY] ??= { queue: reactive([]), nextId: 0 };
7
16
  const queue = state.queue;
8
- function push(variant, message, options = {}) {
17
+ function push(variant, input, given = {}) {
18
+ const [message, options] = split(input, given);
9
19
  if (typeof window === "undefined") return -1;
10
20
  const duration = options.duration ?? DEFAULT_DURATION;
11
21
  const fields = {
@@ -16,7 +26,8 @@ function push(variant, message, options = {}) {
16
26
  duration,
17
27
  dismissAt: duration > 0 ? Date.now() + duration : null,
18
28
  actions: options.actions,
19
- onClick: options.onClick
29
+ onClick: options.onClick,
30
+ details: options.details
20
31
  };
21
32
  const same = options.key === void 0 ? -1 : queue.findIndex((t) => t.key === options.key);
22
33
  const existing = queue[same];
package/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@ export type { ActionBarAction } from "./components/LpActionBar.vue";
8
8
  export { default as LpAlert } from "./components/LpAlert.vue";
9
9
  export { default as LpAppShell } from "./components/LpAppShell.vue";
10
10
  export { default as LpAutocomplete } from "./components/LpAutocomplete.vue";
11
- export type { AutocompleteOption } from "./components/LpAutocomplete.vue";
11
+ export type { AutocompleteOption, AutocompleteSelection } from "./components/LpAutocomplete.vue";
12
12
  export { default as LpAvatar } from "./components/LpAvatar.vue";
13
13
  export { default as LpBadge } from "./components/LpBadge.vue";
14
14
  export { default as LpBreadcrumbs } from "./components/LpBreadcrumbs.vue";
@@ -125,6 +125,7 @@ export type { SelectOption, SelectValue } from "./components/LpSelect.vue";
125
125
  export { default as LpServiceNode } from "./components/LpServiceNode.vue";
126
126
  export type { ServiceNodeData } from "./components/LpServiceNode.vue";
127
127
  export { default as LpShift } from "./components/LpShift.vue";
128
+ export { default as LpSwap } from "./components/LpSwap.vue";
128
129
  export { default as LpShortcutsDialog } from "./components/LpShortcutsDialog.vue";
129
130
  export { default as LpSidebar } from "./components/LpSidebar.vue";
130
131
  export type { SidebarItem, SidebarSection } from "./components/LpSidebar.vue";
@@ -237,7 +238,7 @@ export type { UseTilt, UseTiltOptions } from "./composables/useTilt";
237
238
  export { useZoomPan } from "./composables/useZoomPan";
238
239
  export type { UseZoomPan, UseZoomPanOptions } from "./composables/useZoomPan";
239
240
  export { useToast } from "./composables/useToast";
240
- export type { ToastAction, ToastItem, ToastOptions, ToastVariant, } from "./composables/useToast";
241
+ export type { ToastAction, ToastItem, ToastMessage, ToastOptions, ToastVariant, } from "./composables/useToast";
241
242
  export type { ToastPromiseMessages } from "./composables/useToast";
242
243
  export { cacheSystemThemes, normalizeTheme, prefersDarkScheme, themeEntries, } from "./theme/useTheme";
243
244
  export type { SystemThemes, UseTheme } from "./theme/useTheme";
package/dist/index.js CHANGED
@@ -1,24 +1,24 @@
1
1
  import { _ } from "./chunks/LayoutCanvas-t7LWFCEn.js";
2
- import { _ as _2 } from "./chunks/LpAddressInput-DD4zTN4P.js";
2
+ import { _ as _2 } from "./chunks/LpAddressInput-CzdZguBo.js";
3
3
  import { _ as _3 } from "./chunks/LpMapPicker-DybIlWnF.js";
4
4
  import { OSM_TILES, createNominatimProvider, emptyAddress, formatAddress, project, tileUrl, unproject } from "./components/address.js";
5
5
  import { _ as _4 } from "./chunks/LpActionBar-BsSp66S_.js";
6
- import { _ as _5 } from "./chunks/LpAlert--KLL4NLk.js";
7
- import { _ as _6 } from "./chunks/LpAppShell-Cv-Y9qNA.js";
8
- import { _ as _7 } from "./chunks/LpAutocomplete-PnL2ntKl.js";
9
- import { _ as _8 } from "./chunks/LpAvatar-CwGs1JCQ.js";
6
+ import { _ as _5 } from "./chunks/LpAlert-BAOujnXy.js";
7
+ import { _ as _6 } from "./chunks/LpAppShell-BXMNDEpF.js";
8
+ import { _ as _7 } from "./chunks/LpAutocomplete-D9mY5Tqk.js";
9
+ import { _ as _8 } from "./chunks/LpAvatar-K6MOMfgn.js";
10
10
  import { _ as _9 } from "./chunks/LpBadge-Fe0D1bYx.js";
11
11
  import { _ as _10 } from "./chunks/LpBreadcrumbs-CndzPYiG.js";
12
12
  import { _ as _11 } from "./chunks/LpButton-CJSD7N0Z.js";
13
13
  import { _ as _12 } from "./chunks/LpCalendar-CXVDbb2M.js";
14
- import { _ as _13 } from "./chunks/LpCard-B2D0GIUU.js";
14
+ import { _ as _13 } from "./chunks/LpCard-D9HVgX8Y.js";
15
15
  import { default as default2 } from "./components/LpChart.vue.js";
16
16
  import { backgroundOf, csvField, downloadFile, fileStem, inlineSvg, resolveCssColor, svgToPng, toCsv } from "./components/exportData.js";
17
17
  import { CHART_PALETTE, axisTicks, chartSummary, formatTick, markerIndex, niceScale, resolveSlices } from "./components/chart.js";
18
18
  import { _ as _14 } from "./chunks/LpCheckbox-DV9W6T3H.js";
19
19
  import { _ as _15 } from "./chunks/LpCodeBlock-t6MLA3fb.js";
20
20
  import { tokenizeLines } from "./components/codeHighlight.js";
21
- import { _ as _16 } from "./chunks/LpColorPicker-CvBF8TeL.js";
21
+ import { _ as _16 } from "./chunks/LpColorPicker-BO1ZTY51.js";
22
22
  import { DEFAULT_SWATCHES, clampHsv, formatColor, hsvToHsl, hsvToRgb, isLight, normalizeHue, opaqueCss, parseColor, parseHex, rgbToHex, rgbToHsv } from "./components/color.js";
23
23
  import { default as default3 } from "./components/LpCommandPalette.vue.js";
24
24
  import { applyQueryFilters, highlightParts, parseCommandQuery, parsePaletteQuery, rankCommandItems } from "./components/commandSearch.js";
@@ -33,7 +33,7 @@ import { comboKeys, comboSteps } from "./components/keys.js";
33
33
  import { _ as _18 } from "./chunks/LpConfirmDialog-tkgyY3eu.js";
34
34
  import { _ as _19 } from "./chunks/LpConfirmHost-CoYIcUdl.js";
35
35
  import { confirm, useConfirm } from "./components/confirm.js";
36
- import { _ as _20 } from "./chunks/LpContextMenu-BnN1k7ga.js";
36
+ import { _ as _20 } from "./chunks/LpContextMenu-Cv_F2iCG.js";
37
37
  import { _ as _21 } from "./chunks/LpDatePicker-DxdrVNCG.js";
38
38
  import { _ as _22 } from "./chunks/LpDateRangePicker-DjeaqikC.js";
39
39
  import { _ as _23 } from "./chunks/LpRangeCalendar-COeOYF52.js";
@@ -44,7 +44,7 @@ import { _ as _26 } from "./chunks/LpDrawer-C97yoWmh.js";
44
44
  import { _ as _27 } from "./chunks/LpDropdownMenu-CbyYu4Rw.js";
45
45
  import { default as default9 } from "./components/LpDropzone.vue.js";
46
46
  import { _ as _28 } from "./chunks/LpEmptyState-CX8fdGSU.js";
47
- import { _ as _29 } from "./chunks/LpFileTree-DEqh2ySW.js";
47
+ import { _ as _29 } from "./chunks/LpFileTree-DTwUj0L-.js";
48
48
  import { ancestorIds, checkStateOf, checkedCount, computeStats, fileIcon, formatModified, formatSize, sortNodes, subtreeIds, subtreeSize } from "./components/fileTree.js";
49
49
  import { _ as _30 } from "./chunks/LpFieldNotice-B--RzKfj.js";
50
50
  import { _ as _31 } from "./chunks/LpFormField-DMPPL1jF.js";
@@ -56,19 +56,19 @@ import { _ as _34 } from "./chunks/LpInput-C7FHxzj2.js";
56
56
  import { _ as _35 } from "./chunks/LpLightbox-DiksFa4L.js";
57
57
  import { fileNameOf } from "./components/lightbox.js";
58
58
  import { _ as _36 } from "./chunks/LpLink-Krkjhe_y.js";
59
- import { _ as _37 } from "./chunks/LpLogViewer-BuPe23sZ.js";
59
+ import { _ as _37 } from "./chunks/LpLogViewer-D9UgNz2m.js";
60
60
  import { countLevels, matchPositions, matchesTerm, ringWindow, stepMatch } from "./components/logSearch.js";
61
61
  import { _ as _38 } from "./chunks/LpMediaGallery-Cic9UHsa.js";
62
62
  import { _ as _39 } from "./chunks/LpModal-QFfctZsj.js";
63
- import { _ as _40 } from "./chunks/LpModalNav-Cw7m-eks.js";
64
- import { _ as _41 } from "./chunks/LpNotificationBell-xH_JphpS.js";
63
+ import { _ as _40 } from "./chunks/LpModalNav-D4rmflQn.js";
64
+ import { _ as _41 } from "./chunks/LpNotificationBell-CyylOlZR.js";
65
65
  import { _ as _42 } from "./chunks/LpNumberField-BFSRVGkJ.js";
66
66
  import { _ as _43 } from "./chunks/LpTimeField-BTEt0kvL.js";
67
67
  import { _ as _44 } from "./chunks/LpNumberFlow-z4WPZcZu.js";
68
68
  import { _ as _45 } from "./chunks/LpOtpInput-PbeNJ5In.js";
69
69
  import { _ as _46 } from "./chunks/LpPagination-BFl0I7tL.js";
70
70
  import { _ as _47 } from "./chunks/LpPasswordInput-C1G4k_r0.js";
71
- import { _ as _48 } from "./chunks/LpPie-BD5Lsh-2.js";
71
+ import { _ as _48 } from "./chunks/LpPie-B2deu8CI.js";
72
72
  import { _ as _49 } from "./chunks/LpPhoneInput-CZrpVxla.js";
73
73
  import { filterCountries, flagEmoji, loadCountries, matchCountryByValue } from "./components/countries.js";
74
74
  import { _ as _50 } from "./chunks/LpPopover-DCKeqiVn.js";
@@ -82,36 +82,37 @@ import { _ as _57 } from "./chunks/LpSection-BAkbf0OK.js";
82
82
  import { _ as _58 } from "./chunks/LpSelect-C4OXN_q3.js";
83
83
  import { _ as _59 } from "./chunks/LpServiceNode-D8uJ8sGk.js";
84
84
  import { _ as _60 } from "./chunks/LpShift-DXNsauQZ.js";
85
+ import { _ as _61 } from "./chunks/LpSwap-D82jiecV.js";
85
86
  import { default as default11 } from "./components/LpShortcutsDialog.vue.js";
86
- import { _ as _61 } from "./chunks/LpSidebar-B3E_iViL.js";
87
- import { _ as _62 } from "./chunks/LpSkeleton-BB6XIV-w.js";
88
- import { _ as _63 } from "./chunks/LpSlider-Cuzmrd7p.js";
89
- import { _ as _64 } from "./chunks/LpRangeSlider-C1yF9-nP.js";
90
- import { _ as _65 } from "./chunks/LpVideoBackdrop-qWRYBOTQ.js";
87
+ import { _ as _62 } from "./chunks/LpSidebar-D8tub3bt.js";
88
+ import { _ as _63 } from "./chunks/LpSkeleton-BB6XIV-w.js";
89
+ import { _ as _64 } from "./chunks/LpSlider-Cuzmrd7p.js";
90
+ import { _ as _65 } from "./chunks/LpRangeSlider-C1yF9-nP.js";
91
+ import { _ as _66 } from "./chunks/LpVideoBackdrop-qWRYBOTQ.js";
91
92
  import { formatClipTime, isNativeLoop, planLoop, seamProgress, seamReached } from "./components/videoBackdrop.js";
92
- import { _ as _66 } from "./chunks/LpSparkline-CB80N8SJ.js";
93
- import { _ as _67 } from "./chunks/LpStat-oYk6V1LZ.js";
94
- import { _ as _68 } from "./chunks/LpStepper-YgHGzgfD.js";
95
- import { _ as _69 } from "./chunks/LpSwitch-DG2AhXIL.js";
93
+ import { _ as _67 } from "./chunks/LpSparkline-CB80N8SJ.js";
94
+ import { _ as _68 } from "./chunks/LpStat-oYk6V1LZ.js";
95
+ import { _ as _69 } from "./chunks/LpStepper-YgHGzgfD.js";
96
+ import { _ as _70 } from "./chunks/LpSwitch-DG2AhXIL.js";
96
97
  import { default as default12 } from "./components/LpTable.vue.js";
97
98
  import { compareValues } from "./components/tableSort.js";
98
- import { _ as _70 } from "./chunks/LpTableOfContents-DYuqkByy.js";
99
- import { _ as _71 } from "./chunks/LpTabs-00v3VQx4.js";
100
- import { _ as _72 } from "./chunks/LpTagsInput-Sc56iy_j.js";
99
+ import { _ as _71 } from "./chunks/LpTableOfContents-DYuqkByy.js";
100
+ import { _ as _72 } from "./chunks/LpTabs-00v3VQx4.js";
101
+ import { _ as _73 } from "./chunks/LpTagsInput-Sc56iy_j.js";
101
102
  import { addTags, splitTags } from "./components/tags.js";
102
- import { _ as _73 } from "./chunks/LpTextarea-CQZccQI3.js";
103
- import { _ as _74 } from "./chunks/LpThemeSwitcher-CeBG0e_D.js";
104
- import { _ as _75 } from "./chunks/LpTilt-B7KHX1zH.js";
105
- import { _ as _76 } from "./chunks/LpToaster-BcKs7vX7.js";
106
- import { _ as _77 } from "./chunks/LpTooltip-suFCM34F.js";
107
- import { _ as _78 } from "./chunks/LpTour-C29-1wha.js";
103
+ import { _ as _74 } from "./chunks/LpTextarea-CQZccQI3.js";
104
+ import { _ as _75 } from "./chunks/LpThemeSwitcher-ZFFftBQn.js";
105
+ import { _ as _76 } from "./chunks/LpTilt-B7KHX1zH.js";
106
+ import { _ as _77 } from "./chunks/LpToaster-D3OisETH.js";
107
+ import { _ as _78 } from "./chunks/LpTooltip-suFCM34F.js";
108
+ import { _ as _79 } from "./chunks/LpTour-ByaL36fN.js";
108
109
  import { holeOf, openContextMenu, panelsAround } from "./components/tour.js";
109
- import { _ as _79 } from "./chunks/LpHintLayer-DsclGYcD.js";
110
- import { _ as _80 } from "./chunks/LpHints-BTpvisgP.js";
110
+ import { _ as _80 } from "./chunks/LpHintLayer-DsclGYcD.js";
111
+ import { _ as _81 } from "./chunks/LpHints-B_85eSmm.js";
111
112
  import { HINT_SIGNALS, contextMenuTip, createHintSystem, hintKey, hintSummary, localHintStorage, paletteTip, provideHintSystem, runningHintSystem, useHintSystem } from "./composables/useHintSystem.js";
112
- import { _ as _81 } from "./chunks/LpTooltipProvider-CftjV7Xc.js";
113
- import { _ as _82 } from "./chunks/LpTopologyCanvas-6ozNrQKh.js";
114
- import { _ as _83 } from "./chunks/LpUptimeBar-B4ltCfno.js";
113
+ import { _ as _82 } from "./chunks/LpTooltipProvider-CftjV7Xc.js";
114
+ import { _ as _83 } from "./chunks/LpTopologyCanvas-6ozNrQKh.js";
115
+ import { _ as _84 } from "./chunks/LpUptimeBar-B4ltCfno.js";
115
116
  import { deserializeLayout, serializeLayout } from "./layout/tree.js";
116
117
  import { addLeaf, addTab, countBlocks, countLeaves, makeLayout, moveLeaf, removeBlock, removeLeaf, reorderTab, resizeAt, setActiveTab } from "./layout/tree.js";
117
118
  import { blockTitle, defineBlocks } from "./layout/registry.js";
@@ -145,7 +146,7 @@ import { useShift } from "./composables/useShift.js";
145
146
  import { useTilt } from "./composables/useTilt.js";
146
147
  import { useZoomPan } from "./composables/useZoomPan.js";
147
148
  import { useToast } from "./composables/useToast.js";
148
- import { _ as _84 } from "./chunks/LpThemeProvider-CQ2jC9TA.js";
149
+ import { _ as _85 } from "./chunks/LpThemeProvider-CQ2jC9TA.js";
149
150
  import { isInternalHref, resolveLink } from "./components/link.js";
150
151
  function defineTheme(base, overrides = {}) {
151
152
  const src = typeof base === "string" ? presets[base] : base;
@@ -218,8 +219,8 @@ export {
218
219
  _30 as LpFieldNotice,
219
220
  _29 as LpFileTree,
220
221
  _31 as LpFormField,
221
- _79 as LpHintLayer,
222
- _80 as LpHints,
222
+ _80 as LpHintLayer,
223
+ _81 as LpHints,
223
224
  _32 as LpIcon,
224
225
  default10 as LpImageEditor,
225
226
  _33 as LpInfraNode,
@@ -250,7 +251,7 @@ export {
250
251
  _52 as LpRadio,
251
252
  _53 as LpRadioGroup,
252
253
  _23 as LpRangeCalendar,
253
- _64 as LpRangeSlider,
254
+ _65 as LpRangeSlider,
254
255
  _54 as LpRollingText,
255
256
  _55 as LpScrollArea,
256
257
  _57 as LpSection,
@@ -259,29 +260,30 @@ export {
259
260
  _59 as LpServiceNode,
260
261
  _60 as LpShift,
261
262
  default11 as LpShortcutsDialog,
262
- _61 as LpSidebar,
263
- _62 as LpSkeleton,
264
- _63 as LpSlider,
265
- _66 as LpSparkline,
266
- _67 as LpStat,
267
- _68 as LpStepper,
268
- _69 as LpSwitch,
263
+ _62 as LpSidebar,
264
+ _63 as LpSkeleton,
265
+ _64 as LpSlider,
266
+ _67 as LpSparkline,
267
+ _68 as LpStat,
268
+ _69 as LpStepper,
269
+ _61 as LpSwap,
270
+ _70 as LpSwitch,
269
271
  default12 as LpTable,
270
- _70 as LpTableOfContents,
271
- _71 as LpTabs,
272
- _72 as LpTagsInput,
273
- _73 as LpTextarea,
274
- _84 as LpThemeProvider,
275
- _74 as LpThemeSwitcher,
276
- _75 as LpTilt,
272
+ _71 as LpTableOfContents,
273
+ _72 as LpTabs,
274
+ _73 as LpTagsInput,
275
+ _74 as LpTextarea,
276
+ _85 as LpThemeProvider,
277
+ _75 as LpThemeSwitcher,
278
+ _76 as LpTilt,
277
279
  _43 as LpTimeField,
278
- _76 as LpToaster,
279
- _77 as LpTooltip,
280
- _81 as LpTooltipProvider,
281
- _82 as LpTopologyCanvas,
282
- _78 as LpTour,
283
- _83 as LpUptimeBar,
284
- _65 as LpVideoBackdrop,
280
+ _77 as LpToaster,
281
+ _78 as LpTooltip,
282
+ _82 as LpTooltipProvider,
283
+ _83 as LpTopologyCanvas,
284
+ _79 as LpTour,
285
+ _84 as LpUptimeBar,
286
+ _66 as LpVideoBackdrop,
285
287
  N as NO_BACKDROP_ATTR,
286
288
  OSM_TILES,
287
289
  T as THEME_COOKIE,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leavepulse/ui",
3
- "version": "0.60.0",
3
+ "version": "0.62.0",
4
4
  "type": "module",
5
5
  "description": "LeavePulse token-driven Vue component kit.",
6
6
  "repository": {
@@ -30,6 +30,7 @@
30
30
  "./css": "./src/index.css",
31
31
  "./tokens.css": "./src/tokens/tokens.css",
32
32
  "./scrollbar.css": "./src/tokens/scrollbar.css",
33
+ "./selection.css": "./src/tokens/selection.css",
33
34
  "./canvas.css": "./src/canvas.css",
34
35
  "./package.json": "./package.json",
35
36
  "./dist/*.js": {
@@ -80,6 +80,7 @@ export const COMPONENT_NAMES = [
80
80
  "LpSparkline",
81
81
  "LpStat",
82
82
  "LpStepper",
83
+ "LpSwap",
83
84
  "LpSwitch",
84
85
  "LpTable",
85
86
  "LpTableOfContents",
@@ -45,23 +45,34 @@ const props = withDefaults(
45
45
  * of the page from the start should not be shouted on load.
46
46
  */
47
47
  live?: "assertive" | "polite" | false
48
+ /**
49
+ * Work is under way: the icon becomes a spinning loader (unless `icon` is
50
+ * given, which then spins instead) and the alert is announced politely.
51
+ * A loader drawn as a still icon read as a frozen page.
52
+ */
53
+ busy?: boolean
48
54
  }>(),
49
- { variant: "info", live: undefined },
55
+ { variant: "info", live: undefined, busy: false },
50
56
  )
51
57
 
52
58
  const role = computed(() => {
53
- const live = props.live ?? (props.variant === "danger" ? "assertive" : false)
59
+ const live = props.live ?? (props.variant === "danger" ? "assertive" : props.busy ? "polite" : false)
54
60
  return live === "assertive" ? "alert" : live === "polite" ? "status" : undefined
55
61
  })
56
62
 
57
63
  const classes = computed(() => alert({ variant: props.variant }))
58
- const resolvedIcon = computed(() => props.icon ?? ICONS[props.variant ?? "info"])
64
+ const resolvedIcon = computed(() => props.icon ?? (props.busy ? "lucide:loader-circle" : ICONS[props.variant ?? "info"]))
59
65
  const iconTone = computed(() => ICON_TONE[props.variant ?? "info"])
60
66
  </script>
61
67
 
62
68
  <template>
63
- <div :class="classes" :role="role">
64
- <LpIcon :name="resolvedIcon" :size="18" :class="['mt-0.5 shrink-0', iconTone]" aria-hidden="true" />
69
+ <div :class="classes" :role="role" :aria-busy="busy || undefined">
70
+ <LpIcon
71
+ :name="resolvedIcon"
72
+ :size="18"
73
+ :class="['mt-0.5 shrink-0', iconTone, busy ? 'animate-spin motion-reduce:animate-none' : '']"
74
+ aria-hidden="true"
75
+ />
65
76
  <div class="min-w-0">
66
77
  <p v-if="title" class="font-semibold text-ink">{{ title }}</p>
67
78
  <div :class="['text-muted-strong', title ? 'mt-0.5' : '']">
@@ -25,7 +25,7 @@ import {
25
25
  ComboboxRoot,
26
26
  ComboboxViewport,
27
27
  } from "reka-ui"
28
- import { computed, ref, useAttrs, watch } from "vue"
28
+ import { computed, nextTick, ref, useAttrs, watch } from "vue"
29
29
  import { useFieldControl } from "../composables/useField"
30
30
  import { useInputFilter } from "../composables/useInputFilter"
31
31
  import { useLabels } from "../composables/useLpConfig"
@@ -36,6 +36,15 @@ export interface AutocompleteOption {
36
36
  value: string
37
37
  label?: string
38
38
  description?: string
39
+ /** A picture beside the option: an avatar, an icon of a server. */
40
+ image?: string
41
+ }
42
+
43
+ /** What the field's value stands for: a name for an id, with an optional muted hint and picture. */
44
+ export interface AutocompleteSelection {
45
+ label: string
46
+ hint?: string
47
+ image?: string
39
48
  }
40
49
 
41
50
  const props = withDefaults(
@@ -65,6 +74,14 @@ const props = withDefaults(
65
74
  pattern?: RegExp | string
66
75
  /** Allowed-character RegExp; hard-blocks other input (see LpInput). */
67
76
  restrict?: RegExp
77
+ /**
78
+ * What the current value stands for, shown in the field itself while it is
79
+ * not being edited: a picked id reads as a person (avatar in place of the
80
+ * icon, name, muted hint) instead of digits nobody recognises, and nothing
81
+ * appears or jumps below the field. Focusing it brings the raw text back.
82
+ * When room runs short the hint gives way first; the name stays.
83
+ */
84
+ selected?: AutocompleteSelection | null
68
85
  }>(),
69
86
  {
70
87
  placeholder: "Type…",
@@ -136,6 +153,12 @@ const filtered = computed(() => {
136
153
 
137
154
  const open = ref(false)
138
155
  const focused = ref(false)
156
+ const showSelected = computed(() => !!props.selected?.label && !focused.value)
157
+ // Focusing a picked value starts a new search from its name, not from the raw
158
+ // value: the name is put in the box selected, so typing replaces it, and
159
+ // leaving without typing keeps the pick as it was.
160
+ const draft = ref<string | null>(null)
161
+ const showImage = computed(() => !!props.selected?.image && (!focused.value || draft.value !== null))
139
162
 
140
163
  // Threshold met AND something to show → the list is allowed to open.
141
164
  const canOpen = computed(
@@ -174,19 +197,31 @@ const anchorSize = {
174
197
  function onInput(event: Event) {
175
198
  filterInput(event)
176
199
  typing.value = true
200
+ draft.value = null
177
201
  text.value = (event.target as HTMLInputElement).value
178
202
  }
179
203
 
180
- function onFocus() {
204
+ function onFocus(event: FocusEvent) {
181
205
  // Arriving at a field that already holds a value shows every option, not just
182
206
  // the one it currently holds.
183
207
  typing.value = false
184
208
  focused.value = true
209
+ if (props.selected?.label) {
210
+ draft.value = props.selected.label
211
+ const input = event.target as HTMLInputElement
212
+ void nextTick(() => input.select())
213
+ }
185
214
  if (canOpen.value) open.value = true
186
215
  }
187
216
 
217
+ function onBlur() {
218
+ focused.value = false
219
+ draft.value = null
220
+ }
221
+
188
222
  function choose(opt: AutocompleteOption) {
189
223
  typing.value = false
224
+ draft.value = null
190
225
  emit("update:modelValue", opt.value)
191
226
  emit("select", opt.value)
192
227
  open.value = false
@@ -194,6 +229,7 @@ function choose(opt: AutocompleteOption) {
194
229
 
195
230
  function clear() {
196
231
  typing.value = false
232
+ draft.value = null
197
233
  emit("update:modelValue", "")
198
234
  open.value = false
199
235
  }
@@ -220,7 +256,28 @@ function clear() {
220
256
  : 'border-line focus-within:border-brand',
221
257
  ]"
222
258
  >
223
- <LpIcon v-if="icon" :name="icon" :size="16" class="shrink-0 text-muted" />
259
+ <!-- Picture and icon share one fixed box and cross-fade, so the text
260
+ beside them never moves when one gives way to the other. -->
261
+ <span
262
+ v-if="icon || selected?.image"
263
+ class="relative grid shrink-0 place-items-center"
264
+ :class="selected?.image ? 'size-5' : 'size-4'"
265
+ >
266
+ <LpIcon
267
+ v-if="icon"
268
+ :name="icon"
269
+ :size="16"
270
+ class="col-start-1 row-start-1 text-muted transition-[opacity,scale] duration-[var(--duration-fast)] motion-reduce:transition-none"
271
+ :class="showImage ? 'scale-75 opacity-0' : 'opacity-100'"
272
+ />
273
+ <img
274
+ v-if="selected?.image"
275
+ :src="selected.image"
276
+ alt=""
277
+ class="col-start-1 row-start-1 size-5 rounded-full object-cover transition-[opacity,scale] duration-[var(--duration-fast)] motion-reduce:transition-none"
278
+ :class="showImage ? 'opacity-100' : 'scale-75 opacity-0'"
279
+ />
280
+ </span>
224
281
  <!--
225
282
  Bind value one-way and update from the raw DOM event, NOT via reka's
226
283
  ComboboxInput v-model: reka defers the value/search update to nextTick
@@ -228,23 +285,49 @@ function clear() {
228
285
  keystroke lag by one char. Reading event.target.value here is synchronous
229
286
  so `text` (and the reactive `open`) track the field exactly.
230
287
  -->
231
- <ComboboxInput
232
- v-bind="inputAttrs"
233
- :id="field.id"
234
- :model-value="text"
235
- :placeholder="placeholder"
236
- :disabled="field.disabled"
237
- :required="field.required || undefined"
238
- :aria-invalid="isInvalidState || undefined"
239
- :aria-describedby="field.describedBy"
240
- class="lp-autofill min-w-0 flex-1 self-stretch bg-transparent outline-none placeholder:text-muted"
241
- @beforeinput="onBeforeInput"
242
- @paste="onPaste"
243
- @compositionend="onCompositionEnd"
244
- @input="onInput"
245
- @focus="onFocus"
246
- @blur="focused = false"
247
- />
288
+ <div class="relative flex min-w-0 flex-1 self-stretch">
289
+ <ComboboxInput
290
+ v-bind="inputAttrs"
291
+ :id="field.id"
292
+ :model-value="draft ?? text"
293
+ :placeholder="placeholder"
294
+ :disabled="field.disabled"
295
+ :required="field.required || undefined"
296
+ :aria-invalid="isInvalidState || undefined"
297
+ :aria-describedby="field.describedBy"
298
+ class="lp-autofill min-w-0 flex-1 self-stretch bg-transparent outline-none placeholder:text-muted"
299
+ :class="showSelected && 'text-transparent placeholder:text-transparent'"
300
+ @beforeinput="onBeforeInput"
301
+ @paste="onPaste"
302
+ @compositionend="onCompositionEnd"
303
+ @input="onInput"
304
+ @focus="onFocus"
305
+ @blur="onBlur"
306
+ />
307
+ <!-- Focus and blur do not animate the name: it sits exactly where the
308
+ input's own text sits, so that swap cannot be seen. The hint, which
309
+ exists in one state alone, eases in and out beside it, and the
310
+ whole line fades when a pick appears or is cleared. -->
311
+ <Transition
312
+ enter-active-class="transition-opacity duration-[var(--duration-fast)] motion-reduce:transition-none"
313
+ leave-active-class="transition-opacity duration-[var(--duration-fast)] motion-reduce:transition-none"
314
+ enter-from-class="opacity-0"
315
+ leave-to-class="opacity-0"
316
+ >
317
+ <span
318
+ v-if="selected?.label"
319
+ class="pointer-events-none absolute inset-0 flex min-w-0 items-center gap-1.5"
320
+ aria-hidden="true"
321
+ >
322
+ <span class="max-w-full shrink-0 truncate" :class="showSelected ? '' : 'invisible'">{{ selected.label }}</span>
323
+ <span
324
+ v-if="selected.hint"
325
+ class="min-w-0 truncate text-sm text-muted transition-[opacity,translate] duration-[var(--duration-fast)] motion-reduce:transition-none"
326
+ :class="showSelected ? 'opacity-100' : '-translate-x-1 opacity-0'"
327
+ >{{ selected.hint }}</span>
328
+ </span>
329
+ </Transition>
330
+ </div>
248
331
  <button
249
332
  v-if="clearable && hasValue"
250
333
  type="button"
@@ -285,10 +368,13 @@ function clear() {
285
368
  :class="[OPTION_ITEM, 'justify-between']"
286
369
  @select.prevent="choose(opt)"
287
370
  >
288
- <span class="flex min-w-0 flex-col">
289
- <span class="truncate">{{ opt.label ?? opt.value }}</span>
290
- <span v-if="opt.description" class="truncate text-xs text-muted">
291
- {{ opt.description }}
371
+ <span class="flex min-w-0 items-center gap-2.5">
372
+ <img v-if="opt.image" :src="opt.image" alt="" class="size-6 shrink-0 rounded-full object-cover" />
373
+ <span class="flex min-w-0 flex-col">
374
+ <span class="truncate">{{ opt.label ?? opt.value }}</span>
375
+ <span v-if="opt.description" class="truncate text-xs text-muted">
376
+ {{ opt.description }}
377
+ </span>
292
378
  </span>
293
379
  </span>
294
380
  </ComboboxItem>
@@ -9,7 +9,7 @@
9
9
  import { ContextMenuContent, ContextMenuPortal, ContextMenuRoot, ContextMenuTrigger } from "reka-ui"
10
10
  import { computed } from "vue"
11
11
  import { hints } from "../composables/useHints"
12
- import { HINT_SIGNALS } from "../composables/useHintSystem"
12
+ import { HINT_SIGNALS, noteContextTarget } from "../composables/useHintSystem"
13
13
  import ContextMenuItems from "./ContextMenuItems.vue"
14
14
  import MenuTitle from "./MenuTitle.vue"
15
15
  import { POPOVER_PANEL } from "./dropdown"
@@ -103,8 +103,11 @@ function opened(open: boolean) {
103
103
  if (open) hints.use(HINT_SIGNALS.contextMenu)
104
104
  }
105
105
 
106
- function leftClick() {
107
- if (hinted.value) hints.use(HINT_SIGNALS.contextTarget)
106
+ function leftClick(event: MouseEvent) {
107
+ if (!hinted.value) return
108
+ hints.use(HINT_SIGNALS.contextTarget)
109
+ const el = event.currentTarget as Element
110
+ noteContextTarget((event.target as Element).closest?.("[data-lp-context-row]") ?? el)
108
111
  }
109
112
 
110
113
  function returnFocus(event: Event) {