@leavepulse/ui 0.12.0 → 0.14.4

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 (160) hide show
  1. package/dist/assets/LpCommandPalette-D315B47-.css +16 -0
  2. package/dist/chunks/LayoutCanvas-Dd20boUG.js +50 -0
  3. package/dist/chunks/LayoutNode-DGpZA9LX.js +225 -0
  4. package/dist/chunks/LpAlert-BAYHDSS1.js +69 -0
  5. package/dist/chunks/LpAppShell-C22642JK.js +170 -0
  6. package/dist/chunks/LpAutocomplete-B9uoCs4t.js +196 -0
  7. package/dist/chunks/LpAvatar-B1Ez0ZAx.js +88 -0
  8. package/dist/chunks/LpBadge-CXzBPnwO.js +42 -0
  9. package/dist/chunks/LpBreadcrumbs-B7Aqatzw.js +42 -0
  10. package/dist/chunks/LpButton-5VXXCjwF.js +101 -0
  11. package/dist/chunks/LpCalendar-B7ZL0ty3.js +150 -0
  12. package/dist/chunks/LpCard-1yvWT9fx.js +66 -0
  13. package/dist/chunks/LpCheckbox-BO0zuuPh.js +55 -0
  14. package/dist/chunks/LpCodeBlock-CSiOSPs8.js +253 -0
  15. package/dist/chunks/LpConfirmDialog-CnHtHWHm.js +62 -0
  16. package/dist/chunks/LpContextMenu-D0mzXqA7.js +151 -0
  17. package/dist/chunks/LpDatePicker-dY2q4dr4.js +122 -0
  18. package/dist/chunks/LpDisclosure-DP6yfgfM.js +76 -0
  19. package/dist/chunks/LpDivider-CUX46KR0.js +43 -0
  20. package/dist/chunks/LpDrawer-CBNVhkg0.js +310 -0
  21. package/dist/chunks/LpDropdownMenu-DJpoqRCb.js +65 -0
  22. package/dist/chunks/LpEmptyState-CrfyRJUG.js +47 -0
  23. package/dist/chunks/LpFormField-Bn-ZwC6z.js +47 -0
  24. package/dist/chunks/LpIcon-CCnX5_2j.js +41 -0
  25. package/dist/chunks/LpInfraNode-C_PHcthC.js +99 -0
  26. package/dist/chunks/LpInput-BauZRyzm.js +136 -0
  27. package/dist/chunks/LpLaneNode-HtnQf2FF.js +33 -0
  28. package/dist/chunks/LpLink-DHwOEp4e.js +36 -0
  29. package/dist/chunks/LpLogViewer-CDBjhXl6.js +469 -0
  30. package/dist/chunks/LpModal-D17Bocq7.js +128 -0
  31. package/dist/chunks/LpNotificationBell-Bm51SkgV.js +232 -0
  32. package/dist/chunks/LpNumberField-C5ar-OwE.js +55 -0
  33. package/dist/chunks/LpOtpInput-DqaT0Z75.js +65 -0
  34. package/dist/chunks/LpPagination-CeGuNe_q.js +132 -0
  35. package/dist/chunks/LpPasswordInput-DIHFw4u0.js +64 -0
  36. package/dist/chunks/LpPhoneInput-LQvVFGMw.js +199 -0
  37. package/dist/chunks/LpPopover-8268UmlC.js +54 -0
  38. package/dist/chunks/LpProgress-BhI0IbjC.js +37 -0
  39. package/dist/chunks/LpRadio-CL-pI0_O.js +39 -0
  40. package/dist/chunks/LpRadioGroup-B4yDTmi6.js +52 -0
  41. package/dist/chunks/LpScrollArea-CcHul_XS.js +67 -0
  42. package/dist/chunks/LpSegmented-BmpanyAo.js +74 -0
  43. package/dist/chunks/LpSelect-DznhPEWX.js +222 -0
  44. package/dist/chunks/LpServiceNode-Dc-Ut5RS.js +51 -0
  45. package/dist/chunks/LpSidebar-CTUdtX7W.js +152 -0
  46. package/dist/chunks/LpSidebarNav-BRwovIc8.js +126 -0
  47. package/dist/chunks/LpSkeleton-eSFIo4kD.js +22 -0
  48. package/dist/chunks/LpSlider-B1SF3pRV.js +53 -0
  49. package/dist/chunks/LpStat-CdbEFOvn.js +88 -0
  50. package/dist/chunks/LpStepper-5dFTrW21.js +53 -0
  51. package/dist/chunks/LpSwitch-v6fnccSM.js +28 -0
  52. package/dist/chunks/LpTable-PHE6UKor.js +229 -0
  53. package/dist/chunks/LpTableOfContents-CjvxPMpU.js +155 -0
  54. package/dist/chunks/LpTabs-D7aTVPOb.js +115 -0
  55. package/dist/chunks/LpTextarea-CRAkhemZ.js +44 -0
  56. package/dist/chunks/LpThemeSwitcher-BgWwfkxs.js +601 -0
  57. package/dist/chunks/LpTilt-DuyKbpkA.js +49 -0
  58. package/dist/chunks/LpToaster-gDB1EaU3.js +196 -0
  59. package/dist/chunks/LpTooltip-CIwoRCag.js +49 -0
  60. package/dist/chunks/LpTopologyCanvas-DVwyQfKa.js +237 -0
  61. package/dist/chunks/LpUptimeBar-CB366k0O.js +127 -0
  62. package/dist/component-names.d.ts +1 -1
  63. package/dist/components/LayoutCanvas.vue.js +4 -0
  64. package/dist/components/LayoutNode.vue.js +4 -0
  65. package/dist/components/LpAlert.vue.js +4 -0
  66. package/dist/components/LpAppShell.vue.js +4 -0
  67. package/dist/components/LpAutocomplete.vue.js +4 -0
  68. package/dist/components/LpAvatar.vue.js +4 -0
  69. package/dist/components/LpBadge.vue.js +4 -0
  70. package/dist/components/LpBreadcrumbs.vue.js +4 -0
  71. package/dist/components/LpButton.vue.js +4 -0
  72. package/dist/components/LpCalendar.vue.js +4 -0
  73. package/dist/components/LpCard.vue.js +4 -0
  74. package/dist/components/LpCheckbox.vue.js +4 -0
  75. package/dist/components/LpCodeBlock.vue.js +4 -0
  76. package/dist/components/LpCommandPalette.vue.js +310 -0
  77. package/dist/components/LpConfirmDialog.vue.js +4 -0
  78. package/dist/components/LpContextMenu.vue.d.ts +7 -0
  79. package/dist/components/LpContextMenu.vue.js +4 -0
  80. package/dist/components/LpDatePicker.vue.js +4 -0
  81. package/dist/components/LpDisclosure.vue.js +4 -0
  82. package/dist/components/LpDivider.vue.js +4 -0
  83. package/dist/components/LpDrawer.vue.d.ts +8 -0
  84. package/dist/components/LpDrawer.vue.js +4 -0
  85. package/dist/components/LpDropdownMenu.vue.js +4 -0
  86. package/dist/components/LpEmptyState.vue.js +4 -0
  87. package/dist/components/LpFormField.vue.js +4 -0
  88. package/dist/components/LpIcon.vue.js +4 -0
  89. package/dist/components/LpInfraNode.vue.js +4 -0
  90. package/dist/components/LpInput.vue.js +4 -0
  91. package/dist/components/LpLaneNode.vue.js +4 -0
  92. package/dist/components/LpLink.vue.js +4 -0
  93. package/dist/components/LpLogViewer.vue.js +4 -0
  94. package/dist/components/LpModal.vue.js +4 -0
  95. package/dist/components/LpNotificationBell.vue.js +4 -0
  96. package/dist/components/LpNumberField.vue.js +4 -0
  97. package/dist/components/LpOtpInput.vue.js +4 -0
  98. package/dist/components/LpPagination.vue.js +4 -0
  99. package/dist/components/LpPasswordInput.vue.js +4 -0
  100. package/dist/components/LpPhoneInput.vue.js +4 -0
  101. package/dist/components/LpPopover.vue.js +4 -0
  102. package/dist/components/LpProgress.vue.js +4 -0
  103. package/dist/components/LpRadio.vue.js +4 -0
  104. package/dist/components/LpRadioGroup.vue.js +4 -0
  105. package/dist/components/LpScrollArea.vue.js +4 -0
  106. package/dist/components/LpSegmented.vue.js +4 -0
  107. package/dist/components/LpSelect.vue.d.ts +2 -0
  108. package/dist/components/LpSelect.vue.js +4 -0
  109. package/dist/components/LpServiceNode.vue.js +4 -0
  110. package/dist/components/LpSidebar.vue.js +4 -0
  111. package/dist/components/LpSidebarNav.vue.js +4 -0
  112. package/dist/components/LpSkeleton.vue.js +4 -0
  113. package/dist/components/LpSlider.vue.js +4 -0
  114. package/dist/components/LpStat.vue.js +4 -0
  115. package/dist/components/LpStepper.vue.js +4 -0
  116. package/dist/components/LpSwitch.vue.js +4 -0
  117. package/dist/components/LpTable.vue.js +4 -0
  118. package/dist/components/LpTableOfContents.vue.js +4 -0
  119. package/dist/components/LpTabs.vue.d.ts +4 -2
  120. package/dist/components/LpTabs.vue.js +4 -0
  121. package/dist/components/LpTextarea.vue.js +4 -0
  122. package/dist/components/LpThemeSwitcher.vue.js +4 -0
  123. package/dist/components/LpTilt.vue.d.ts +31 -0
  124. package/dist/components/LpTilt.vue.js +4 -0
  125. package/dist/components/LpToaster.vue.js +4 -0
  126. package/dist/components/LpTooltip.vue.js +4 -0
  127. package/dist/components/LpTopologyCanvas.vue.js +4 -0
  128. package/dist/components/LpUptimeBar.vue.js +4 -0
  129. package/dist/components/codeHighlight.js +230 -0
  130. package/dist/{countries-data-9rgqJWHo.js → components/countries-data.js} +2 -2
  131. package/dist/components/countries.js +38 -0
  132. package/dist/components/dropdown.js +14 -0
  133. package/dist/components/sidebar.js +1 -0
  134. package/dist/composables/useInputFilter.js +44 -0
  135. package/dist/composables/usePillTransition.js +11 -0
  136. package/dist/composables/useTilt.d.ts +23 -0
  137. package/dist/composables/useTilt.js +57 -0
  138. package/dist/composables/useToast.d.ts +2 -0
  139. package/dist/composables/useToast.js +49 -0
  140. package/dist/index.d.ts +3 -0
  141. package/dist/index.js +197 -17596
  142. package/dist/layout/registry.js +10 -0
  143. package/dist/layout/tree.js +192 -0
  144. package/dist/layout/useLayout.js +61 -0
  145. package/package.json +5 -3
  146. package/src/component-names.ts +1 -0
  147. package/src/components/LpContextMenu.vue +14 -4
  148. package/src/components/LpDrawer.vue +29 -1
  149. package/src/components/LpLogViewer.vue +161 -31
  150. package/src/components/LpSelect.vue +0 -0
  151. package/src/components/LpSidebar.vue +5 -2
  152. package/src/components/LpTabs.vue +19 -6
  153. package/src/components/LpThemeSwitcher.vue +13 -2
  154. package/src/components/LpTilt.vue +61 -0
  155. package/src/components/LpToaster.vue +11 -4
  156. package/src/composables/useTilt.ts +103 -0
  157. package/src/composables/useToast.ts +2 -0
  158. package/src/index.ts +3 -0
  159. package/src/nuxt.ts +10 -1
  160. package/dist/index.css +0 -1
