@leavepulse/ui 0.35.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.
@@ -6,7 +6,7 @@ import { _ as _sfc_main$3 } from "./LpButton-5VXXCjwF.js";
6
6
  import { _ as _sfc_main$1 } from "./LpFormField-CazBc2-a.js";
7
7
  import { _ as _sfc_main$4 } from "./LpIcon-CCnX5_2j.js";
8
8
  import { _ as _sfc_main$5 } from "./LpInput-9EXWUM9B.js";
9
- import { _ as _sfc_main$7 } from "./LpModal-DAqXzsm-.js";
9
+ import { _ as _sfc_main$7 } from "./LpModal-7LvuAA7U.js";
10
10
  import { _ as _sfc_main$6 } from "./LpSelect-ANIVtrpM.js";
11
11
  import { useMergedAttrs } from "../composables/useMergedClass.js";
12
12
  const _hoisted_1 = { class: "flex items-stretch gap-2" };
@@ -1,7 +1,7 @@
1
1
  import { defineComponent, computed, openBlock, createBlock, createSlots, withCtx, renderSlot, createVNode, createTextVNode, toDisplayString, createElementVNode, normalizeClass } from "vue";
2
2
  import { _ as _sfc_main$2 } from "./LpButton-5VXXCjwF.js";
3
3
  import { _ as _sfc_main$3 } from "./LpIcon-CCnX5_2j.js";
4
- import { _ as _sfc_main$1 } from "./LpModal-DAqXzsm-.js";
4
+ import { _ as _sfc_main$1 } from "./LpModal-7LvuAA7U.js";
5
5
  const _hoisted_1 = { class: "inline-flex items-center gap-2.5" };
