@leavepulse/ui 0.19.0 → 0.19.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/{LpAddressInput-CWlfDyKz.js → LpAddressInput-CCtskgfZ.js} +1 -1
- package/dist/chunks/{LpConfirmDialog-JNW4rpx3.js → LpConfirmDialog-Z9hsfm2D.js} +1 -1
- package/dist/chunks/{LpModal-BJTiTSDJ.js → LpModal-g2dGpF7l.js} +74 -4
- package/dist/components/LpAddressInput.vue.js +1 -1
- package/dist/components/LpConfirmDialog.vue.js +1 -1
- package/dist/components/LpModal.vue.d.ts +5 -5
- package/dist/components/LpModal.vue.js +1 -1
- package/dist/components/fileTree.js +2 -1
- package/dist/index.js +3 -3
- package/package.json +1 -1
- package/src/components/LpModal.vue +133 -3
- package/src/components/fileTree.ts +8 -1
|
@@ -6,7 +6,7 @@ import { _ as _sfc_main$3 } from "./LpButton-5VXXCjwF.js";
|
|
|
6
6
|
import { _ as _sfc_main$1 } from "./LpFormField-Bn-ZwC6z.js";
|
|
7
7
|
import { _ as _sfc_main$4 } from "./LpIcon-CCnX5_2j.js";
|
|
8
8
|
import { _ as _sfc_main$5 } from "./LpInput-BauZRyzm.js";
|
|
9
|
-
import { _ as _sfc_main$7 } from "./LpModal-
|
|
9
|
+
import { _ as _sfc_main$7 } from "./LpModal-g2dGpF7l.js";
|
|
10
10
|
import { _ as _sfc_main$6 } from "./LpSelect-D5mU4cZO.js";
|
|
11
11
|
const _hoisted_1 = { class: "flex flex-col gap-3" };
|
|
12
12
|
const _hoisted_2 = { class: "flex items-stretch gap-2" };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineComponent, openBlock, createBlock, withCtx, renderSlot, createVNode, createTextVNode, toDisplayString } from "vue";
|
|
2
2
|
import { _ as _sfc_main$2 } from "./LpButton-5VXXCjwF.js";
|
|
3
|
-
import { _ as _sfc_main$1 } from "./LpModal-
|
|
3
|
+
import { _ as _sfc_main$1 } from "./LpModal-g2dGpF7l.js";
|
|
4
4
|
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
5
5
|
__name: "LpConfirmDialog",
|
|
6
6
|
props: {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineComponent, computed, useSlots, openBlock, createBlock, unref, withCtx, createVNode, createElementVNode, mergeProps, createElementBlock, renderSlot, createTextVNode, toDisplayString, createCommentVNode, normalizeClass } from "vue";
|
|
1
|
+
import { defineComponent, computed, useSlots, ref, watch, onBeforeUnmount, openBlock, createBlock, unref, withCtx, createVNode, createElementVNode, mergeProps, createElementBlock, renderSlot, createTextVNode, toDisplayString, createCommentVNode, normalizeClass } from "vue";
|
|
2
2
|
import { DialogRoot, DialogPortal, DialogOverlay, DialogContent, DialogTitle, DialogDescription, DialogClose } from "reka-ui";
|
|
3
3
|
import { CLOSE_ICON } from "../components/dropdown.js";
|
|
4
4
|
import { _ as _sfc_main$1 } from "./LpIcon-CCnX5_2j.js";
|
|
@@ -13,6 +13,7 @@ const _hoisted_4 = {
|
|
|
13
13
|
key: 3,
|
|
14
14
|
class: "flex shrink-0 justify-end gap-2 p-5 pt-4"
|
|
15
15
|
};
|
|
16
|
+
const TWEEN_MS = 160;
|
|
16
17
|
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
17
18
|
__name: "LpModal",
|
|
18
19
|
props: {
|
|
@@ -49,6 +50,73 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
49
50
|
slots.footer ? "" : "pb-5"
|
|
50
51
|
].filter(Boolean).join(" ")
|
|
51
52
|
);
|
|
53
|
+
const panelRef = ref(null);
|
|
54
|
+
const tweening = ref(false);
|
|
55
|
+
const resizing = ref(false);
|
|
56
|
+
let ro;
|
|
57
|
+
let mo;
|
|
58
|
+
let raf = 0;
|
|
59
|
+
let settleTimer;
|
|
60
|
+
const retune = (el) => {
|
|
61
|
+
const from = el.offsetHeight;
|
|
62
|
+
el.style.height = "";
|
|
63
|
+
el.style.maxHeight = "none";
|
|
64
|
+
const to = el.offsetHeight;
|
|
65
|
+
el.style.maxHeight = "";
|
|
66
|
+
if (to === 0 || to === from) return;
|
|
67
|
+
el.style.height = `${from}px`;
|
|
68
|
+
void el.offsetHeight;
|
|
69
|
+
el.style.height = `${to}px`;
|
|
70
|
+
resizing.value = true;
|
|
71
|
+
clearTimeout(settleTimer);
|
|
72
|
+
settleTimer = setTimeout(() => {
|
|
73
|
+
resizing.value = false;
|
|
74
|
+
el.style.height = "";
|
|
75
|
+
}, TWEEN_MS);
|
|
76
|
+
};
|
|
77
|
+
watch(
|
|
78
|
+
() => props.open,
|
|
79
|
+
(isOpen) => {
|
|
80
|
+
ro?.disconnect();
|
|
81
|
+
mo?.disconnect();
|
|
82
|
+
ro = void 0;
|
|
83
|
+
mo = void 0;
|
|
84
|
+
cancelAnimationFrame(raf);
|
|
85
|
+
clearTimeout(settleTimer);
|
|
86
|
+
tweening.value = false;
|
|
87
|
+
resizing.value = false;
|
|
88
|
+
const el = panelRef.value?.$el;
|
|
89
|
+
if (!isOpen || !(el instanceof HTMLElement)) return;
|
|
90
|
+
el.style.height = `${el.offsetHeight}px`;
|
|
91
|
+
raf = requestAnimationFrame(() => {
|
|
92
|
+
tweening.value = true;
|
|
93
|
+
el.style.height = "";
|
|
94
|
+
});
|
|
95
|
+
const watchChildren = () => {
|
|
96
|
+
if (!ro) return;
|
|
97
|
+
ro.disconnect();
|
|
98
|
+
for (const child of Array.from(el.children)) ro.observe(child);
|
|
99
|
+
};
|
|
100
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
101
|
+
ro = new ResizeObserver(() => retune(el));
|
|
102
|
+
watchChildren();
|
|
103
|
+
}
|
|
104
|
+
if (typeof MutationObserver !== "undefined") {
|
|
105
|
+
mo = new MutationObserver(() => {
|
|
106
|
+
watchChildren();
|
|
107
|
+
retune(el);
|
|
108
|
+
});
|
|
109
|
+
mo.observe(el, { childList: true, subtree: true, characterData: true });
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
{ flush: "post" }
|
|
113
|
+
);
|
|
114
|
+
onBeforeUnmount(() => {
|
|
115
|
+
ro?.disconnect();
|
|
116
|
+
mo?.disconnect();
|
|
117
|
+
cancelAnimationFrame(raf);
|
|
118
|
+
clearTimeout(settleTimer);
|
|
119
|
+
});
|
|
52
120
|
return (_ctx, _cache) => {
|
|
53
121
|
return openBlock(), createBlock(unref(DialogRoot), {
|
|
54
122
|
open: __props.open,
|
|
@@ -60,7 +128,9 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
60
128
|
createVNode(unref(DialogOverlay), { class: "lp-scrim fixed inset-0 z-(--z-overlay) backdrop-blur-sm data-[state=open]:animate-[fade-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[fade-out_120ms_ease]" }),
|
|
61
129
|
createElementVNode("div", _hoisted_1, [
|
|
62
130
|
createVNode(unref(DialogContent), mergeProps({
|
|
63
|
-
|
|
131
|
+
ref_key: "panelRef",
|
|
132
|
+
ref: panelRef,
|
|
133
|
+
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, tweening.value ? "transition-[height] duration-fast ease-[var(--ease-emphasized)] motion-reduce:transition-none" : ""]],
|
|
64
134
|
style: __props.width ? { width: __props.width } : void 0
|
|
65
135
|
}, describedByAttrs.value), {
|
|
66
136
|
default: withCtx(() => [
|
|
@@ -105,14 +175,14 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
105
175
|
renderSlot(_ctx.$slots, "default")
|
|
106
176
|
], 2)) : (openBlock(), createBlock(_sfc_main$2, {
|
|
107
177
|
key: 2,
|
|
108
|
-
class: "min-h-0 flex-1 text-sm text-ink/90",
|
|
178
|
+
class: normalizeClass(["min-h-0 flex-1 text-sm text-ink/90", resizing.value ? "[&_[data-scrollbarimpl]]:invisible" : ""]),
|
|
109
179
|
"content-class": bodyPad.value
|
|
110
180
|
}, {
|
|
111
181
|
default: withCtx(() => [
|
|
112
182
|
renderSlot(_ctx.$slots, "default")
|
|
113
183
|
]),
|
|
114
184
|
_: 3
|
|
115
|
-
}, 8, ["content-class"])),
|
|
185
|
+
}, 8, ["class", "content-class"])),
|
|
116
186
|
_ctx.$slots.footer ? (openBlock(), createElementBlock("footer", _hoisted_4, [
|
|
117
187
|
renderSlot(_ctx.$slots, "footer")
|
|
118
188
|
])) : createCommentVNode("", true)
|
|
@@ -16,15 +16,15 @@ type __VLS_Props = {
|
|
|
16
16
|
*/
|
|
17
17
|
fillBody?: boolean;
|
|
18
18
|
};
|
|
19
|
-
declare var
|
|
19
|
+
declare var __VLS_35: {}, __VLS_54: {}, __VLS_62: {}, __VLS_64: {};
|
|
20
20
|
type __VLS_Slots = {} & {
|
|
21
|
-
title?: (props: typeof
|
|
21
|
+
title?: (props: typeof __VLS_35) => any;
|
|
22
22
|
} & {
|
|
23
|
-
default?: (props: typeof
|
|
23
|
+
default?: (props: typeof __VLS_54) => any;
|
|
24
24
|
} & {
|
|
25
|
-
default?: (props: typeof
|
|
25
|
+
default?: (props: typeof __VLS_62) => any;
|
|
26
26
|
} & {
|
|
27
|
-
footer?: (props: typeof
|
|
27
|
+
footer?: (props: typeof __VLS_64) => any;
|
|
28
28
|
};
|
|
29
29
|
declare const __VLS_base: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {} & {
|
|
30
30
|
"update:open": (value: boolean) => any;
|
|
@@ -62,10 +62,11 @@ function fileIcon(node, expanded = false) {
|
|
|
62
62
|
const ext = dot > 0 ? lower.slice(dot + 1) : "";
|
|
63
63
|
return EXT_ICONS[ext] ?? "lucide:file";
|
|
64
64
|
}
|
|
65
|
+
const NAME_COLLATOR = new Intl.Collator(void 0, { numeric: true, sensitivity: "base" });
|
|
65
66
|
function sortNodes(nodes) {
|
|
66
67
|
return [...nodes].sort((a, b) => {
|
|
67
68
|
if (a.kind !== b.kind) return a.kind === "dir" ? -1 : 1;
|
|
68
|
-
return a.name
|
|
69
|
+
return NAME_COLLATOR.compare(a.name, b.name);
|
|
69
70
|
});
|
|
70
71
|
}
|
|
71
72
|
function formatSize(bytes) {
|
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-CCtskgfZ.js";
|
|
3
3
|
import { _ as _3 } from "./chunks/LpMapPicker-DFKrd1Ne.js";
|
|
4
4
|
import { OSM_TILES, createNominatimProvider, emptyAddress, formatAddress, project, tileUrl, unproject } from "./components/address.js";
|
|
5
5
|
import { _ as _4 } from "./chunks/LpAlert-BAYHDSS1.js";
|
|
@@ -14,7 +14,7 @@ import { _ as _12 } from "./chunks/LpCard-BmrTvWOe.js";
|
|
|
14
14
|
import { _ as _13 } from "./chunks/LpCheckbox-BO0zuuPh.js";
|
|
15
15
|
import { _ as _14 } from "./chunks/LpCodeBlock-CSiOSPs8.js";
|
|
16
16
|
import { default as default2 } from "./components/LpCommandPalette.vue.js";
|
|
17
|
-
import { _ as _15 } from "./chunks/LpConfirmDialog-
|
|
17
|
+
import { _ as _15 } from "./chunks/LpConfirmDialog-Z9hsfm2D.js";
|
|
18
18
|
import { _ as _16 } from "./chunks/LpContextMenu-D0mzXqA7.js";
|
|
19
19
|
import { _ as _17 } from "./chunks/LpDatePicker-dY2q4dr4.js";
|
|
20
20
|
import { _ as _18 } from "./chunks/LpDisclosure-DP6yfgfM.js";
|
|
@@ -32,7 +32,7 @@ import { _ as _28 } from "./chunks/LpLightbox-CzhFe9wZ.js";
|
|
|
32
32
|
import { fileNameOf } from "./components/lightbox.js";
|
|
33
33
|
import { _ as _29 } from "./chunks/LpLink-DHwOEp4e.js";
|
|
34
34
|
import { _ as _30 } from "./chunks/LpLogViewer-tmKPNTuU.js";
|
|
35
|
-
import { _ as _31 } from "./chunks/LpModal-
|
|
35
|
+
import { _ as _31 } from "./chunks/LpModal-g2dGpF7l.js";
|
|
36
36
|
import { _ as _32 } from "./chunks/LpNotificationBell-DHze8wYN.js";
|
|
37
37
|
import { _ as _33 } from "./chunks/LpNumberField-C5ar-OwE.js";
|
|
38
38
|
import { _ as _34 } from "./chunks/LpOtpInput-DqaT0Z75.js";
|
package/package.json
CHANGED
|
@@ -8,7 +8,8 @@ import {
|
|
|
8
8
|
DialogRoot,
|
|
9
9
|
DialogTitle,
|
|
10
10
|
} from "reka-ui"
|
|
11
|
-
import {
|
|
11
|
+
import type { ComponentPublicInstance } from "vue"
|
|
12
|
+
import { computed, onBeforeUnmount, ref, useSlots, watch } from "vue"
|
|
12
13
|
import { CLOSE_ICON } from "./dropdown"
|
|
13
14
|
import LpIcon from "./LpIcon.vue"
|
|
14
15
|
import LpScrollArea from "./LpScrollArea.vue"
|
|
@@ -70,6 +71,129 @@ const bodyPad = computed(() =>
|
|
|
70
71
|
.filter(Boolean)
|
|
71
72
|
.join(" "),
|
|
72
73
|
)
|
|
74
|
+
|
|
75
|
+
/*
|
|
76
|
+
* Height is content-derived, so a body that arrives late — a placeholder swapped
|
|
77
|
+
* for the loaded thing — resizes the panel in a single frame, right on top of
|
|
78
|
+
* the open animation. CSS alone can't smooth it: `height: auto` isn't
|
|
79
|
+
* interpolable, and the keyword-interpolation escape hatches
|
|
80
|
+
* (`interpolate-size`, `calc-size()`) are Chromium-only, so they would do
|
|
81
|
+
* nothing in the WebKitGTK launcher. Measuring and pinning a pixel height is
|
|
82
|
+
* what works on every engine.
|
|
83
|
+
*
|
|
84
|
+
* The pin is always the panel's own natural height, so `max-h` still clips and
|
|
85
|
+
* the body keeps `flex-1 min-h-0` — an over-tall panel hands its overflow to the
|
|
86
|
+
* scroll area exactly as before. A panel whose size never moves is pinned once
|
|
87
|
+
* and never transitions.
|
|
88
|
+
*/
|
|
89
|
+
// reka renders a real element but exposes it as a component instance, so the
|
|
90
|
+
// panel is reached through $el.
|
|
91
|
+
const panelRef = ref<ComponentPublicInstance | null>(null)
|
|
92
|
+
const tweening = ref(false)
|
|
93
|
+
const resizing = ref(false)
|
|
94
|
+
|
|
95
|
+
/** Kept in step with the `duration-fast` utility on the panel. */
|
|
96
|
+
const TWEEN_MS = 160
|
|
97
|
+
|
|
98
|
+
let ro: ResizeObserver | undefined
|
|
99
|
+
let mo: MutationObserver | undefined
|
|
100
|
+
let raf = 0
|
|
101
|
+
let settleTimer: ReturnType<typeof setTimeout> | undefined
|
|
102
|
+
|
|
103
|
+
// The height is driven imperatively rather than through a style binding. By the
|
|
104
|
+
// time replacement content is in the DOM the panel has already reflowed to its
|
|
105
|
+
// new size, so a bound value would only ever be written once, with no earlier
|
|
106
|
+
// height for the transition to start from. Writing the OLD height, forcing a
|
|
107
|
+
// reflow, then writing the new one gives the transition both ends.
|
|
108
|
+
const retune = (el: HTMLElement) => {
|
|
109
|
+
const from = el.offsetHeight
|
|
110
|
+
el.style.height = ""
|
|
111
|
+
// `max-h` comes from a class, so the panel already reports the clipped figure
|
|
112
|
+
// once the cap bites. Pinning that would leave a short panel sitting over
|
|
113
|
+
// content it can no longer grow to fit, so the natural height is measured with
|
|
114
|
+
// the cap lifted and the clamp is left to CSS afterwards.
|
|
115
|
+
el.style.maxHeight = "none"
|
|
116
|
+
const to = el.offsetHeight
|
|
117
|
+
el.style.maxHeight = ""
|
|
118
|
+
// Nothing moved, so there is nothing to ease — and leaving the height unset
|
|
119
|
+
// keeps the panel free to size itself.
|
|
120
|
+
if (to === 0 || to === from) return
|
|
121
|
+
el.style.height = `${from}px`
|
|
122
|
+
void el.offsetHeight
|
|
123
|
+
el.style.height = `${to}px`
|
|
124
|
+
|
|
125
|
+
// While the panel is still catching up, the body is shorter than the content
|
|
126
|
+
// it already holds, so the scroll area would flash a bar for an overflow that
|
|
127
|
+
// resolves itself the moment the transition lands.
|
|
128
|
+
resizing.value = true
|
|
129
|
+
clearTimeout(settleTimer)
|
|
130
|
+
settleTimer = setTimeout(() => {
|
|
131
|
+
resizing.value = false
|
|
132
|
+
// Released once the panel has arrived. A height left pinned in pixels stops
|
|
133
|
+
// being a starting point and becomes a cap: content that grows afterwards
|
|
134
|
+
// is clipped, and `max-h` can no longer size the panel itself.
|
|
135
|
+
el.style.height = ""
|
|
136
|
+
}, TWEEN_MS)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Keyed off `open` rather than the ref: the panel is portalled and remounts on
|
|
140
|
+
// every open, and the teardown has to run on close, when the ref is already gone.
|
|
141
|
+
watch(
|
|
142
|
+
() => props.open,
|
|
143
|
+
(isOpen) => {
|
|
144
|
+
ro?.disconnect()
|
|
145
|
+
mo?.disconnect()
|
|
146
|
+
ro = undefined
|
|
147
|
+
mo = undefined
|
|
148
|
+
cancelAnimationFrame(raf)
|
|
149
|
+
clearTimeout(settleTimer)
|
|
150
|
+
tweening.value = false
|
|
151
|
+
resizing.value = false
|
|
152
|
+
|
|
153
|
+
const el = panelRef.value?.$el
|
|
154
|
+
if (!isOpen || !(el instanceof HTMLElement)) return
|
|
155
|
+
|
|
156
|
+
el.style.height = `${el.offsetHeight}px`
|
|
157
|
+
// The opening panel must not animate its height in from the pre-content box,
|
|
158
|
+
// so the transition only arms once that first pin has painted. The pin is
|
|
159
|
+
// dropped in the same frame: it exists to give the first `retune` something
|
|
160
|
+
// to start from, and holding it would cap the panel at its opening size.
|
|
161
|
+
raf = requestAnimationFrame(() => {
|
|
162
|
+
tweening.value = true
|
|
163
|
+
el.style.height = ""
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
// Two triggers, since either can move the height alone: the resize observer
|
|
167
|
+
// catches a child growing in place (an image decoding, a list filling in),
|
|
168
|
+
// the mutation observer catches one subtree swapped for another, where every
|
|
169
|
+
// observed box may keep its size. Children are observed, never the panel —
|
|
170
|
+
// that would feed the pin straight back in.
|
|
171
|
+
const watchChildren = () => {
|
|
172
|
+
if (!ro) return
|
|
173
|
+
ro.disconnect()
|
|
174
|
+
for (const child of Array.from(el.children)) ro.observe(child)
|
|
175
|
+
}
|
|
176
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
177
|
+
ro = new ResizeObserver(() => retune(el))
|
|
178
|
+
watchChildren()
|
|
179
|
+
}
|
|
180
|
+
if (typeof MutationObserver !== "undefined") {
|
|
181
|
+
mo = new MutationObserver(() => {
|
|
182
|
+
watchChildren()
|
|
183
|
+
retune(el)
|
|
184
|
+
})
|
|
185
|
+
mo.observe(el, { childList: true, subtree: true, characterData: true })
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
{ flush: "post" },
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
onBeforeUnmount(() => {
|
|
192
|
+
ro?.disconnect()
|
|
193
|
+
mo?.disconnect()
|
|
194
|
+
cancelAnimationFrame(raf)
|
|
195
|
+
clearTimeout(settleTimer)
|
|
196
|
+
})
|
|
73
197
|
</script>
|
|
74
198
|
|
|
75
199
|
<template>
|
|
@@ -100,8 +224,9 @@ const bodyPad = computed(() =>
|
|
|
100
224
|
pass a description keeps the generated id and the link to
|
|
101
225
|
DialogDescription. -->
|
|
102
226
|
<DialogContent
|
|
103
|
-
|
|
104
|
-
|
|
227
|
+
ref="panelRef"
|
|
228
|
+
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)]"
|
|
229
|
+
:class="[widthClass, tweening ? 'transition-[height] duration-fast ease-[var(--ease-emphasized)] motion-reduce:transition-none' : '']"
|
|
105
230
|
:style="width ? { width } : undefined"
|
|
106
231
|
v-bind="describedByAttrs"
|
|
107
232
|
>
|
|
@@ -135,9 +260,14 @@ const bodyPad = computed(() =>
|
|
|
135
260
|
>
|
|
136
261
|
<slot />
|
|
137
262
|
</div>
|
|
263
|
+
<!-- While the panel is still growing it is briefly shorter than the body
|
|
264
|
+
it already holds, so the scroll area raises a bar for an overflow
|
|
265
|
+
that resolves itself as the transition lands. Hiding just the bar
|
|
266
|
+
for the length of the tween leaves scrolling itself untouched. -->
|
|
138
267
|
<LpScrollArea
|
|
139
268
|
v-else
|
|
140
269
|
class="min-h-0 flex-1 text-sm text-ink/90"
|
|
270
|
+
:class="resizing ? '[&_[data-scrollbarimpl]]:invisible' : ''"
|
|
141
271
|
:content-class="bodyPad"
|
|
142
272
|
>
|
|
143
273
|
<slot />
|
|
@@ -104,11 +104,18 @@ export function fileIcon(node: FileNode, expanded = false): string {
|
|
|
104
104
|
return EXT_ICONS[ext] ?? "lucide:file"
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/*
|
|
108
|
+
* Built once. `String.localeCompare` with options constructs a collator on every
|
|
109
|
+
* call, which on a few thousand nodes costs tens of milliseconds — enough to
|
|
110
|
+
* drop frames while a dialog is animating open.
|
|
111
|
+
*/
|
|
112
|
+
const NAME_COLLATOR = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" })
|
|
113
|
+
|
|
107
114
|
/** Directories first, then case-insensitive by name — the file-manager order. */
|
|
108
115
|
export function sortNodes(nodes: FileNode[]): FileNode[] {
|
|
109
116
|
return [...nodes].sort((a, b) => {
|
|
110
117
|
if (a.kind !== b.kind) return a.kind === "dir" ? -1 : 1
|
|
111
|
-
return a.name
|
|
118
|
+
return NAME_COLLATOR.compare(a.name, b.name)
|
|
112
119
|
})
|
|
113
120
|
}
|
|
114
121
|
|