@leavepulse/ui 0.34.0 → 0.36.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.
- package/dist/assets/{LpTable-Ckoi05Am.css → LpTable-DfSwPCJ-.css} +21 -8
- package/dist/chunks/{LpAddressInput-FZXfljWV.js → LpAddressInput-D4t8WFD-.js} +1 -1
- package/dist/chunks/{LpConfirmDialog-CQBVaLpo.js → LpConfirmDialog-CxbizvSB.js} +1 -1
- package/dist/chunks/LpModal-7LvuAA7U.js +224 -0
- package/dist/chunks/LpModalNav-DBMGlq_x.js +166 -0
- package/dist/component-names.d.ts +1 -1
- package/dist/components/LpActionBar.vue.d.ts +1 -1
- package/dist/components/LpAddressInput.vue.d.ts +3 -3
- package/dist/components/LpAddressInput.vue.js +1 -1
- package/dist/components/LpAppShell.vue.d.ts +4 -1
- package/dist/components/LpAutocomplete.vue.d.ts +1 -1
- package/dist/components/LpBadge.vue.d.ts +2 -2
- package/dist/components/LpCodeBlock.vue.d.ts +1 -1
- package/dist/components/LpColorPicker.vue.d.ts +9 -4
- package/dist/components/LpConfirmDialog.vue.js +1 -1
- package/dist/components/LpDropzone.vue.d.ts +4 -4
- package/dist/components/LpFileTree.vue.d.ts +4 -4
- package/dist/components/LpFormField.vue.d.ts +1 -1
- package/dist/components/LpLaneNode.vue.d.ts +11 -0
- package/dist/components/LpLightbox.vue.d.ts +2 -2
- package/dist/components/LpLogViewer.vue.d.ts +1 -1
- package/dist/components/LpMapPicker.vue.d.ts +1 -1
- package/dist/components/LpMediaGallery.vue.d.ts +1 -1
- package/dist/components/LpModal.vue.d.ts +9 -5
- package/dist/components/LpModal.vue.js +1 -1
- package/dist/components/LpModalNav.vue.d.ts +63 -0
- package/dist/components/LpModalNav.vue.js +4 -0
- package/dist/components/LpPie.vue.d.ts +22 -4
- package/dist/components/LpPopover.vue.d.ts +1 -1
- package/dist/components/LpProgress.vue.d.ts +2 -2
- package/dist/components/LpRollingText.vue.d.ts +3 -3
- package/dist/components/LpSelect.vue.d.ts +6 -6
- package/dist/components/LpShift.vue.d.ts +1 -1
- package/dist/components/LpShortcutsDialog.vue.d.ts +1 -1
- package/dist/components/LpShortcutsDialog.vue.js +1 -1
- package/dist/components/LpSidebar.vue.d.ts +7 -1
- package/dist/components/LpSidebarNav.vue.d.ts +2 -2
- package/dist/components/LpSlider.vue.d.ts +2 -2
- package/dist/components/LpSparkline.vue.d.ts +2 -2
- package/dist/components/LpTable.vue.d.ts +9 -9
- package/dist/components/LpTable.vue.js +7 -4
- package/dist/components/LpTabs.vue.d.ts +1 -1
- package/dist/components/LpThemeSwitcher.vue.d.ts +1 -1
- package/dist/components/LpTilt.vue.d.ts +1 -1
- package/dist/components/LpTooltip.vue.d.ts +1 -1
- package/dist/components/LpTopologyCanvas.vue.d.ts +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +75 -73
- package/package.json +1 -1
- package/src/component-names.ts +1 -0
- package/src/components/LpModal.vue +69 -1
- package/src/components/LpModalNav.vue +202 -0
- package/src/components/LpTable.vue +22 -2
- package/src/index.ts +1 -0
- package/dist/chunks/LpModal-DAqXzsm-.js +0 -192
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { _ } from "./chunks/LayoutCanvas-Dd20boUG.js";
|
|
2
|
-
import { _ as _2 } from "./chunks/LpAddressInput-
|
|
2
|
+
import { _ as _2 } from "./chunks/LpAddressInput-D4t8WFD-.js";
|
|
3
3
|
import { _ as _3 } from "./chunks/LpMapPicker-ul4oYno6.js";
|
|
4
4
|
import { OSM_TILES, createNominatimProvider, emptyAddress, formatAddress, project, tileUrl, unproject } from "./components/address.js";
|
|
5
5
|
import { _ as _4 } from "./chunks/LpActionBar-BlvtGB2P.js";
|
|
@@ -19,7 +19,7 @@ import { _ as _15 } from "./chunks/LpCodeBlock-Bo0B4TFR.js";
|
|
|
19
19
|
import { _ as _16 } from "./chunks/LpColorPicker-CFC6iaNK.js";
|
|
20
20
|
import { DEFAULT_SWATCHES, clampHsv, formatColor, hsvToHsl, hsvToRgb, isLight, normalizeHue, opaqueCss, parseColor, parseHex, rgbToHex, rgbToHsv } from "./components/color.js";
|
|
21
21
|
import { default as default3 } from "./components/LpCommandPalette.vue.js";
|
|
22
|
-
import { _ as _17 } from "./chunks/LpConfirmDialog-
|
|
22
|
+
import { _ as _17 } from "./chunks/LpConfirmDialog-CxbizvSB.js";
|
|
23
23
|
import { _ as _18 } from "./chunks/LpContextMenu-B6WBLleS.js";
|
|
24
24
|
import { _ as _19 } from "./chunks/LpDatePicker-6JFXllj5.js";
|
|
25
25
|
import { _ as _20 } from "./chunks/LpDisclosure-ChQYmjlR.js";
|
|
@@ -40,46 +40,47 @@ import { fileNameOf } from "./components/lightbox.js";
|
|
|
40
40
|
import { _ as _31 } from "./chunks/LpLink-DCND-zyu.js";
|
|
41
41
|
import { _ as _32 } from "./chunks/LpLogViewer-y53ZqSVq.js";
|
|
42
42
|
import { _ as _33 } from "./chunks/LpMediaGallery-Cid7PGYx.js";
|
|
43
|
-
import { _ as _34 } from "./chunks/LpModal-
|
|
44
|
-
import { _ as _35 } from "./chunks/
|
|
45
|
-
import { _ as _36 } from "./chunks/
|
|
46
|
-
import { _ as _37 } from "./chunks/
|
|
47
|
-
import { _ as _38 } from "./chunks/
|
|
48
|
-
import { _ as _39 } from "./chunks/
|
|
49
|
-
import { _ as _40 } from "./chunks/
|
|
50
|
-
import { _ as _41 } from "./chunks/
|
|
51
|
-
import { _ as _42 } from "./chunks/
|
|
43
|
+
import { _ as _34 } from "./chunks/LpModal-7LvuAA7U.js";
|
|
44
|
+
import { _ as _35 } from "./chunks/LpModalNav-DBMGlq_x.js";
|
|
45
|
+
import { _ as _36 } from "./chunks/LpNotificationBell-lHePf89W.js";
|
|
46
|
+
import { _ as _37 } from "./chunks/LpNumberField-CWuX08iX.js";
|
|
47
|
+
import { _ as _38 } from "./chunks/LpNumberFlow-amWYaeuA.js";
|
|
48
|
+
import { _ as _39 } from "./chunks/LpOtpInput-y2v-pmiN.js";
|
|
49
|
+
import { _ as _40 } from "./chunks/LpPagination-5FxjmmB5.js";
|
|
50
|
+
import { _ as _41 } from "./chunks/LpPasswordInput--sjYCkLS.js";
|
|
51
|
+
import { _ as _42 } from "./chunks/LpPie-c2-q4DJu.js";
|
|
52
|
+
import { _ as _43 } from "./chunks/LpPhoneInput-BDSKVl_z.js";
|
|
52
53
|
import { flagEmoji, loadCountries, matchCountryByValue } from "./components/countries.js";
|
|
53
|
-
import { _ as
|
|
54
|
-
import { _ as
|
|
55
|
-
import { _ as
|
|
56
|
-
import { _ as
|
|
57
|
-
import { _ as
|
|
58
|
-
import { _ as
|
|
59
|
-
import { _ as
|
|
60
|
-
import { _ as
|
|
61
|
-
import { _ as
|
|
62
|
-
import { _ as
|
|
63
|
-
import { _ as
|
|
54
|
+
import { _ as _44 } from "./chunks/LpPopover-8268UmlC.js";
|
|
55
|
+
import { _ as _45 } from "./chunks/LpProgress-Btq5W4Ut.js";
|
|
56
|
+
import { _ as _46 } from "./chunks/LpRadio-B5AfqTpZ.js";
|
|
57
|
+
import { _ as _47 } from "./chunks/LpRadioGroup-xPIuVDO_.js";
|
|
58
|
+
import { _ as _48 } from "./chunks/LpRollingText-CM5gkSeF.js";
|
|
59
|
+
import { _ as _49 } from "./chunks/LpScrollArea-D8xyKXcF.js";
|
|
60
|
+
import { _ as _50 } from "./chunks/LpSegmented-fV0Sk92p.js";
|
|
61
|
+
import { _ as _51 } from "./chunks/LpSection-aM6J_u51.js";
|
|
62
|
+
import { _ as _52 } from "./chunks/LpSelect-ANIVtrpM.js";
|
|
63
|
+
import { _ as _53 } from "./chunks/LpServiceNode-D8uJ8sGk.js";
|
|
64
|
+
import { _ as _54 } from "./chunks/LpShift-DXNsauQZ.js";
|
|
64
65
|
import { default as default6 } from "./components/LpShortcutsDialog.vue.js";
|
|
65
|
-
import { _ as
|
|
66
|
-
import { _ as
|
|
67
|
-
import { _ as
|
|
68
|
-
import { _ as
|
|
69
|
-
import { _ as
|
|
70
|
-
import { _ as
|
|
71
|
-
import { _ as
|
|
66
|
+
import { _ as _55 } from "./chunks/LpSidebar-DcYavim2.js";
|
|
67
|
+
import { _ as _56 } from "./chunks/LpSkeleton-CTI-vBgW.js";
|
|
68
|
+
import { _ as _57 } from "./chunks/LpSlider-BwNIBkAK.js";
|
|
69
|
+
import { _ as _58 } from "./chunks/LpSparkline-C6xQ2dwv.js";
|
|
70
|
+
import { _ as _59 } from "./chunks/LpStat-CQaW8R--.js";
|
|
71
|
+
import { _ as _60 } from "./chunks/LpStepper-Bk-XaQYU.js";
|
|
72
|
+
import { _ as _61 } from "./chunks/LpSwitch-xgnRU2k0.js";
|
|
72
73
|
import { default as default7 } from "./components/LpTable.vue.js";
|
|
73
|
-
import { _ as
|
|
74
|
-
import { _ as
|
|
75
|
-
import { _ as
|
|
74
|
+
import { _ as _62 } from "./chunks/LpTableOfContents-CjvxPMpU.js";
|
|
75
|
+
import { _ as _63 } from "./chunks/LpTabs-Cefe_lys.js";
|
|
76
|
+
import { _ as _64 } from "./chunks/LpTextarea-t7F6gv8E.js";
|
|
76
77
|
import { p as presets, a as parseTheme, s as serializeTheme } from "./chunks/LpThemeSwitcher-r1te9nB2.js";
|
|
77
|
-
import { D, _ as
|
|
78
|
-
import { _ as
|
|
79
|
-
import { _ as
|
|
80
|
-
import { _ as
|
|
81
|
-
import { _ as
|
|
82
|
-
import { _ as
|
|
78
|
+
import { D, _ as _65, N, T, b, c, d, e, f, g, i, l, h, j, n, r, t, k, m, u, v } from "./chunks/LpThemeSwitcher-r1te9nB2.js";
|
|
79
|
+
import { _ as _66 } from "./chunks/LpTilt-DuyKbpkA.js";
|
|
80
|
+
import { _ as _67 } from "./chunks/LpToaster-gDB1EaU3.js";
|
|
81
|
+
import { _ as _68 } from "./chunks/LpTooltip-hCLPAqMG.js";
|
|
82
|
+
import { _ as _69 } from "./chunks/LpTopologyCanvas-Cm1L39uL.js";
|
|
83
|
+
import { _ as _70 } from "./chunks/LpUptimeBar-DvAMno0S.js";
|
|
83
84
|
import { deserializeLayout, serializeLayout } from "./layout/tree.js";
|
|
84
85
|
import { addLeaf, addTab, countBlocks, countLeaves, makeLayout, moveLeaf, removeBlock, removeLeaf, reorderTab, resizeAt, setActiveTab } from "./layout/tree.js";
|
|
85
86
|
import { blockTitle, defineBlocks } from "./layout/registry.js";
|
|
@@ -169,43 +170,44 @@ export {
|
|
|
169
170
|
_3 as LpMapPicker,
|
|
170
171
|
_33 as LpMediaGallery,
|
|
171
172
|
_34 as LpModal,
|
|
172
|
-
_35 as
|
|
173
|
-
_36 as
|
|
174
|
-
_37 as
|
|
175
|
-
_38 as
|
|
176
|
-
_39 as
|
|
177
|
-
_40 as
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
_44 as
|
|
182
|
-
_45 as
|
|
183
|
-
_46 as
|
|
184
|
-
_47 as
|
|
185
|
-
_48 as
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
_52 as
|
|
190
|
-
_53 as
|
|
173
|
+
_35 as LpModalNav,
|
|
174
|
+
_36 as LpNotificationBell,
|
|
175
|
+
_37 as LpNumberField,
|
|
176
|
+
_38 as LpNumberFlow,
|
|
177
|
+
_39 as LpOtpInput,
|
|
178
|
+
_40 as LpPagination,
|
|
179
|
+
_41 as LpPasswordInput,
|
|
180
|
+
_43 as LpPhoneInput,
|
|
181
|
+
_42 as LpPie,
|
|
182
|
+
_44 as LpPopover,
|
|
183
|
+
_45 as LpProgress,
|
|
184
|
+
_46 as LpRadio,
|
|
185
|
+
_47 as LpRadioGroup,
|
|
186
|
+
_48 as LpRollingText,
|
|
187
|
+
_49 as LpScrollArea,
|
|
188
|
+
_51 as LpSection,
|
|
189
|
+
_50 as LpSegmented,
|
|
190
|
+
_52 as LpSelect,
|
|
191
|
+
_53 as LpServiceNode,
|
|
192
|
+
_54 as LpShift,
|
|
191
193
|
default6 as LpShortcutsDialog,
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
194
|
+
_55 as LpSidebar,
|
|
195
|
+
_56 as LpSkeleton,
|
|
196
|
+
_57 as LpSlider,
|
|
197
|
+
_58 as LpSparkline,
|
|
198
|
+
_59 as LpStat,
|
|
199
|
+
_60 as LpStepper,
|
|
200
|
+
_61 as LpSwitch,
|
|
199
201
|
default7 as LpTable,
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
202
|
+
_62 as LpTableOfContents,
|
|
203
|
+
_63 as LpTabs,
|
|
204
|
+
_64 as LpTextarea,
|
|
205
|
+
_65 as LpThemeSwitcher,
|
|
206
|
+
_66 as LpTilt,
|
|
207
|
+
_67 as LpToaster,
|
|
208
|
+
_68 as LpTooltip,
|
|
209
|
+
_69 as LpTopologyCanvas,
|
|
210
|
+
_70 as LpUptimeBar,
|
|
209
211
|
N as NO_BACKDROP_ATTR,
|
|
210
212
|
OSM_TILES,
|
|
211
213
|
T as THEME_COOKIE,
|
package/package.json
CHANGED
package/src/component-names.ts
CHANGED
|
@@ -72,6 +72,20 @@ defineEmits<{ (e: "update:open", value: boolean): void }>()
|
|
|
72
72
|
// Spread rather than a plain `:aria-describedby` binding: with a description
|
|
73
73
|
// the key must be absent entirely so reka-ui's own generated id survives, and
|
|
74
74
|
// only the description-less case overrides it away.
|
|
75
|
+
/*
|
|
76
|
+
* A click on the aside lands outside DialogContent, and reka reads anything
|
|
77
|
+
* outside it as a click on the scrim — the dialog closed under the pointer the
|
|
78
|
+
* moment you picked a section. The aside marks itself, and a click that starts
|
|
79
|
+
* inside one keeps the dialog open. Without an aside nothing carries the
|
|
80
|
+
* attribute and every click behaves exactly as before.
|
|
81
|
+
*/
|
|
82
|
+
const onInteractOutside = (event: Event) => {
|
|
83
|
+
const target = event.target
|
|
84
|
+
if (target instanceof Element && target.closest("[data-lp-modal-aside]")) {
|
|
85
|
+
event.preventDefault()
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
75
89
|
const describedByAttrs = computed(() =>
|
|
76
90
|
props.description ? {} : { "aria-describedby": undefined },
|
|
77
91
|
)
|
|
@@ -93,6 +107,9 @@ const widthClass = computed(() => {
|
|
|
93
107
|
// inset horizontally; top only when there's no header, bottom only when there's
|
|
94
108
|
// no footer (header/footer own those edges).
|
|
95
109
|
const slots = useSlots()
|
|
110
|
+
|
|
111
|
+
// Dialog and aside only form a pair when something fills the slot.
|
|
112
|
+
const hasAside = computed(() => Boolean(slots.aside || slots.asideEnd))
|
|
96
113
|
// The body clips overflow and focus rings are drawn outside their control, so a
|
|
97
114
|
// field flush against the top edge lost its ring; pt-1 clears the ring's width.
|
|
98
115
|
/*
|
|
@@ -225,10 +242,50 @@ watch(
|
|
|
225
242
|
this wrapper: `max-h-full` on a centred flex child resolves against a
|
|
226
243
|
box it is free to overflow, so a tall body stopped handing its
|
|
227
244
|
overflow to the scroll area below and simply ran off-screen. -->
|
|
245
|
+
<!-- A companion panel standing OUTSIDE the dialog, in the same centring
|
|
246
|
+
row: a section list, a preview, a picker. Inside, it would take the
|
|
247
|
+
width the body already wants; measured against the viewport it would
|
|
248
|
+
have to be re-measured on every resize.
|
|
249
|
+
|
|
250
|
+
The row keeps `items-center` until something fills the slot, so a
|
|
251
|
+
dialog without one centres exactly as it always has. With an aside
|
|
252
|
+
the two align to the top of the row instead, and because the row
|
|
253
|
+
collapses to its content the taller of the pair sets that top edge —
|
|
254
|
+
so the aside may be shorter than the dialog, and the dialog is never
|
|
255
|
+
shorter than the aside. Neither is stretched: `stretch` here would
|
|
256
|
+
pull every dialog to the full height of its own ceiling.
|
|
257
|
+
|
|
258
|
+
Height, width, the gap to the dialog and whether it appears at all
|
|
259
|
+
are the caller's, and deliberately so: a utility class written in
|
|
260
|
+
this file is not in the consuming app's Tailwind scan and would never
|
|
261
|
+
be generated. The gap in particular has to go with the aside — kept
|
|
262
|
+
here, it survives an aside hidden at a breakpoint this file knows
|
|
263
|
+
nothing about, and pushes the dialog off centre by its own width. -->
|
|
228
264
|
<div
|
|
229
265
|
class="fixed inset-0 flex items-center justify-center pointer-events-none"
|
|
230
266
|
:style="{ zIndex: layer.panel }"
|
|
231
267
|
>
|
|
268
|
+
<!-- The dialog and its aside stand in a row of their own, and that row is
|
|
269
|
+
what the wrapper centres. Aligning them on the full-screen wrapper
|
|
270
|
+
instead would pin them to the top of the WINDOW, and the dialog would
|
|
271
|
+
be free to end up shorter than the aside beside it.
|
|
272
|
+
`items-start` inside the row: the row collapses to the taller of the
|
|
273
|
+
two, both hang from its top edge, and neither is stretched. -->
|
|
274
|
+
<div
|
|
275
|
+
class="flex items-start"
|
|
276
|
+
:style="hasAside ? { maxHeight: 'min(90vh, calc(100dvh - 2rem))' } : undefined"
|
|
277
|
+
>
|
|
278
|
+
<!-- `pointer-events-auto` because the wrapper lets clicks through to the
|
|
279
|
+
overlay, and the aside is not the overlay. `data-lp-modal-aside` is
|
|
280
|
+
what tells `interact-outside` a click here is not a click on the
|
|
281
|
+
scrim — without it the dialog closes under the pointer. -->
|
|
282
|
+
<aside
|
|
283
|
+
v-if="$slots.aside"
|
|
284
|
+
data-lp-modal-aside
|
|
285
|
+
class="pointer-events-auto flex min-h-0 flex-col"
|
|
286
|
+
>
|
|
287
|
+
<slot name="aside" />
|
|
288
|
+
</aside>
|
|
232
289
|
<!-- Without a description reka-ui warns on every open, and the opt-out it
|
|
233
290
|
checks for is an ABSENT `aria-describedby` — despite the message
|
|
234
291
|
naming the string "undefined" (a leftover from Radix, where
|
|
@@ -240,9 +297,11 @@ watch(
|
|
|
240
297
|
ref="panelRef"
|
|
241
298
|
class="pointer-events-auto flex max-h-[min(90vh,calc(100dvh-2rem))] min-h-0 flex-col overflow-hidden rounded-card border border-line bg-surface-raised shadow-panel outline-none data-[state=open]:animate-[rise-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[rise-out_120ms_cubic-bezier(0.4,0,1,1)]"
|
|
242
299
|
:class="[widthClass, tweening ? 'transition-[height] duration-fast ease-[var(--ease-emphasized)] motion-reduce:transition-none' : '']"
|
|
243
|
-
:style="width ? { width } :
|
|
300
|
+
:style="{ ...(width ? { width } : {}), ...(hasAside ? { alignSelf: 'stretch' } : {}) }"
|
|
244
301
|
v-bind="describedByAttrs"
|
|
245
302
|
@open-auto-focus="onOpenAutoFocus"
|
|
303
|
+
@interact-outside="onInteractOutside"
|
|
304
|
+
@pointer-down-outside="onInteractOutside"
|
|
246
305
|
>
|
|
247
306
|
<header v-if="title || $slots.title" class="flex shrink-0 items-start justify-between gap-4 p-5 pb-3">
|
|
248
307
|
<div class="flex flex-col gap-1">
|
|
@@ -294,6 +353,15 @@ watch(
|
|
|
294
353
|
<slot name="footer" />
|
|
295
354
|
</footer>
|
|
296
355
|
</DialogContent>
|
|
356
|
+
|
|
357
|
+
<aside
|
|
358
|
+
v-if="$slots.asideEnd"
|
|
359
|
+
data-lp-modal-aside
|
|
360
|
+
class="pointer-events-auto flex min-h-0 flex-col"
|
|
361
|
+
>
|
|
362
|
+
<slot name="asideEnd" />
|
|
363
|
+
</aside>
|
|
364
|
+
</div>
|
|
297
365
|
</div>
|
|
298
366
|
</DialogPortal>
|
|
299
367
|
</DialogRoot>
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/*
|
|
3
|
+
* A dialog whose sections stand BESIDE it rather than inside it — settings, a
|
|
4
|
+
* profile with tabs' worth of detail, a wizard you can skip around in. Put the
|
|
5
|
+
* list inside the panel and it takes the width the content already wants; a row
|
|
6
|
+
* of tabs across the top runs out of room at about five.
|
|
7
|
+
*
|
|
8
|
+
* This is LpModal's `aside` slot plus LpSidebar, wired together: the pair
|
|
9
|
+
* shares one centring row, so the list lines up with the dialog at any window
|
|
10
|
+
* width and neither has to measure the other. What is added on top is the part
|
|
11
|
+
* that only matters here — below `mobileBreakpoint` there is no room beside the
|
|
12
|
+
* dialog at all, so the rail folds into a drawer and the header grows the
|
|
13
|
+
* button that opens it. LpSidebar leaves that button to the app, and without
|
|
14
|
+
* one the sections are simply unreachable on a phone.
|
|
15
|
+
*/
|
|
16
|
+
import { computed, ref } from "vue"
|
|
17
|
+
import LpButton from "./LpButton.vue"
|
|
18
|
+
import LpIcon from "./LpIcon.vue"
|
|
19
|
+
import LpModal from "./LpModal.vue"
|
|
20
|
+
import LpSidebar from "./LpSidebar.vue"
|
|
21
|
+
import type { SidebarItem, SidebarSection } from "./sidebar"
|
|
22
|
+
|
|
23
|
+
export type { SidebarItem, SidebarSection } from "./sidebar"
|
|
24
|
+
|
|
25
|
+
type Breakpoint = "sm" | "md" | "lg" | "xl"
|
|
26
|
+
|
|
27
|
+
const props = withDefaults(
|
|
28
|
+
defineProps<{
|
|
29
|
+
open?: boolean
|
|
30
|
+
/** Active section id (v-model). */
|
|
31
|
+
modelValue?: string
|
|
32
|
+
/** Flat sections, or grouped ones — the shapes LpSidebar already takes. */
|
|
33
|
+
items?: SidebarItem[]
|
|
34
|
+
sections?: SidebarSection[]
|
|
35
|
+
title?: string
|
|
36
|
+
description?: string
|
|
37
|
+
size?: "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "full"
|
|
38
|
+
width?: string
|
|
39
|
+
/** Width of the nav rail (any CSS length). */
|
|
40
|
+
navWidth?: string
|
|
41
|
+
/** Put a filter above the nav — see LpSidebar's `searchable`. */
|
|
42
|
+
searchable?: boolean
|
|
43
|
+
searchPlaceholder?: string
|
|
44
|
+
/** Skeleton rows in place of the nav while the sections load. */
|
|
45
|
+
loading?: boolean
|
|
46
|
+
/** Breakpoint below which the rail becomes a drawer. */
|
|
47
|
+
mobileBreakpoint?: Breakpoint
|
|
48
|
+
/** Label for the button that opens that drawer. */
|
|
49
|
+
navLabel?: string
|
|
50
|
+
/** Stand the rail to the right of the dialog instead of the left. */
|
|
51
|
+
navSide?: "start" | "end"
|
|
52
|
+
/**
|
|
53
|
+
* Let the body fill the height instead of scrolling itself. Off by default,
|
|
54
|
+
* as in a plain modal: it existed here to make the rail a full-height
|
|
55
|
+
* column inside the body, and the rail is no longer in the body.
|
|
56
|
+
*/
|
|
57
|
+
fillBody?: boolean
|
|
58
|
+
}>(),
|
|
59
|
+
{
|
|
60
|
+
size: "2xl",
|
|
61
|
+
navWidth: "13rem",
|
|
62
|
+
mobileBreakpoint: "lg",
|
|
63
|
+
navLabel: "Sections",
|
|
64
|
+
navSide: "start",
|
|
65
|
+
},
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
const emit = defineEmits<{
|
|
69
|
+
(e: "update:open", value: boolean): void
|
|
70
|
+
(e: "update:modelValue", value: string): void
|
|
71
|
+
}>()
|
|
72
|
+
|
|
73
|
+
const navOpen = ref(false)
|
|
74
|
+
|
|
75
|
+
const active = computed({
|
|
76
|
+
get: () => props.modelValue ?? "",
|
|
77
|
+
set: (v: string) => emit("update:modelValue", v),
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const hasNav = computed(() => Boolean(props.items?.length || props.sections?.length))
|
|
81
|
+
|
|
82
|
+
/*
|
|
83
|
+
* Both halves of the responsive switch are spelled out rather than built from
|
|
84
|
+
* the prop: a class assembled at runtime is in nobody's Tailwind scan and would
|
|
85
|
+
* never be generated. The rail hides exactly where the button appears.
|
|
86
|
+
*
|
|
87
|
+
* `responsive` on LpSidebar hides its own rail below the breakpoint and draws
|
|
88
|
+
* the drawer as a SIBLING of it, so wrapping the whole thing in a `hidden`
|
|
89
|
+
* element takes the drawer with it — and the sections are then unreachable at
|
|
90
|
+
* exactly the width where the drawer is the only way to them. What is left here
|
|
91
|
+
* is the gap, which has to disappear along with the rail: an aside that is
|
|
92
|
+
* empty but still spaced pushes the dialog off centre.
|
|
93
|
+
*/
|
|
94
|
+
const RAIL_GAP: Record<Breakpoint, string> = {
|
|
95
|
+
sm: "sm:mr-3",
|
|
96
|
+
md: "md:mr-3",
|
|
97
|
+
lg: "lg:mr-3",
|
|
98
|
+
xl: "xl:mr-3",
|
|
99
|
+
}
|
|
100
|
+
const RAIL_GAP_END: Record<Breakpoint, string> = {
|
|
101
|
+
sm: "sm:ml-3",
|
|
102
|
+
md: "md:ml-3",
|
|
103
|
+
lg: "lg:ml-3",
|
|
104
|
+
xl: "xl:ml-3",
|
|
105
|
+
}
|
|
106
|
+
const TRIGGER_VISIBILITY: Record<Breakpoint, string> = {
|
|
107
|
+
sm: "sm:hidden",
|
|
108
|
+
md: "md:hidden",
|
|
109
|
+
lg: "lg:hidden",
|
|
110
|
+
xl: "xl:hidden",
|
|
111
|
+
}
|
|
112
|
+
const railClass = computed(() =>
|
|
113
|
+
(props.navSide === "end" ? RAIL_GAP_END : RAIL_GAP)[props.mobileBreakpoint],
|
|
114
|
+
)
|
|
115
|
+
const triggerClass = computed(() => TRIGGER_VISIBILITY[props.mobileBreakpoint])
|
|
116
|
+
|
|
117
|
+
/*
|
|
118
|
+
* The rail carries the dialog's own ceiling, so a long list scrolls with the
|
|
119
|
+
* dialog instead of running past the bottom of the window. Inline because it
|
|
120
|
+
* mirrors LpModal's max-height, which is not a class this file can put in the
|
|
121
|
+
* consuming app's scan.
|
|
122
|
+
*/
|
|
123
|
+
const railStyle = { maxHeight: "min(90vh, calc(100dvh - 2rem))" }
|
|
124
|
+
</script>
|
|
125
|
+
|
|
126
|
+
<template>
|
|
127
|
+
<LpModal
|
|
128
|
+
:open="open"
|
|
129
|
+
:title="title"
|
|
130
|
+
:description="description"
|
|
131
|
+
:size="size"
|
|
132
|
+
:width="width"
|
|
133
|
+
:fill-body="fillBody"
|
|
134
|
+
@update:open="emit('update:open', $event)"
|
|
135
|
+
>
|
|
136
|
+
<!-- The gap sits on a wrapper rather than on LpSidebar itself: LpSidebar
|
|
137
|
+
splits its `class` between its root and the inner `nav`, so a margin
|
|
138
|
+
given to it lands on both. -->
|
|
139
|
+
<template v-if="hasNav && navSide === 'start'" #aside>
|
|
140
|
+
<div :class="railClass">
|
|
141
|
+
<LpSidebar
|
|
142
|
+
v-model="active"
|
|
143
|
+
v-model:open="navOpen"
|
|
144
|
+
variant="panel"
|
|
145
|
+
:items="items"
|
|
146
|
+
:sections="sections"
|
|
147
|
+
:width="navWidth"
|
|
148
|
+
:searchable="searchable"
|
|
149
|
+
:search-placeholder="searchPlaceholder"
|
|
150
|
+
:loading="loading"
|
|
151
|
+
responsive
|
|
152
|
+
:mobile-breakpoint="mobileBreakpoint"
|
|
153
|
+
:style="railStyle"
|
|
154
|
+
class="rounded-card border border-line bg-surface-raised p-2 shadow-panel"
|
|
155
|
+
/>
|
|
156
|
+
</div>
|
|
157
|
+
</template>
|
|
158
|
+
|
|
159
|
+
<template v-if="hasNav && navSide === 'end'" #asideEnd>
|
|
160
|
+
<div :class="railClass">
|
|
161
|
+
<LpSidebar
|
|
162
|
+
v-model="active"
|
|
163
|
+
v-model:open="navOpen"
|
|
164
|
+
variant="panel"
|
|
165
|
+
:items="items"
|
|
166
|
+
:sections="sections"
|
|
167
|
+
:width="navWidth"
|
|
168
|
+
:searchable="searchable"
|
|
169
|
+
:search-placeholder="searchPlaceholder"
|
|
170
|
+
:loading="loading"
|
|
171
|
+
responsive
|
|
172
|
+
:mobile-breakpoint="mobileBreakpoint"
|
|
173
|
+
:style="railStyle"
|
|
174
|
+
class="rounded-card border border-line bg-surface-raised p-2 shadow-panel"
|
|
175
|
+
/>
|
|
176
|
+
</div>
|
|
177
|
+
</template>
|
|
178
|
+
|
|
179
|
+
<template #title>
|
|
180
|
+
<div class="flex items-center gap-2">
|
|
181
|
+
<LpButton
|
|
182
|
+
v-if="hasNav"
|
|
183
|
+
variant="ghost"
|
|
184
|
+
size="sm"
|
|
185
|
+
square
|
|
186
|
+
:aria-label="navLabel"
|
|
187
|
+
:class="triggerClass"
|
|
188
|
+
@click="navOpen = true"
|
|
189
|
+
>
|
|
190
|
+
<LpIcon name="lucide:menu" :size="18" />
|
|
191
|
+
</LpButton>
|
|
192
|
+
<slot name="title">{{ title }}</slot>
|
|
193
|
+
</div>
|
|
194
|
+
</template>
|
|
195
|
+
|
|
196
|
+
<slot />
|
|
197
|
+
|
|
198
|
+
<template v-if="$slots.footer" #footer>
|
|
199
|
+
<slot name="footer" />
|
|
200
|
+
</template>
|
|
201
|
+
</LpModal>
|
|
202
|
+
</template>
|
|
@@ -451,7 +451,14 @@ const barInsetTop = computed(() =>
|
|
|
451
451
|
this is the fallback rather than a competitor. -->
|
|
452
452
|
<LpContextMenu :items="contextMenu" always>
|
|
453
453
|
<table class="w-full border-collapse text-sm">
|
|
454
|
-
|
|
454
|
+
<!-- The background belongs on `thead` itself, not only on its row:
|
|
455
|
+
`border-collapse` collapses the borders, and with a sticky header the
|
|
456
|
+
rows show through that seam — a band of body text rides across the
|
|
457
|
+
header on every scroll. -->
|
|
458
|
+
<thead
|
|
459
|
+
ref="headEl"
|
|
460
|
+
:class="stickyHeader ? 'sticky top-0 z-10 bg-surface-soft' : ''"
|
|
461
|
+
>
|
|
455
462
|
<tr class="border-b border-line bg-surface-soft">
|
|
456
463
|
<th v-if="selectable" class="w-px px-4 py-3">
|
|
457
464
|
<LpCheckbox
|
|
@@ -522,7 +529,7 @@ const barInsetTop = computed(() =>
|
|
|
522
529
|
name="lp-row"
|
|
523
530
|
:ref="(el: unknown) => roving.setContainer((el as { $el?: HTMLElement })?.$el ?? null)"
|
|
524
531
|
>
|
|
525
|
-
<tr v-if="displayRows.length === 0" key="lp-table-empty">
|
|
532
|
+
<tr v-if="displayRows.length === 0" key="lp-table-empty" class="lp-row-empty">
|
|
526
533
|
<td :colspan="colSpan" class="px-4 py-10 text-center text-muted">
|
|
527
534
|
<LpIcon :name="emptyIcon" :size="22" class="mx-auto mb-2 opacity-60" />
|
|
528
535
|
<div>{{ emptyLabel }}</div>
|
|
@@ -594,6 +601,19 @@ const barInsetTop = computed(() =>
|
|
|
594
601
|
transform: translateX(-0.5rem);
|
|
595
602
|
}
|
|
596
603
|
|
|
604
|
+
/*
|
|
605
|
+
* The empty-state row leaves at once, without the fade. It and the first real
|
|
606
|
+
* rows would otherwise share the table for the length of the animation, and a
|
|
607
|
+
* row of blank height sits above them — the table jumps once on every load.
|
|
608
|
+
*
|
|
609
|
+
* `position: absolute` is what a list would use here, but a positioned `<tr>`
|
|
610
|
+
* drops out of the table layout entirely. Nothing sits below this row to be
|
|
611
|
+
* dragged around, so simply not animating its exit costs nothing.
|
|
612
|
+
*/
|
|
613
|
+
.lp-row-empty.lp-row-leave-active {
|
|
614
|
+
display: none;
|
|
615
|
+
}
|
|
616
|
+
|
|
597
617
|
/*
|
|
598
618
|
* The leaving row stays in the flow. Taking it out with `position: absolute` is
|
|
599
619
|
* the usual trick for a list, but a `<tr>` positioned that way loses the table's
|
package/src/index.ts
CHANGED
|
@@ -105,6 +105,7 @@ export { default as LpLogViewer } from "./components/LpLogViewer.vue"
|
|
|
105
105
|
export type { LogLevel, LogLine } from "./components/LpLogViewer.vue"
|
|
106
106
|
export { default as LpMediaGallery } from "./components/LpMediaGallery.vue"
|
|
107
107
|
export { default as LpModal } from "./components/LpModal.vue"
|
|
108
|
+
export { default as LpModalNav } from "./components/LpModalNav.vue"
|
|
108
109
|
export { default as LpNotificationBell } from "./components/LpNotificationBell.vue"
|
|
109
110
|
export type {
|
|
110
111
|
NotificationItem,
|