6
6
  const _sfc_main = /* @__PURE__ */ defineComponent({
7
7
  __name: "LpConfirmDialog",
@@ -0,0 +1,224 @@
1
+ import { defineComponent, computed, useSlots, watch, onBeforeUnmount, ref, openBlock, createBlock, unref, withCtx, createVNode, normalizeStyle, createElementVNode, createElementBlock, renderSlot, createCommentVNode, mergeProps, createTextVNode, toDisplayString, normalizeClass } from "vue";
2
+ import { DialogRoot, DialogPortal, DialogOverlay, DialogContent, DialogTitle, DialogDescription, DialogClose } from "reka-ui";
3
+ import { useShift } from "../composables/useShift.js";
4
+ import { useModalLayer } from "../components/modalDepth.js";
5
+ import { CLOSE_ICON } from "../components/dropdown.js";
6
+ import { _ as _sfc_main$1 } from "./LpIcon-CCnX5_2j.js";
7
+ import { _ as _sfc_main$2 } from "./LpScrollArea-D8xyKXcF.js";
8
+ const _hoisted_1 = {
9
+ key: 0,
10
+ "data-lp-modal-aside": "",
11
+ class: "pointer-events-auto flex min-h-0 flex-col"
12
+ };
13
+ const _hoisted_2 = {
14
+ key: 0,
15
+ class: "flex shrink-0 items-start justify-between gap-4 p-5 pb-3"
16
+ };
17
+ const _hoisted_3 = { class: "flex flex-col gap-1" };
18
+ const _hoisted_4 = {
19
+ key: 3,
20
+ class: "flex shrink-0 justify-end gap-2 p-5 pt-4"
21
+ };
22
+ const _hoisted_5 = {
23
+ key: 1,
24
+ "data-lp-modal-aside": "",
25
+ class: "pointer-events-auto flex min-h-0 flex-col"
26
+ };
27
+ const _sfc_main = /* @__PURE__ */ defineComponent({
28
+ __name: "LpModal",
29
+ props: {
30
+ open: { type: Boolean },
31
+ title: {},
32
+ description: {},
33
+ size: { default: "md" },
34
+ width: {},
35
+ fillBody: { type: Boolean },
36
+ autoFocus: { default: "auto" }
37
+ },
38
+ emits: ["update:open"],
39
+ setup(__props) {
40
+ const { claim, release, layer } = useModalLayer();
41
+ const props = __props;
42
+ const onInteractOutside = (event) => {
43
+ const target = event.target;
44
+ if (target instanceof Element && target.closest("[data-lp-modal-aside]")) {
45
+ event.preventDefault();
46
+ }
47
+ };
48
+ const describedByAttrs = computed(
49
+ () => props.description ? {} : { "aria-describedby": void 0 }
50
+ );
51
+ const widthClass = computed(() => {
52
+ if (props.width) return "";
53
+ return {
54
+ sm: "w-[min(92vw,24rem)]",
55
+ md: "w-[min(92vw,28rem)]",
56
+ lg: "w-[min(92vw,34rem)]",
57
+ xl: "w-[min(92vw,42rem)]",
58
+ "2xl": "w-[min(94vw,56rem)]",
59
+ "3xl": "w-[min(95vw,72rem)]",
60
+ full: "w-[96vw]"
61
+ }[props.size];
62
+ });
63
+ const slots = useSlots();
64
+ const hasAside = computed(() => Boolean(slots.aside || slots.asideEnd));
65
+ watch(() => props.open, (open) => open ? claim() : release(), { immediate: true });
66
+ onBeforeUnmount(release);
67
+ const bodyPad = computed(
68
+ () => [
69
+ "px-5",
70
+ props.title || slots.title ? "pt-1" : "pt-5",
71
+ slots.footer ? "" : "pb-5"
72
+ ].filter(Boolean).join(" ")
73
+ );
74
+ const panelRef = ref(null);
75
+ const AUTO_FOCUS_ORDER = [
76
+ "input:not([type=hidden]):not([disabled]):not([readonly])",
77
+ "textarea:not([disabled]):not([readonly])",
78
+ "select:not([disabled])",
79
+ "[data-lp-autofocus]",
80
+ "button:not([disabled]):not([data-lp-dialog-close])",
81
+ "[href]",
82
+ "[tabindex]:not([tabindex='-1'])"
83
+ ];
84
+ function onOpenAutoFocus(event) {
85
+ if (props.autoFocus === "none") return;
86
+ const panel = panelRef.value?.$el;
87
+ if (!(panel instanceof HTMLElement)) return;
88
+ const selectors = props.autoFocus === "auto" ? AUTO_FOCUS_ORDER : [props.autoFocus];
89
+ for (const selector of selectors) {
90
+ const target = panel.querySelector(selector);
91
+ if (!target || target.hasAttribute("disabled")) continue;
92
+ event.preventDefault();
93
+ if (!target.closest("[data-lp-ring-owner]")) {
94
+ target.setAttribute("data-lp-focus-ring", "");
95
+ target.addEventListener(
96
+ "blur",
97
+ () => target.removeAttribute("data-lp-focus-ring"),
98
+ { once: true }
99
+ );
100
+ }
101
+ target.focus();
102
+ return;
103
+ }
104
+ }
105
+ const {
106
+ el: sizedEl,
107
+ tweening,
108
+ resizing
109
+ } = useShift({ axis: "height", enabled: () => props.open });
110
+ watch(
111
+ () => props.open ? panelRef.value : null,
112
+ (panel) => {
113
+ const el = panel?.$el;
114
+ sizedEl.value = el instanceof HTMLElement ? el : null;
115
+ },
116
+ { flush: "post" }
117
+ );
118
+ return (_ctx, _cache) => {
119
+ return openBlock(), createBlock(unref(DialogRoot), {
120
+ open: __props.open,
121
+ "onUpdate:open": _cache[0] || (_cache[0] = (v) => _ctx.$emit("update:open", v))
122
+ }, {
123
+ default: withCtx(() => [
124
+ createVNode(unref(DialogPortal), null, {
125
+ default: withCtx(() => [
126
+ createVNode(unref(DialogOverlay), {
127
+ class: "lp-scrim fixed inset-0 data-[state=open]:animate-[fade-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[fade-out_120ms_ease]",
128
+ style: normalizeStyle({ zIndex: unref(layer).scrim })
129
+ }, null, 8, ["style"]),
130
+ createElementVNode("div", {
131
+ class: "fixed inset-0 flex items-center justify-center pointer-events-none",
132
+ style: normalizeStyle({ zIndex: unref(layer).panel })
133
+ }, [
134
+ createElementVNode("div", {
135
+ class: "flex items-start",
136
+ style: normalizeStyle(hasAside.value ? { maxHeight: "min(90vh, calc(100dvh - 2rem))" } : void 0)
137
+ }, [
138
+ _ctx.$slots.aside ? (openBlock(), createElementBlock("aside", _hoisted_1, [
139
+ renderSlot(_ctx.$slots, "aside")
140
+ ])) : createCommentVNode("", true),
141
+ createVNode(unref(DialogContent), mergeProps({
142
+ ref_key: "panelRef",
143
+ ref: panelRef,
144
+ 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)]", [widthClass.value, unref(tweening) ? "transition-[height] duration-fast ease-[var(--ease-emphasized)] motion-reduce:transition-none" : ""]],
145
+ style: { ...__props.width ? { width: __props.width } : {}, ...hasAside.value ? { alignSelf: "stretch" } : {} }
146
+ }, describedByAttrs.value, {
147
+ onOpenAutoFocus,
148
+ onInteractOutside,
149
+ onPointerDownOutside: onInteractOutside
150
+ }), {
151
+ default: withCtx(() => [
152
+ __props.title || _ctx.$slots.title ? (openBlock(), createElementBlock("header", _hoisted_2, [
153
+ createElementVNode("div", _hoisted_3, [
154
+ createVNode(unref(DialogTitle), { class: "text-base font-semibold text-ink" }, {
155
+ default: withCtx(() => [
156
+ renderSlot(_ctx.$slots, "title", {}, () => [
157
+ createTextVNode(toDisplayString(__props.title), 1)
158
+ ])
159
+ ]),
160
+ _: 3
161
+ }),
162
+ __props.description ? (openBlock(), createBlock(unref(DialogDescription), {
163
+ key: 0,
164
+ class: "text-sm text-muted"
165
+ }, {
166
+ default: withCtx(() => [
167
+ createTextVNode(toDisplayString(__props.description), 1)
168
+ ]),
169
+ _: 1
170
+ })) : createCommentVNode("", true)
171
+ ]),
172
+ createVNode(unref(DialogClose), {
173
+ "data-lp-dialog-close": "",
174
+ class: "group flex shrink-0 items-center rounded-md p-1 text-muted outline-none transition-colors duration-[var(--duration-fast)] hover:text-ink focus-visible:ring-2 focus-visible:ring-ring",
175
+ "aria-label": "Close"
176
+ }, {
177
+ default: withCtx(() => [
178
+ createVNode(_sfc_main$1, {
179
+ name: "lucide:x",
180
+ size: 18,
181
+ class: normalizeClass(unref(CLOSE_ICON))
182
+ }, null, 8, ["class"])
183
+ ]),
184
+ _: 1
185
+ })
186
+ ])) : createCommentVNode("", true),
187
+ __props.fillBody ? (openBlock(), createElementBlock("div", {
188
+ key: 1,
189
+ class: normalizeClass(["flex min-h-0 flex-1 flex-col overflow-hidden text-sm text-ink/90", bodyPad.value])
190
+ }, [
191
+ renderSlot(_ctx.$slots, "default")
192
+ ], 2)) : (openBlock(), createBlock(_sfc_main$2, {
193
+ key: 2,
194
+ class: normalizeClass(["min-h-0 flex-1 text-sm text-ink/90", unref(resizing) ? "[&_[data-scrollbarimpl]]:invisible" : ""]),
195
+ "content-class": bodyPad.value
196
+ }, {
197
+ default: withCtx(() => [
198
+ renderSlot(_ctx.$slots, "default")
199
+ ]),
200
+ _: 3
201
+ }, 8, ["class", "content-class"])),
202
+ _ctx.$slots.footer ? (openBlock(), createElementBlock("footer", _hoisted_4, [
203
+ renderSlot(_ctx.$slots, "footer")
204
+ ])) : createCommentVNode("", true)
205
+ ]),
206
+ _: 3
207
+ }, 16, ["class", "style"]),
208
+ _ctx.$slots.asideEnd ? (openBlock(), createElementBlock("aside", _hoisted_5, [
209
+ renderSlot(_ctx.$slots, "asideEnd")
210
+ ])) : createCommentVNode("", true)
211
+ ], 4)
212
+ ], 4)
213
+ ]),
214
+ _: 3
215
+ })
216
+ ]),
217
+ _: 3
218
+ }, 8, ["open"]);
219
+ };
220
+ }
221
+ });
222
+ export {
223
+ _sfc_main as _
224
+ };
@@ -0,0 +1,166 @@
1
+ import { defineComponent, ref, computed, openBlock, createBlock, createSlots, withCtx, renderSlot, createElementVNode, normalizeClass, createVNode, createCommentVNode, createTextVNode, toDisplayString } from "vue";
2
+ import { _ as _sfc_main$2 } from "./LpButton-5VXXCjwF.js";
3
+ import { _ as _sfc_main$3 } from "./LpIcon-CCnX5_2j.js";
4
+ import { _ as _sfc_main$1 } from "./LpModal-7LvuAA7U.js";
5
+ import { _ as _sfc_main$4 } from "./LpSidebar-DcYavim2.js";
6
+ const _hoisted_1 = { class: "flex items-center gap-2" };
7
+ const _sfc_main = /* @__PURE__ */ defineComponent({
8
+ __name: "LpModalNav",
9
+ props: {
10
+ open: { type: Boolean },
11
+ modelValue: {},
12
+ items: {},
13
+ sections: {},
14
+ title: {},
15
+ description: {},
16
+ size: { default: "2xl" },
17
+ width: {},
18
+ navWidth: { default: "13rem" },
19
+ searchable: { type: Boolean },
20
+ searchPlaceholder: {},
21
+ loading: { type: Boolean },
22
+ mobileBreakpoint: { default: "lg" },
23
+ navLabel: { default: "Sections" },
24
+ navSide: { default: "start" },
25
+ fillBody: { type: Boolean }
26
+ },
27
+ emits: ["update:open", "update:modelValue"],
28
+ setup(__props, { emit: __emit }) {
29
+ const props = __props;
30
+ const emit = __emit;
31
+ const navOpen = ref(false);
32
+ const active = computed({
33
+ get: () => props.modelValue ?? "",
34
+ set: (v) => emit("update:modelValue", v)
35
+ });
36
+ const hasNav = computed(() => Boolean(props.items?.length || props.sections?.length));
37
+ const RAIL_GAP = {
38
+ sm: "sm:mr-3",
39
+ md: "md:mr-3",
40
+ lg: "lg:mr-3",
41
+ xl: "xl:mr-3"
42
+ };
43
+ const RAIL_GAP_END = {
44
+ sm: "sm:ml-3",
45
+ md: "md:ml-3",
46
+ lg: "lg:ml-3",
47
+ xl: "xl:ml-3"
48
+ };
49
+ const TRIGGER_VISIBILITY = {
50
+ sm: "sm:hidden",
51
+ md: "md:hidden",
52
+ lg: "lg:hidden",
53
+ xl: "xl:hidden"
54
+ };
55
+ const railClass = computed(
56
+ () => (props.navSide === "end" ? RAIL_GAP_END : RAIL_GAP)[props.mobileBreakpoint]
57
+ );
58
+ const triggerClass = computed(() => TRIGGER_VISIBILITY[props.mobileBreakpoint]);
59
+ const railStyle = { maxHeight: "min(90vh, calc(100dvh - 2rem))" };
60
+ return (_ctx, _cache) => {
61
+ return openBlock(), createBlock(_sfc_main$1, {
62
+ open: __props.open,
63
+ title: __props.title,
64
+ description: __props.description,
65
+ size: __props.size,
66
+ width: __props.width,
67
+ "fill-body": __props.fillBody,
68
+ "onUpdate:open": _cache[5] || (_cache[5] = ($event) => emit("update:open", $event))
69
+ }, createSlots({
70
+ title: withCtx(() => [
71
+ createElementVNode("div", _hoisted_1, [
72
+ hasNav.value ? (openBlock(), createBlock(_sfc_main$2, {
73
+ key: 0,
74
+ variant: "ghost",
75
+ size: "sm",
76
+ square: "",
77
+ "aria-label": __props.navLabel,
78
+ class: normalizeClass(triggerClass.value),
79
+ onClick: _cache[4] || (_cache[4] = ($event) => navOpen.value = true)
80
+ }, {
81
+ default: withCtx(() => [
82
+ createVNode(_sfc_main$3, {
83
+ name: "lucide:menu",
84
+ size: 18
85
+ })
86
+ ]),
87
+ _: 1
88
+ }, 8, ["aria-label", "class"])) : createCommentVNode("", true),
89
+ renderSlot(_ctx.$slots, "title", {}, () => [
90
+ createTextVNode(toDisplayString(__props.title), 1)
91
+ ])
92
+ ])
93
+ ]),
94
+ default: withCtx(() => [
95
+ renderSlot(_ctx.$slots, "default")
96
+ ]),
97
+ _: 2
98
+ }, [
99
+ hasNav.value && __props.navSide === "start" ? {
100
+ name: "aside",
101
+ fn: withCtx(() => [
102
+ createElementVNode("div", {
103
+ class: normalizeClass(railClass.value)
104
+ }, [
105
+ createVNode(_sfc_main$4, {
106
+ modelValue: active.value,
107
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => active.value = $event),
108
+ open: navOpen.value,
109
+ "onUpdate:open": _cache[1] || (_cache[1] = ($event) => navOpen.value = $event),
110
+ variant: "panel",
111
+ items: __props.items,
112
+ sections: __props.sections,
113
+ width: __props.navWidth,
114
+ searchable: __props.searchable,
115
+ "search-placeholder": __props.searchPlaceholder,
116
+ loading: __props.loading,
117
+ responsive: "",
118
+ "mobile-breakpoint": __props.mobileBreakpoint,
119
+ style: railStyle,
120
+ class: "rounded-card border border-line bg-surface-raised p-2 shadow-panel"
121
+ }, null, 8, ["modelValue", "open", "items", "sections", "width", "searchable", "search-placeholder", "loading", "mobile-breakpoint"])
122
+ ], 2)
123
+ ]),
124
+ key: "0"
125
+ } : void 0,
126
+ hasNav.value && __props.navSide === "end" ? {
127
+ name: "asideEnd",
128
+ fn: withCtx(() => [
129
+ createElementVNode("div", {
130
+ class: normalizeClass(railClass.value)
131
+ }, [
132
+ createVNode(_sfc_main$4, {
133
+ modelValue: active.value,
134
+ "onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => active.value = $event),
135
+ open: navOpen.value,
136
+ "onUpdate:open": _cache[3] || (_cache[3] = ($event) => navOpen.value = $event),
137
+ variant: "panel",
138
+ items: __props.items,
139
+ sections: __props.sections,
140
+ width: __props.navWidth,
141
+ searchable: __props.searchable,
142
+ "search-placeholder": __props.searchPlaceholder,
143
+ loading: __props.loading,
144
+ responsive: "",
145
+ "mobile-breakpoint": __props.mobileBreakpoint,
146
+ style: railStyle,
147
+ class: "rounded-card border border-line bg-surface-raised p-2 shadow-panel"
148
+ }, null, 8, ["modelValue", "open", "items", "sections", "width", "searchable", "search-placeholder", "loading", "mobile-breakpoint"])
149
+ ], 2)
150
+ ]),
151
+ key: "1"
152
+ } : void 0,
153
+ _ctx.$slots.footer ? {
154
+ name: "footer",
155
+ fn: withCtx(() => [
156
+ renderSlot(_ctx.$slots, "footer")
157
+ ]),
158
+ key: "2"
159
+ } : void 0
160
+ ]), 1032, ["open", "title", "description", "size", "width", "fill-body"]);
161
+ };
162
+ }
163
+ });
164
+ export {
165
+ _sfc_main as _
166
+ };
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpAddressInput-FZXfljWV.js";
1
+ import { _ as _sfc_main } from "../chunks/LpAddressInput-D4t8WFD-.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpConfirmDialog-CQBVaLpo.js";
1
+ import { _ as _sfc_main } from "../chunks/LpConfirmDialog-CxbizvSB.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
@@ -26,15 +26,19 @@ type __VLS_Props = {
26
26
  */
27
27
  autoFocus?: "auto" | "none" | string;
28
28
  };