@@ -0,0 +1,10 @@
1
+ function defineBlocks(blocks) {
2
+ return blocks;
3
+ }
4
+ function blockTitle(registry, id) {
5
+ return registry[id]?.title ?? id;
6
+ }
7
+ export {
8
+ blockTitle,
9
+ defineBlocks
10
+ };
@@ -0,0 +1,192 @@
1
+ import { reactive } from "vue";
2
+ let uid = 0;
3
+ const nid = () => `lp-n${uid++}`;
4
+ function leaf(block) {
5
+ return { kind: "leaf", id: nid(), blocks: [block], active: 0, size: 1 };
6
+ }
7
+ function makeLayout(blocks) {
8
+ return reactive({
9
+ kind: "split",
10
+ id: nid(),
11
+ dir: "row",
12
+ size: 1,
13
+ children: blocks.map(leaf)
14
+ });
15
+ }
16
+ function findParent(node, childId) {
17
+ for (const c of node.children) {
18
+ if (c.id === childId) return node;
19
+ if (c.kind === "split") {
20
+ const r = findParent(c, childId);
21
+ if (r) return r;
22
+ }
23
+ }
24
+ return null;
25
+ }
26
+ function findLeaf(node, id) {
27
+ if (node.kind === "leaf") return node.id === id ? node : null;
28
+ for (const c of node.children) {
29
+ const r = findLeaf(c, id);
30
+ if (r) return r;
31
+ }
32
+ return null;
33
+ }
34
+ function countLeaves(node) {
35
+ if (node.kind === "leaf") return 1;
36
+ return node.children.reduce((s, c) => s + countLeaves(c), 0);
37
+ }
38
+ function countBlocks(node) {
39
+ if (node.kind === "leaf") return node.blocks.length;
40
+ return node.children.reduce((s, c) => s + countBlocks(c), 0);
41
+ }
42
+ function detach(root, id) {
43
+ const parent = findParent(root, id);
44
+ if (!parent) return;
45
+ const idx = parent.children.findIndex((c) => c.id === id);
46
+ parent.children.splice(idx, 1);
47
+ collapse(root, parent);
48
+ }
49
+ function collapse(root, parent) {
50
+ if (parent === root || parent.children.length !== 1) return;
51
+ const grand = findParent(root, parent.id);
52
+ if (!grand) return;
53
+ const only = parent.children[0];
54
+ only.size = parent.size;
55
+ const pIdx = grand.children.findIndex((c) => c.id === parent.id);
56
+ grand.children.splice(pIdx, 1, only);
57
+ }
58
+ function removeLeaf(root, id) {
59
+ if (countLeaves(root) <= 1) return;
60
+ detach(root, id);
61
+ }
62
+ function removeBlock(root, leafId, index) {
63
+ const target = findLeaf(root, leafId);
64
+ if (!target || index < 0 || index >= target.blocks.length) return;
65
+ if (target.blocks.length > 1) {
66
+ target.blocks.splice(index, 1);
67
+ if (target.active >= target.blocks.length) target.active = target.blocks.length - 1;
68
+ return;
69
+ }
70
+ if (countBlocks(root) <= 1) return;
71
+ detach(root, leafId);
72
+ }
73
+ function addLeaf(root, block, side = "right") {
74
+ const targetId = root.children[root.children.length - 1]?.id;
75
+ if (!targetId) {
76
+ root.children.push(leaf(block));
77
+ return;
78
+ }
79
+ if (side === "center") {
80
+ addTab(root, targetId, block);
81
+ return;
82
+ }
83
+ insertBeside(root, leaf(block), targetId, side);
84
+ }
85
+ function addTab(root, leafId, block) {
86
+ const target = findLeaf(root, leafId);
87
+ if (!target) return;
88
+ target.blocks.push(block);
89
+ target.active = target.blocks.length - 1;
90
+ }
91
+ function setActiveTab(root, leafId, index) {
92
+ const target = findLeaf(root, leafId);
93
+ if (!target || index < 0 || index >= target.blocks.length) return;
94
+ target.active = index;
95
+ }
96
+ function reorderTab(root, leafId, from, to) {
97
+ const target = findLeaf(root, leafId);
98
+ if (!target) return;
99
+ const n = target.blocks.length;
100
+ if (from < 0 || from >= n || to < 0 || to >= n || from === to) return;
101
+ const activeBlock = target.blocks[target.active];
102
+ const [moved] = target.blocks.splice(from, 1);
103
+ target.blocks.splice(to, 0, moved);
104
+ target.active = target.blocks.indexOf(activeBlock);
105
+ }
106
+ function insertBeside(root, moving, targetId, side) {
107
+ const parent = findParent(root, targetId);
108
+ if (!parent) return;
109
+ const idx = parent.children.findIndex((c) => c.id === targetId);
110
+ const target = parent.children[idx];
111
+ const wantDir = side === "left" || side === "right" ? "row" : "col";
112
+ const before = side === "left" || side === "top";
113
+ if (parent.dir === wantDir) {
114
+ moving.size = target.size / 2;
115
+ target.size = target.size / 2;
116
+ parent.children.splice(before ? idx : idx + 1, 0, moving);
117
+ } else {
118
+ const wrap = {
119
+ kind: "split",
120
+ id: nid(),
121
+ dir: wantDir,
122
+ size: target.size,
123
+ children: before ? [{ ...moving, size: 1 }, { ...target, size: 1 }] : [{ ...target, size: 1 }, { ...moving, size: 1 }]
124
+ };
125
+ parent.children.splice(idx, 1, wrap);
126
+ }
127
+ }
128
+ function moveLeaf(root, movingId, targetId, side) {
129
+ if (movingId === targetId) return;
130
+ const moving = findLeaf(root, movingId);
131
+ if (!moving) return;
132
+ if (side === "center") {
133
+ const target = findLeaf(root, targetId);
134
+ if (!target) return;
135
+ const merged = moving.blocks;
136
+ detach(root, movingId);
137
+ target.active = target.blocks.length;
138
+ target.blocks.push(...merged);
139
+ return;
140
+ }
141
+ detach(root, movingId);
142
+ moving.size = 1;
143
+ insertBeside(root, moving, targetId, side);
144
+ }
145
+ function resizeAt(parent, index, deltaFraction) {
146
+ const a = parent.children[index];
147
+ const b = parent.children[index + 1];
148
+ if (!a || !b) return;
149
+ const total = a.size + b.size;
150
+ const min = total * 0.12;
151
+ const na = Math.max(min, Math.min(total - min, a.size + deltaFraction * total));
152
+ a.size = na;
153
+ b.size = total - na;
154
+ }
155
+ function serializeLayout(root) {
156
+ const strip = (n) => n.kind === "leaf" ? { kind: "leaf", blocks: [...n.blocks], active: n.active, size: n.size } : { kind: "split", dir: n.dir, size: n.size, children: n.children.map(strip) };
157
+ return strip(root);
158
+ }
159
+ function deserializeLayout(data) {
160
+ const hydrate = (n) => {
161
+ if (n.kind === "leaf") {
162
+ const blocks = n.blocks ?? (n.block != null ? [n.block] : []);
163
+ const active = Math.min(Math.max(n.active ?? 0, 0), Math.max(blocks.length - 1, 0));
164
+ return { kind: "leaf", id: nid(), blocks, active, size: n.size ?? 1 };
165
+ }
166
+ return {
167
+ kind: "split",
168
+ id: nid(),
169
+ dir: n.dir,
170
+ size: n.size ?? 1,
171
+ children: n.children.map(hydrate)
172
+ };
173
+ };
174
+ return reactive(hydrate(data));
175
+ }
176
+ export {
177
+ addLeaf,
178
+ addTab,
179
+ countBlocks,
180
+ countLeaves,
181
+ deserializeLayout,
182
+ findLeaf,
183
+ findParent,
184
+ makeLayout,
185
+ moveLeaf,
186
+ removeBlock,
187
+ removeLeaf,
188
+ reorderTab,
189
+ resizeAt,
190
+ serializeLayout,
191
+ setActiveTab
192
+ };
@@ -0,0 +1,61 @@
1
+ import { shallowRef, computed, ref } from "vue";
2
+ import { makeLayout, countBlocks, serializeLayout, resizeAt, setActiveTab, deserializeLayout, moveLeaf, removeLeaf, removeBlock, reorderTab, addTab, addLeaf } from "./tree.js";
3
+ function useLayout(options) {
4
+ const limit = options.historyLimit ?? 50;
5
+ const layout = shallowRef(makeLayout(options.initial));
6
+ const count = computed(() => countBlocks(layout.value));
7
+ const past = ref([]);
8
+ const future = ref([]);
9
+ const canUndo = computed(() => past.value.length > 0);
10
+ const canRedo = computed(() => future.value.length > 0);
11
+ const snapshot = () => JSON.stringify(serializeLayout(layout.value));
12
+ function record(mutate) {
13
+ const before = snapshot();
14
+ mutate();
15
+ past.value.push(before);
16
+ if (past.value.length > limit) past.value.shift();
17
+ future.value = [];
18
+ }
19
+ function restore(serialized) {
20
+ layout.value = deserializeLayout(JSON.parse(serialized));
21
+ }
22
+ return {
23
+ layout,
24
+ count,
25
+ canUndo,
26
+ canRedo,
27
+ add: (block, side) => record(() => addLeaf(layout.value, block, side)),
28
+ addTab: (leafId, block) => record(() => addTab(layout.value, leafId, block)),
29
+ setActive: (leafId, index) => setActiveTab(layout.value, leafId, index),
30
+ reorderTab: (leafId, from, to) => record(() => reorderTab(layout.value, leafId, from, to)),
31
+ removeBlock: (leafId, index) => record(() => removeBlock(layout.value, leafId, index)),
32
+ remove: (leafId) => record(() => removeLeaf(layout.value, leafId)),
33
+ move: (movingId, targetId, side) => record(() => moveLeaf(layout.value, movingId, targetId, side)),
34
+ // Resize fires continuously during a divider drag; recording every frame
35
+ // would flood history, so it mutates without a snapshot. (A consumer that
36
+ // wants resize in history can wrap a single record() around the gesture.)
37
+ resize: (parent, index, delta) => resizeAt(parent, index, delta),
38
+ undo: () => {
39
+ const prev = past.value.pop();
40
+ if (prev === void 0) return;
41
+ future.value.push(snapshot());
42
+ restore(prev);
43
+ },
44
+ redo: () => {
45
+ const next = future.value.pop();
46
+ if (next === void 0) return;
47
+ past.value.push(snapshot());
48
+ restore(next);
49
+ },
50
+ reset: (blocks) => record(() => {
51
+ layout.value = makeLayout(blocks ?? options.initial);
52
+ }),
53
+ serialize: () => serializeLayout(layout.value),
54
+ hydrate: (data) => record(() => {
55
+ layout.value = deserializeLayout(data);
56
+ })
57
+ };
58
+ }
59
+ export {
60
+ useLayout
61
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leavepulse/ui",
3
- "version": "0.12.0",
3
+ "version": "0.14.4",
4
4
  "type": "module",
5
5
  "description": "LeavePulse token-driven Vue component kit.",
6
6
  "repository": {
@@ -26,7 +26,9 @@
26
26
  "./css": "./src/index.css",
27
27
  "./tokens.css": "./src/tokens/tokens.css",
28
28
  "./scrollbar.css": "./src/tokens/scrollbar.css",
29
- "./canvas.css": "./src/canvas.css"
29
+ "./canvas.css": "./src/canvas.css",
30
+ "./package.json": "./package.json",
31
+ "./dist/*": "./dist/*"
30
32
  },
31
33
  "publishConfig": {
32
34
  "registry": "https://npm.pkg.github.com"
@@ -58,7 +60,6 @@
58
60
  "tailwind-merge": "^3.0.0",
59
61
  "tailwind-variants": "^3.0.0",
60
62
  "tailwindcss": "^4.0.0",
61
- "vaul-vue": "^0.4.0",
62
63
  "vue": "^3.5.0"
63
64
  },
64
65
  "peerDependenciesMeta": {
@@ -80,6 +81,7 @@
80
81
  "typescript": "^5.6.3",
81
82
  "vite": "^7.0.0",
82
83
  "vite-plugin-dts": "^5.0.2",
84
+ "vite-plugin-lib-inject-css": "^2.2.2",
83
85
  "vue": "3.5.31",
84
86
  "vue-tsc": "^3.0.0"
85
87
  },
@@ -58,6 +58,7 @@ export const COMPONENT_NAMES = [
58
58
  "LpTabs",
59
59
  "LpTextarea",
60
60
  "LpThemeSwitcher",
61
+ "LpTilt",
61
62
  "LpToaster",
62
63
  "LpTooltip",
63
64
  "LpTopologyCanvas",
@@ -34,12 +34,22 @@ export interface ContextMenuItemDef {
34
34
  children?: ContextMenuItemDef[]
35
35
  }
36
36
 
37
- const props = defineProps<{ items: ContextMenuItemDef[] }>()
37
+ const props = defineProps<{
38
+ items: ContextMenuItemDef[]
39
+ /**
40
+ * Keep the reka menu mounted even while `items` is momentarily empty. Use when
41
+ * the items are resolved on the right-click itself (e.g. a list that figures
42
+ * out which row was clicked): without this the trigger would still be in
43
+ * native-menu mode on that first click and swallow it.
44
+ */
45
+ always?: boolean
46
+ }>()
38
47
 
39
48
  // With no items, render just the trigger slot — no reka machinery — so the
40
49
  // element keeps the browser's native context menu. Lets callers wrap
41
- // unconditionally and pass [] to opt out per element.
42
- const enabled = computed(() => props.items.length > 0)
50
+ // unconditionally and pass [] to opt out per element. `always` overrides this so
51
+ // a late-populated menu is ready on the very first click.
52
+ const enabled = computed(() => props.always || props.items.length > 0)
43
53
 
44
54
  const PANEL = `${POPOVER_PANEL} z-(--z-popover) min-w-48 p-1 outline-none`
45
55
  const ITEM =
@@ -55,7 +65,7 @@ const ICON =
55
65
  <ContextMenuTrigger as-child>
56
66
  <slot />
57
67
  </ContextMenuTrigger>
58
- <ContextMenuPortal>
68
+ <ContextMenuPortal v-if="items.length">
59
69
  <ContextMenuContent :class="PANEL">
60
70
  <template v-for="(item, i) in items" :key="i">
61
71
  <ContextMenuSeparator v-if="item.separatorBefore" class="my-1 h-px bg-line" />
@@ -73,6 +73,14 @@ const props = withDefaults(
73
73
  * meaningful for left/right drawers.
74
74
  */
75
75
  edgeOpen?: boolean
76
+ /**
77
+ * Tailwind breakpoint above which the edge strip is disabled. Edge
78
+ * swipe-to-open only makes sense on mobile: the strip listens for
79
+ * touch/pen pointers only, so on a desktop it can never open the drawer
80
+ * but still overlays the screen edge and swallows clicks. Pass the same
81
+ * breakpoint at which the host switches to the drawer (LpSidebar does).
82
+ */
83
+ edgeBreakpoint?: "sm" | "md" | "lg" | "xl"
76
84
  /** Width (px) of the edge grab strip. */
77
85
  edgeSize?: number
78
86
  /** Fraction (0–1) of the panel width the pull must reach to open on release. */
@@ -287,6 +295,19 @@ const edgeStripStyle = computed(() => {
287
295
  ? { right: "0", width: w }
288
296
  : { left: "0", width: w }
289
297
  })
298
+
299
+ // The strip must only exist below the host's mobile breakpoint. It is hidden
300
+ // with the same Tailwind breakpoint classes the rest of the mobile logic
301
+ // (LpSidebar rail, burger) uses — no JS media query needed.
302
+ const EDGE_STRIP_HIDDEN: Record<NonNullable<typeof props.edgeBreakpoint>, string> = {
303
+ sm: "sm:hidden",
304
+ md: "md:hidden",
305
+ lg: "lg:hidden",
306
+ xl: "xl:hidden",
307
+ }
308
+ const edgeStripClass = computed(() =>
309
+ props.edgeBreakpoint ? EDGE_STRIP_HIDDEN[props.edgeBreakpoint] : "",
310
+ )
290
311
  </script>
291
312
 
292
313
  <template>
@@ -296,6 +317,7 @@ const edgeStripStyle = computed(() => {
296
317
  <div
297
318
  ref="edgeStrip"
298
319
  class="fixed inset-y-0 z-(--z-overlay) touch-none"
320
+ :class="edgeStripClass"
299
321
  :style="edgeStripStyle"
300
322
  aria-hidden="true"
301
323
  />
@@ -380,9 +402,15 @@ const edgeStripStyle = computed(() => {
380
402
  no-drag wiring on the OUTER element so vaul still sees one content
381
403
  container (overlay scroll lives one level in); padding moves to the
382
404
  scroll content so the bar floats at the panel edge. -->
405
+ <!-- select-text (arbitrary + important): vaul injects a global
406
+ `[data-vaul-drawer]{user-select:none}` rule for pointer devices to
407
+ stop a swipe-drag from selecting text. That also kills legitimate
408
+ selection/Ctrl+C in the body (e.g. log lines), so we override it back
409
+ to selectable here. Drag-to-dismiss still starts on the handle/header
410
+ and empty space, not on this text. -->
383
411
  <div
384
412
  v-no-drag-controls="noDragControls"
385
- class="flex min-h-0 min-w-0 flex-1 flex-col"
413
+ class="flex min-h-0 min-w-0 flex-1 flex-col [-webkit-user-select:text]! [user-select:text]!"
386
414
  :data-vaul-no-drag="noDragContent ? '' : undefined"
387
415
  >
388
416
  <!-- min-w-0 on the body + scroll content so a wide child (e.g. a log