29
- declare var __VLS_37: {}, __VLS_56: {}, __VLS_64: {}, __VLS_66: {};
29
+ declare var __VLS_21: {}, __VLS_41: {}, __VLS_60: {}, __VLS_68: {}, __VLS_70: {}, __VLS_72: {};
30
30
  type __VLS_Slots = {} & {
31
- title?: (props: typeof __VLS_37) => any;
31
+ aside?: (props: typeof __VLS_21) => any;
32
32
  } & {
33
- default?: (props: typeof __VLS_56) => any;
33
+ title?: (props: typeof __VLS_41) => any;
34
34
  } & {
35
- default?: (props: typeof __VLS_64) => any;
35
+ default?: (props: typeof __VLS_60) => any;
36
36
  } & {
37
- footer?: (props: typeof __VLS_66) => any;
37
+ default?: (props: typeof __VLS_68) => any;
38
+ } & {
39
+ footer?: (props: typeof __VLS_70) => any;
40
+ } & {
41
+ asideEnd?: (props: typeof __VLS_72) => any;
38
42
  };
39
43
  declare const __VLS_base: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {} & {
40
44
  "update:open": (value: boolean) => any;
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpModal-DAqXzsm-.js";
1
+ import { _ as _sfc_main } from "../chunks/LpModal-7LvuAA7U.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
@@ -10,10 +10,6 @@ type __VLS_Props = {
10
10
  sections?: SidebarSection[];
11
11
  title?: string;
12
12
  description?: string;
13
- /**
14
- * Defaults wider than `LpModal`'s `md`: the rail comes out of the content's
15
- * width, and at `md` what is left is too narrow to be worth splitting.
16
- */
17
13
  size?: "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "full";
18
14
  width?: string;
19
15
  /** Width of the nav rail (any CSS length). */
@@ -27,29 +23,22 @@ type __VLS_Props = {
27
23
  mobileBreakpoint?: Breakpoint;
28
24
  /** Label for the button that opens that drawer. */
29
25
  navLabel?: string;
26
+ /** Stand the rail to the right of the dialog instead of the left. */
27
+ navSide?: "start" | "end";
30
28
  /**
31
- * Let the body fill the height instead of scrolling itself. On by default,
32
- * unlike a plain modal: the rail is only a full-height column if the two
33
- * of them agree on a height to fill.
29
+ * Let the body fill the height instead of scrolling itself. Off by default,
30
+ * as in a plain modal: it existed here to make the rail a full-height
31
+ * column inside the body, and the rail is no longer in the body.
34
32
  */
35
33
  fillBody?: boolean;
36
34
  };
37
- declare var __VLS_24: {}, __VLS_33: {}, __VLS_36: {
38
- item: SidebarItem;
39
- active: boolean;
40
- }, __VLS_39: {}, __VLS_41: {}, __VLS_44: {};
35
+ declare var __VLS_36: {}, __VLS_38: {}, __VLS_41: {};
41
36
  type __VLS_Slots = {} & {
42
- title?: (props: typeof __VLS_24) => any;
43
- } & {
44
- navHeader?: (props: typeof __VLS_33) => any;
45
- } & {
46
- navItem?: (props: typeof __VLS_36) => any;
47
- } & {
48
- navFooter?: (props: typeof __VLS_39) => any;
37
+ title?: (props: typeof __VLS_36) => any;
49
38
  } & {
50
- default?: (props: typeof __VLS_41) => any;
39
+ default?: (props: typeof __VLS_38) => any;
51
40
  } & {
52
- footer?: (props: typeof __VLS_44) => any;
41
+ footer?: (props: typeof __VLS_41) => any;
53
42
  };
54
43
  declare const __VLS_base: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {} & {
55
44
  "update:modelValue": (value: string) => any;
@@ -59,10 +48,10 @@ declare const __VLS_base: import('vue').DefineComponent<__VLS_Props, {}, {}, {},
59
48
  "onUpdate:open"?: ((value: boolean) => any) | undefined;
60
49
  }>, {
61
50
  size: "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "full";
62
- fillBody: boolean;
63
51
  mobileBreakpoint: Breakpoint;
64
52
  navWidth: string;
65
53
  navLabel: string;
54
+ navSide: "start" | "end";
66
55
  }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
67
56
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
68
57
  declare const _default: typeof __VLS_export;
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpModalNav-B7W9VAne.js";
1
+ import { _ as _sfc_main } from "../chunks/LpModalNav-DBMGlq_x.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
@@ -1,5 +1,5 @@
1
1
  import { defineComponent, openBlock, createBlock, withCtx, createElementVNode, createElementBlock, Fragment, renderList, toDisplayString, unref } from "vue";
2
- import { _ as _sfc_main$1 } from "../chunks/LpModal-DAqXzsm-.js";
2
+ import { _ as _sfc_main$1 } from "../chunks/LpModal-7LvuAA7U.js";
3
3
  import { appleModifierLabel } from "../composables/useHotkeys.js";
4
4
  import { _ as _export_sfc } from "../chunks/_plugin-vue_export-helper-1tPrXgE0.js";
5
5
  import '../assets/LpShortcutsDialog-B7H-RZLQ.css';const _hoisted_1 = { class: "shortcut-groups" };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { _ } from "./chunks/LayoutCanvas-Dd20boUG.js";
2
- import { _ as _2 } from "./chunks/LpAddressInput-FZXfljWV.js";
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-CQBVaLpo.js";
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,8 +40,8 @@ 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-DAqXzsm-.js";
44
- import { _ as _35 } from "./chunks/LpModalNav-B7W9VAne.js";
43
+ import { _ as _34 } from "./chunks/LpModal-7LvuAA7U.js";
44
+ import { _ as _35 } from "./chunks/LpModalNav-DBMGlq_x.js";
45
45
  import { _ as _36 } from "./chunks/LpNotificationBell-lHePf89W.js";
46
46
  import { _ as _37 } from "./chunks/LpNumberField-CWuX08iX.js";
47
47
  import { _ as _38 } from "./chunks/LpNumberFlow-amWYaeuA.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leavepulse/ui",
3
- "version": "0.35.0",
3
+ "version": "0.36.0",
4
4
  "type": "module",
5
5
  "description": "LeavePulse token-driven Vue component kit.",
6
6
  "repository": {
@@ -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 } : undefined"
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>
@@ -1,24 +1,17 @@
1
1
  <script setup lang="ts">
2
2
  /*
3
- * A dialog whose sections stay visible while you read one — settings, a profile
4
- * with tabs' worth of detail, a wizard that lets you skip a step. The shape
5
- * these all reach for is a nav column that persists next to the body, and
6
- * without it they fall back to a row of tabs that runs out of width, or to a
7
- * back-and-forth between a list screen and a detail screen.
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.
8
7
  *
9
- * The nav lives INSIDE the dialog. Standing it outside, as a sibling floating
10
- * beside the panel, is the arrangement this component deliberately does not
11
- * use: it then sits outside the focus trap, `interact-outside` reads a click on
12
- * it as a click on the scrim and closes the dialog under the pointer, and its
13
- * height has to be reconciled with a dialog height that changes as content
14
- * arrives. Inside, all three come out right for free, and `LpModal` keeps the
15
- * centring every other modal in the kit relies on.
16
- *
17
- * Width is the one cost: the rail comes out of the body's width, which is why
18
- * the default `size` is wider than a plain modal's. Below `mobileBreakpoint`
19
- * there is no width to spare at all, so the rail becomes a drawer — and since
20
- * LpSidebar leaves opening it to the app, the header grows the button for it
21
- * rather than leaving the sections unreachable on a phone.
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.
22
15
  */
23
16
  import { computed, ref } from "vue"
24
17
  import LpButton from "./LpButton.vue"
@@ -41,10 +34,6 @@ const props = withDefaults(
41
34
  sections?: SidebarSection[]
42
35
  title?: string
43
36
  description?: string
44
- /**
45
- * Defaults wider than `LpModal`'s `md`: the rail comes out of the content's
46
- * width, and at `md` what is left is too narrow to be worth splitting.
47
- */
48
37
  size?: "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "full"
49
38
  width?: string
50
39
  /** Width of the nav rail (any CSS length). */
@@ -58,10 +47,12 @@ const props = withDefaults(
58
47
  mobileBreakpoint?: Breakpoint
59
48
  /** Label for the button that opens that drawer. */
60
49
  navLabel?: string
50
+ /** Stand the rail to the right of the dialog instead of the left. */
51
+ navSide?: "start" | "end"
61
52
  /**
62
- * Let the body fill the height instead of scrolling itself. On by default,
63
- * unlike a plain modal: the rail is only a full-height column if the two
64
- * of them agree on a height to fill.
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.
65
56
  */
66
57
  fillBody?: boolean
67
58
  }>(),
@@ -70,7 +61,7 @@ const props = withDefaults(
70
61
  navWidth: "13rem",
71
62
  mobileBreakpoint: "lg",
72
63
  navLabel: "Sections",
73
- fillBody: true,
64
+ navSide: "start",
74
65
  },
75
66
  )
76
67
 
@@ -86,23 +77,50 @@ const active = computed({
86
77
  set: (v: string) => emit("update:modelValue", v),
87
78
  })
88
79
 
89
- const hasNav = computed(
90
- () => Boolean(props.items?.length || props.sections?.length),
91
- )
80
+ const hasNav = computed(() => Boolean(props.items?.length || props.sections?.length))
92
81
 
93
82
  /*
94
- * Spelled out rather than built from the prop: a class assembled at runtime is
95
- * not in anyone's Tailwind scan and would never be generated. Mirrors the
96
- * rail's own visibility in LpSidebar — the button appears exactly where the
97
- * rail disappears.
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.
98
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
+ }
99
106
  const TRIGGER_VISIBILITY: Record<Breakpoint, string> = {
100
107
  sm: "sm:hidden",
101
108
  md: "md:hidden",
102
109
  lg: "lg:hidden",
103
110
  xl: "xl:hidden",
104
111
  }
112
+ const railClass = computed(() =>
113
+ (props.navSide === "end" ? RAIL_GAP_END : RAIL_GAP)[props.mobileBreakpoint],
114
+ )
105
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))" }
106
124
  </script>
107
125
 
108
126
  <template>
@@ -115,6 +133,49 @@ const triggerClass = computed(() => TRIGGER_VISIBILITY[props.mobileBreakpoint])
115
133
  :fill-body="fillBody"
116
134
  @update:open="emit('update:open', $event)"
117
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
+
118
179
  <template #title>
119
180
  <div class="flex items-center gap-2">
120
181
  <LpButton
@@ -132,43 +193,7 @@ const triggerClass = computed(() => TRIGGER_VISIBILITY[props.mobileBreakpoint])
132
193
  </div>
133
194
  </template>
134
195
 
135
- <div class="flex min-h-0 flex-1 gap-4">
136
- <!-- `shrink-0`: a flex item's default is to give up width to a greedy
137
- sibling, so a wide table in the body would squeeze the rail down to
138
- its longest unbreakable word. -->
139
- <LpSidebar
140
- v-if="hasNav"
141
- v-model="active"
142
- v-model:open="navOpen"
143
- variant="panel"
144
- :items="items"
145
- :sections="sections"
146
- :width="navWidth"
147
- :searchable="searchable"
148
- :search-placeholder="searchPlaceholder"
149
- :loading="loading"
150
- responsive
151
- :mobile-breakpoint="mobileBreakpoint"
152
- class="shrink-0"
153
- >
154
- <template v-if="$slots.navHeader" #header>
155
- <slot name="navHeader" />
156
- </template>
157
- <template v-if="$slots.navItem" #item="slotProps">
158
- <slot name="navItem" v-bind="slotProps" />
159
- </template>
160
- <template v-if="$slots.navFooter" #footer>
161
- <slot name="navFooter" />
162
- </template>
163
- </LpSidebar>
164
-
165
- <!-- `min-w-0` is the same guard in the other direction: without it a wide
166
- child sets this column's minimum to its own width, and the dialog
167
- grows past its size instead of the content scrolling. -->
168
- <div class="flex min-h-0 min-w-0 flex-1 flex-col">
169
- <slot />
170
- </div>
171
- </div>
196
+ <slot />
172
197
 
173
198
  <template v-if="$slots.footer" #footer>
174
199
  <slot name="footer" />
@@ -1,192 +0,0 @@
1
- import { defineComponent, computed, useSlots, watch, onBeforeUnmount, ref, openBlock, createBlock, unref, withCtx, createVNode, normalizeStyle, createElementVNode, mergeProps, createElementBlock, renderSlot, createTextVNode, toDisplayString, createCommentVNode, normalizeClass } from "vue";
2
- import { DialogRoot, DialogPortal, DialogOverlay, DialogContent, DialogTitle, DialogDescription, DialogClose } from "reka-ui";
3
- import { useShift } from "../composables/useShift.js";
4
- import { useModalLayer } from "../components/modalDepth.js";
5
- import { CLOSE_ICON } from "../components/dropdown.js";
6
- import { _ as _sfc_main$1 } from "./LpIcon-CCnX5_2j.js";
7
- import { _ as _sfc_main$2 } from "./LpScrollArea-D8xyKXcF.js";
8
- const _hoisted_1 = {
9
- key: 0,
10
- class: "flex shrink-0 items-start justify-between gap-4 p-5 pb-3"
11
- };
12
- const _hoisted_2 = { class: "flex flex-col gap-1" };
13
- const _hoisted_3 = {
14
- key: 3,
15
- class: "flex shrink-0 justify-end gap-2 p-5 pt-4"
16
- };
17
- const _sfc_main = /* @__PURE__ */ defineComponent({
18
- __name: "LpModal",
19
- props: {
20
- open: { type: Boolean },
21
- title: {},
22
- description: {},
23
- size: { default: "md" },
24
- width: {},
25
- fillBody: { type: Boolean },
26
- autoFocus: { default: "auto" }
27
- },
28
- emits: ["update:open"],
29
- setup(__props) {
30
- const { claim, release, layer } = useModalLayer();
31
- const props = __props;
32
- const describedByAttrs = computed(
33
- () => props.description ? {} : { "aria-describedby": void 0 }
34
- );
35
- const widthClass = computed(() => {
36
- if (props.width) return "";
37
- return {
38
- sm: "w-[min(92vw,24rem)]",
39
- md: "w-[min(92vw,28rem)]",
40
- lg: "w-[min(92vw,34rem)]",
41
- xl: "w-[min(92vw,42rem)]",
42
- "2xl": "w-[min(94vw,56rem)]",
43
- "3xl": "w-[min(95vw,72rem)]",
44
- full: "w-[96vw]"
45
- }[props.size];
46
- });
47
- const slots = useSlots();
48
- watch(() => props.open, (open) => open ? claim() : release(), { immediate: true });
49
- onBeforeUnmount(release);
50
- const bodyPad = computed(
51
- () => [
52
- "px-5",
53
- props.title || slots.title ? "pt-1" : "pt-5",
54
- slots.footer ? "" : "pb-5"
55
- ].filter(Boolean).join(" ")
56
- );
57
- const panelRef = ref(null);
58
- const AUTO_FOCUS_ORDER = [
59
- "input:not([type=hidden]):not([disabled]):not([readonly])",
60
- "textarea:not([disabled]):not([readonly])",
61
- "select:not([disabled])",
62
- "[data-lp-autofocus]",
63
- "button:not([disabled]):not([data-lp-dialog-close])",
64
- "[href]",
65
- "[tabindex]:not([tabindex='-1'])"
66
- ];
67
- function onOpenAutoFocus(event) {
68
- if (props.autoFocus === "none") return;
69
- const panel = panelRef.value?.$el;
70
- if (!(panel instanceof HTMLElement)) return;
71
- const selectors = props.autoFocus === "auto" ? AUTO_FOCUS_ORDER : [props.autoFocus];
72
- for (const selector of selectors) {
73
- const target = panel.querySelector(selector);
74
- if (!target || target.hasAttribute("disabled")) continue;
75
- event.preventDefault();
76
- if (!target.closest("[data-lp-ring-owner]")) {
77
- target.setAttribute("data-lp-focus-ring", "");
78
- target.addEventListener(
79
- "blur",
80
- () => target.removeAttribute("data-lp-focus-ring"),
81
- { once: true }
82
- );
83
- }
84
- target.focus();
85
- return;
86
- }
87
- }
88
- const {
89
- el: sizedEl,
90
- tweening,
91
- resizing
92
- } = useShift({ axis: "height", enabled: () => props.open });
93
- watch(
94
- () => props.open ? panelRef.value : null,
95
- (panel) => {
96
- const el = panel?.$el;
97
- sizedEl.value = el instanceof HTMLElement ? el : null;
98
- },
99
- { flush: "post" }
100
- );
101
- return (_ctx, _cache) => {
102
- return openBlock(), createBlock(unref(DialogRoot), {
103
- open: __props.open,
104
- "onUpdate:open": _cache[0] || (_cache[0] = (v) => _ctx.$emit("update:open", v))
105
- }, {
106
- default: withCtx(() => [
107
- createVNode(unref(DialogPortal), null, {
108
- default: withCtx(() => [
109
- createVNode(unref(DialogOverlay), {
110
- class: "lp-scrim fixed inset-0 data-[state=open]:animate-[fade-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[fade-out_120ms_ease]",
111
- style: normalizeStyle({ zIndex: unref(layer).scrim })
112
- }, null, 8, ["style"]),
113
- createElementVNode("div", {
114
- class: "fixed inset-0 flex items-center justify-center pointer-events-none",
115
- style: normalizeStyle({ zIndex: unref(layer).panel })
116
- }, [
117
- createVNode(unref(DialogContent), mergeProps({
118
- ref_key: "panelRef",
119
- ref: panelRef,
120
- 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)]", [widthClass.value, unref(tweening) ? "transition-[height] duration-fast ease-[var(--ease-emphasized)] motion-reduce:transition-none" : ""]],
121
- style: __props.width ? { width: __props.width } : void 0
122
- }, describedByAttrs.value, { onOpenAutoFocus }), {
123
- default: withCtx(() => [
124
- __props.title || _ctx.$slots.title ? (openBlock(), createElementBlock("header", _hoisted_1, [
125
- createElementVNode("div", _hoisted_2, [
126
- createVNode(unref(DialogTitle), { class: "text-base font-semibold text-ink" }, {
127
- default: withCtx(() => [
128
- renderSlot(_ctx.$slots, "title", {}, () => [
129
- createTextVNode(toDisplayString(__props.title), 1)
130
- ])
131
- ]),
132
- _: 3
133
- }),
134
- __props.description ? (openBlock(), createBlock(unref(DialogDescription), {
135
- key: 0,
136
- class: "text-sm text-muted"
137
- }, {
138
- default: withCtx(() => [
139
- createTextVNode(toDisplayString(__props.description), 1)
140
- ]),
141
- _: 1
142
- })) : createCommentVNode("", true)
143
- ]),
144
- createVNode(unref(DialogClose), {
145
- "data-lp-dialog-close": "",
146
- class: "group flex shrink-0 items-center rounded-md p-1 text-muted outline-none transition-colors duration-[var(--duration-fast)] hover:text-ink focus-visible:ring-2 focus-visible:ring-ring",
147
- "aria-label": "Close"
148
- }, {
149
- default: withCtx(() => [
150
- createVNode(_sfc_main$1, {
151
- name: "lucide:x",
152
- size: 18,
153
- class: normalizeClass(unref(CLOSE_ICON))
154
- }, null, 8, ["class"])
155
- ]),
156
- _: 1
157
- })
158
- ])) : createCommentVNode("", true),
159
- __props.fillBody ? (openBlock(), createElementBlock("div", {
160
- key: 1,
161
- class: normalizeClass(["flex min-h-0 flex-1 flex-col overflow-hidden text-sm text-ink/90", bodyPad.value])
162
- }, [
163
- renderSlot(_ctx.$slots, "default")
164
- ], 2)) : (openBlock(), createBlock(_sfc_main$2, {
165
- key: 2,
166
- class: normalizeClass(["min-h-0 flex-1 text-sm text-ink/90", unref(resizing) ? "[&_[data-scrollbarimpl]]:invisible" : ""]),
167
- "content-class": bodyPad.value
168
- }, {
169
- default: withCtx(() => [
170
- renderSlot(_ctx.$slots, "default")
171
- ]),
172
- _: 3
173
- }, 8, ["class", "content-class"])),
174
- _ctx.$slots.footer ? (openBlock(), createElementBlock("footer", _hoisted_3, [
175
- renderSlot(_ctx.$slots, "footer")
176
- ])) : createCommentVNode("", true)
177
- ]),
178
- _: 3
179
- }, 16, ["class", "style"])
180
- ], 4)
181
- ]),
182
- _: 3
183
- })
184
- ]),
185
- _: 3
186
- }, 8, ["open"]);
187
- };
188
- }
189
- });
190
- export {
191
- _sfc_main as _
192
- };
@@ -1,142 +0,0 @@
1
- import { defineComponent, ref, computed, openBlock, createBlock, createSlots, withCtx, createElementVNode, renderSlot, normalizeProps, guardReactiveProps, createCommentVNode, normalizeClass, createVNode, createTextVNode, toDisplayString } from "vue";
2
- import { _ as _sfc_main$3 } from "./LpButton-5VXXCjwF.js";
3
- import { _ as _sfc_main$4 } from "./LpIcon-CCnX5_2j.js";
4
- import { _ as _sfc_main$1 } from "./LpModal-DAqXzsm-.js";
5
- import { _ as _sfc_main$2 } from "./LpSidebar-DcYavim2.js";
6
- const _hoisted_1 = { class: "flex items-center gap-2" };
7
- const _hoisted_2 = { class: "flex min-h-0 flex-1 gap-4" };
8
- const _hoisted_3 = { class: "flex min-h-0 min-w-0 flex-1 flex-col" };
9
- const _sfc_main = /* @__PURE__ */ defineComponent({
10
- __name: "LpModalNav",
11
- props: {
12
- open: { type: Boolean },
13
- modelValue: {},
14
- items: {},
15
- sections: {},
16
- title: {},
17
- description: {},
18
- size: { default: "2xl" },
19
- width: {},
20
- navWidth: { default: "13rem" },
21
- searchable: { type: Boolean },
22
- searchPlaceholder: {},
23
- loading: { type: Boolean },
24
- mobileBreakpoint: { default: "lg" },
25
- navLabel: { default: "Sections" },
26
- fillBody: { type: Boolean, default: true }
27
- },
28
- emits: ["update:open", "update:modelValue"],
29
- setup(__props, { emit: __emit }) {
30
- const props = __props;
31
- const emit = __emit;
32
- const navOpen = ref(false);
33
- const active = computed({
34
- get: () => props.modelValue ?? "",
35
- set: (v) => emit("update:modelValue", v)
36
- });
37
- const hasNav = computed(
38
- () => Boolean(props.items?.length || props.sections?.length)
39
- );
40
- const TRIGGER_VISIBILITY = {
41
- sm: "sm:hidden",
42
- md: "md:hidden",
43
- lg: "lg:hidden",
44
- xl: "xl:hidden"
45
- };
46
- const triggerClass = computed(() => TRIGGER_VISIBILITY[props.mobileBreakpoint]);
47
- return (_ctx, _cache) => {
48
- return openBlock(), createBlock(_sfc_main$1, {
49
- open: __props.open,
50
- title: __props.title,
51
- description: __props.description,
52
- size: __props.size,
53
- width: __props.width,
54
- "fill-body": __props.fillBody,
55
- "onUpdate:open": _cache[3] || (_cache[3] = ($event) => emit("update:open", $event))
56
- }, createSlots({
57
- title: withCtx(() => [
58
- createElementVNode("div", _hoisted_1, [
59
- hasNav.value ? (openBlock(), createBlock(_sfc_main$3, {
60
- key: 0,
61
- variant: "ghost",
62
- size: "sm",
63
- square: "",
64
- "aria-label": __props.navLabel,
65
- class: normalizeClass(triggerClass.value),
66
- onClick: _cache[0] || (_cache[0] = ($event) => navOpen.value = true)
67
- }, {
68
- default: withCtx(() => [
69
- createVNode(_sfc_main$4, {
70
- name: "lucide:menu",
71
- size: 18
72
- })
73
- ]),
74
- _: 1
75
- }, 8, ["aria-label", "class"])) : createCommentVNode("", true),
76
- renderSlot(_ctx.$slots, "title", {}, () => [
77
- createTextVNode(toDisplayString(__props.title), 1)
78
- ])
79
- ])
80
- ]),
81
- default: withCtx(() => [
82
- createElementVNode("div", _hoisted_2, [
83
- hasNav.value ? (openBlock(), createBlock(_sfc_main$2, {
84
- key: 0,
85
- modelValue: active.value,
86
- "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => active.value = $event),
87
- open: navOpen.value,
88
- "onUpdate:open": _cache[2] || (_cache[2] = ($event) => navOpen.value = $event),
89
- variant: "panel",
90
- items: __props.items,
91
- sections: __props.sections,
92
- width: __props.navWidth,
93
- searchable: __props.searchable,
94
- "search-placeholder": __props.searchPlaceholder,
95
- loading: __props.loading,
96
- responsive: "",
97
- "mobile-breakpoint": __props.mobileBreakpoint,
98
- class: "shrink-0"
99
- }, createSlots({ _: 2 }, [
100
- _ctx.$slots.navHeader ? {
101
- name: "header",
102
- fn: withCtx(() => [
103
- renderSlot(_ctx.$slots, "navHeader")
104
- ]),
105
- key: "0"
106
- } : void 0,
107
- _ctx.$slots.navItem ? {
108
- name: "item",
109
- fn: withCtx((slotProps) => [
110
- renderSlot(_ctx.$slots, "navItem", normalizeProps(guardReactiveProps(slotProps)))
111
- ]),
112
- key: "1"
113
- } : void 0,
114
- _ctx.$slots.navFooter ? {
115
- name: "footer",
116
- fn: withCtx(() => [
117
- renderSlot(_ctx.$slots, "navFooter")
118
- ]),
119
- key: "2"
120
- } : void 0
121
- ]), 1032, ["modelValue", "open", "items", "sections", "width", "searchable", "search-placeholder", "loading", "mobile-breakpoint"])) : createCommentVNode("", true),
122
- createElementVNode("div", _hoisted_3, [
123
- renderSlot(_ctx.$slots, "default")
124
- ])
125
- ])
126
- ]),
127
- _: 2
128
- }, [
129
- _ctx.$slots.footer ? {
130
- name: "footer",
131
- fn: withCtx(() => [
132
- renderSlot(_ctx.$slots, "footer")
133
- ]),
134
- key: "0"
135
- } : void 0
136
- ]), 1032, ["open", "title", "description", "size", "width", "fill-body"]);
137
- };
138
- }
139
- });
140
- export {
141
- _sfc_main as _
142
- };