@leavepulse/ui 0.28.0 → 0.28.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/assets/LpDropzone-DVDC-ABU.css +52 -0
- package/dist/assets/LpImageEditor-DqP5JKCy.css +25 -0
- package/dist/chunks/{LpLogViewer-CcQ3s3Fp.js → LpLogViewer-CKmEQYnm.js} +66 -8
- package/dist/chunks/{LpNumberField-B3JuQFol.js → LpNumberField-NPE8Bumx.js} +14 -3
- package/dist/chunks/{LpProgress-DoIP4jQR.js → LpProgress-Btq5W4Ut.js} +14 -14
- package/dist/component-names.d.ts +1 -1
- package/dist/components/LpDropzone.vue.d.ts +66 -0
- package/dist/components/LpDropzone.vue.js +163 -0
- package/dist/components/LpImageEditor.vue.d.ts +58 -0
- package/dist/components/LpImageEditor.vue.js +357 -0
- package/dist/components/LpLogViewer.vue.d.ts +4 -0
- package/dist/components/LpLogViewer.vue.js +1 -1
- package/dist/components/LpNumberField.vue.d.ts +1 -0
- package/dist/components/LpNumberField.vue.js +1 -1
- package/dist/components/LpProgress.vue.js +1 -1
- package/dist/composables/useHotkeys.d.ts +16 -1
- package/dist/composables/useHotkeys.js +8 -1
- package/dist/composables/useZoomPan.d.ts +24 -0
- package/dist/composables/useZoomPan.js +11 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +9 -5
- package/package.json +1 -1
- package/src/component-names.ts +2 -0
- package/src/components/LpDropzone.vue +299 -0
- package/src/components/LpImageEditor.vue +515 -0
- package/src/components/LpLogViewer.vue +114 -2
- package/src/components/LpNumberField.vue +16 -0
- package/src/components/LpProgress.vue +30 -21
- package/src/composables/useHotkeys.ts +30 -1
- package/src/composables/useZoomPan.ts +30 -0
- package/src/index.ts +2 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { defineComponent, computed, shallowRef, ref, watch, onBeforeUnmount, openBlock, createElementBlock, createElementVNode, unref, withModifiers, normalizeStyle, normalizeClass, createCommentVNode, createVNode, Transition, withCtx, Fragment, renderList, toDisplayString, createBlock, createTextVNode } from "vue";
|
|
2
|
+
import { useHotkeys } from "../composables/useHotkeys.js";
|
|
3
|
+
import { useReveal } from "../composables/useReveal.js";
|
|
4
|
+
import { useZoomPan } from "../composables/useZoomPan.js";
|
|
5
|
+
import { _ as _sfc_main$1 } from "../chunks/LpButton-5VXXCjwF.js";
|
|
6
|
+
import "reka-ui";
|
|
7
|
+
import { _ as _sfc_main$2 } from "../chunks/LpIcon-CCnX5_2j.js";
|
|
8
|
+
import { _ as _export_sfc } from "../chunks/_plugin-vue_export-helper-1tPrXgE0.js";
|
|
9
|
+
import '../assets/LpImageEditor-DqP5JKCy.css';const _hoisted_1 = ["aria-label"];
|
|
10
|
+
const _hoisted_2 = ["src"];
|
|
11
|
+
const _hoisted_3 = {
|
|
12
|
+
key: 0,
|
|
13
|
+
class: "pointer-events-none absolute inset-0 grid grid-cols-3 grid-rows-3"
|
|
14
|
+
};
|
|
15
|
+
const _hoisted_4 = { class: "flex items-center gap-1.5 text-muted" };
|
|
16
|
+
const _hoisted_5 = { class: "flex items-center rounded-control bg-surface-soft p-0.5" };
|
|
17
|
+
const _hoisted_6 = { class: "flex items-center rounded-control bg-surface-soft p-0.5" };
|
|
18
|
+
const _hoisted_7 = { class: "min-w-10 text-center text-xs tabular-nums" };
|
|
19
|
+
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
20
|
+
__name: "LpImageEditor",
|
|
21
|
+
props: {
|
|
22
|
+
file: {},
|
|
23
|
+
aspect: { default: "free" },
|
|
24
|
+
maxEdge: { default: 2400 },
|
|
25
|
+
quality: { default: 0.9 },
|
|
26
|
+
labels: { default: () => ({}) },
|
|
27
|
+
animate: { type: Boolean, default: true }
|
|
28
|
+
},
|
|
29
|
+
setup(__props, { expose: __expose }) {
|
|
30
|
+
const props = __props;
|
|
31
|
+
const { el: revealAnchor, revealed } = useReveal({ immediate: () => !props.animate });
|
|
32
|
+
const text = computed(() => ({
|
|
33
|
+
rotateLeft: "Rotate left",
|
|
34
|
+
rotateRight: "Rotate right",
|
|
35
|
+
zoomIn: "Zoom in",
|
|
36
|
+
zoomOut: "Zoom out",
|
|
37
|
+
reset: "Reset",
|
|
38
|
+
canvas: "Crop area — arrows pan, +/− zoom, R rotates, Alt+wheel turns freely",
|
|
39
|
+
...props.labels
|
|
40
|
+
}));
|
|
41
|
+
const ASPECTS = {
|
|
42
|
+
free: null,
|
|
43
|
+
square: 1,
|
|
44
|
+
"4:3": 4 / 3,
|
|
45
|
+
"3:4": 3 / 4,
|
|
46
|
+
"16:9": 16 / 9
|
|
47
|
+
};
|
|
48
|
+
const source = shallowRef(null);
|
|
49
|
+
const objectUrl = ref("");
|
|
50
|
+
const rotation = ref(0);
|
|
51
|
+
const viewport = ref(null);
|
|
52
|
+
const zoom = useZoomPan({ min: 1, max: 6 });
|
|
53
|
+
watch(viewport, (el) => zoom.setViewport(el));
|
|
54
|
+
const upright = computed(() => (rotation.value % 360 + 360) % 360);
|
|
55
|
+
const edited = computed(() => upright.value !== 0 || zoom.scale.value !== 1);
|
|
56
|
+
watch(
|
|
57
|
+
() => props.file,
|
|
58
|
+
async (file) => {
|
|
59
|
+
revoke();
|
|
60
|
+
rotation.value = 0;
|
|
61
|
+
zoom.reset();
|
|
62
|
+
objectUrl.value = URL.createObjectURL(file);
|
|
63
|
+
source.value = await decode(file, objectUrl.value);
|
|
64
|
+
},
|
|
65
|
+
{ immediate: true }
|
|
66
|
+
);
|
|
67
|
+
async function decode(file, url) {
|
|
68
|
+
try {
|
|
69
|
+
return await createImageBitmap(file);
|
|
70
|
+
} catch {
|
|
71
|
+
return await new Promise((resolve) => {
|
|
72
|
+
const img = new Image();
|
|
73
|
+
img.onload = () => resolve(img);
|
|
74
|
+
img.onerror = () => resolve(null);
|
|
75
|
+
img.src = url;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const sourceSize = computed(() => {
|
|
80
|
+
const s = source.value;
|
|
81
|
+
if (!s) return null;
|
|
82
|
+
return s instanceof HTMLImageElement ? { width: s.naturalWidth, height: s.naturalHeight } : { width: s.width, height: s.height };
|
|
83
|
+
});
|
|
84
|
+
function revoke() {
|
|
85
|
+
if (objectUrl.value) URL.revokeObjectURL(objectUrl.value);
|
|
86
|
+
objectUrl.value = "";
|
|
87
|
+
if (source.value instanceof ImageBitmap) source.value.close();
|
|
88
|
+
source.value = null;
|
|
89
|
+
}
|
|
90
|
+
onBeforeUnmount(revoke);
|
|
91
|
+
function rotate(by) {
|
|
92
|
+
rotation.value += by;
|
|
93
|
+
const { x, y } = zoom.offset.value;
|
|
94
|
+
const quarters = (by / 90 % 4 + 4) % 4;
|
|
95
|
+
let next = { x, y };
|
|
96
|
+
for (let i = 0; i < quarters; i++) next = { x: -next.y, y: next.x };
|
|
97
|
+
zoom.setView(next);
|
|
98
|
+
}
|
|
99
|
+
function reset() {
|
|
100
|
+
rotation.value = 0;
|
|
101
|
+
zoom.reset();
|
|
102
|
+
}
|
|
103
|
+
const boxRatio = computed(() => {
|
|
104
|
+
if (typeof props.aspect === "number" && props.aspect > 0) return props.aspect;
|
|
105
|
+
const ratio = ASPECTS[props.aspect];
|
|
106
|
+
if (ratio) return ratio;
|
|
107
|
+
const size = sourceSize.value;
|
|
108
|
+
if (!size) return 1;
|
|
109
|
+
const turned = upright.value % 180 !== 0;
|
|
110
|
+
const w = turned ? size.height : size.width;
|
|
111
|
+
const h = turned ? size.width : size.height;
|
|
112
|
+
return w / h;
|
|
113
|
+
});
|
|
114
|
+
const frameStyle = computed(() => ({ aspectRatio: String(boxRatio.value) }));
|
|
115
|
+
const imageStyle = computed(() => ({
|
|
116
|
+
...zoom.style.value,
|
|
117
|
+
// Rotation sits innermost, after the composable's scale and translate, so a
|
|
118
|
+
// drag still moves the picture the way the pointer went once it is turned.
|
|
119
|
+
transform: `${zoom.style.value.transform} rotate(${rotation.value}deg)`,
|
|
120
|
+
// A quarter turn travels. Snapping is how you lose track of which way the
|
|
121
|
+
// picture went, and the rest of the kit moves — a control that jumps reads
|
|
122
|
+
// as a different component. The composable suppresses its own transition
|
|
123
|
+
// while a drag is in flight, and that wins here too.
|
|
124
|
+
transition: zoom.style.value.transition ?? `transform var(--duration-medium) var(--ease-emphasized)`
|
|
125
|
+
}));
|
|
126
|
+
async function exportImage() {
|
|
127
|
+
const picture = source.value;
|
|
128
|
+
const size = sourceSize.value;
|
|
129
|
+
const frame = viewport.value;
|
|
130
|
+
if (!picture || !size || !frame) return null;
|
|
131
|
+
if (!edited.value && props.aspect === "free") return null;
|
|
132
|
+
const turned = upright.value % 180 !== 0;
|
|
133
|
+
const uprightW = turned ? size.height : size.width;
|
|
134
|
+
const uprightH = turned ? size.width : size.height;
|
|
135
|
+
const cover = Math.max(frame.clientWidth / uprightW, frame.clientHeight / uprightH);
|
|
136
|
+
const shown = cover * zoom.scale.value;
|
|
137
|
+
const cropW = Math.min(uprightW, frame.clientWidth / shown);
|
|
138
|
+
const cropH = Math.min(uprightH, frame.clientHeight / shown);
|
|
139
|
+
const centreX = clamp(
|
|
140
|
+
uprightW / 2 - zoom.offset.value.x / cover,
|
|
141
|
+
cropW / 2,
|
|
142
|
+
uprightW - cropW / 2
|
|
143
|
+
);
|
|
144
|
+
const centreY = clamp(
|
|
145
|
+
uprightH / 2 - zoom.offset.value.y / cover,
|
|
146
|
+
cropH / 2,
|
|
147
|
+
uprightH - cropH / 2
|
|
148
|
+
);
|
|
149
|
+
const scale = Math.min(1, props.maxEdge / Math.max(cropW, cropH));
|
|
150
|
+
const canvas = document.createElement("canvas");
|
|
151
|
+
canvas.width = Math.max(1, Math.round(cropW * scale));
|
|
152
|
+
canvas.height = Math.max(1, Math.round(cropH * scale));
|
|
153
|
+
const ctx = canvas.getContext("2d");
|
|
154
|
+
if (!ctx) return null;
|
|
155
|
+
ctx.translate(canvas.width / 2, canvas.height / 2);
|
|
156
|
+
ctx.rotate(upright.value * Math.PI / 180);
|
|
157
|
+
ctx.scale(scale, scale);
|
|
158
|
+
const readW = turned ? cropH : cropW;
|
|
159
|
+
const readH = turned ? cropW : cropH;
|
|
160
|
+
const readX = (turned ? centreY : centreX) - readW / 2;
|
|
161
|
+
const readY = (turned ? centreX : centreY) - readH / 2;
|
|
162
|
+
ctx.drawImage(picture, readX, readY, readW, readH, -readW / 2, -readH / 2, readW, readH);
|
|
163
|
+
return await new Promise(
|
|
164
|
+
(resolve) => canvas.toBlob(resolve, "image/jpeg", props.quality)
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
function clamp(value, low, high) {
|
|
168
|
+
return Math.min(Math.max(value, low), Math.max(low, high));
|
|
169
|
+
}
|
|
170
|
+
function onWheel(event) {
|
|
171
|
+
if (event.altKey) {
|
|
172
|
+
event.preventDefault();
|
|
173
|
+
const step = event.shiftKey ? 15 : 1;
|
|
174
|
+
rotation.value += Math.sign(event.deltaY) * step;
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
zoom.onWheel(event);
|
|
178
|
+
}
|
|
179
|
+
function pan(dx, dy, shift) {
|
|
180
|
+
const step = shift ? 40 : 10;
|
|
181
|
+
const { x, y } = zoom.offset.value;
|
|
182
|
+
zoom.setView({ x: x + dx * step, y: y + dy * step });
|
|
183
|
+
}
|
|
184
|
+
useHotkeys(
|
|
185
|
+
() => [
|
|
186
|
+
{ key: "arrowleft", handler: () => pan(1, 0, false) },
|
|
187
|
+
{ key: "arrowleft", shift: true, handler: () => pan(1, 0, true) },
|
|
188
|
+
{ key: "arrowright", handler: () => pan(-1, 0, false) },
|
|
189
|
+
{ key: "arrowright", shift: true, handler: () => pan(-1, 0, true) },
|
|
190
|
+
{ key: "arrowup", handler: () => pan(0, 1, false) },
|
|
191
|
+
{ key: "arrowup", shift: true, handler: () => pan(0, 1, true) },
|
|
192
|
+
{ key: "arrowdown", handler: () => pan(0, -1, false) },
|
|
193
|
+
{ key: "arrowdown", shift: true, handler: () => pan(0, -1, true) },
|
|
194
|
+
{ key: "+", handler: () => zoom.zoomBy(1.3) },
|
|
195
|
+
{ key: "=", handler: () => zoom.zoomBy(1.3) },
|
|
196
|
+
{ key: "-", handler: () => zoom.zoomBy(1 / 1.3) },
|
|
197
|
+
{ key: "r", code: "KeyR", handler: () => rotate(90) },
|
|
198
|
+
// Shift reverses it, the way a modifier reverses a direction elsewhere.
|
|
199
|
+
{ key: "r", code: "KeyR", shift: true, handler: () => rotate(-90) },
|
|
200
|
+
{ key: "0", code: "Digit0", handler: reset }
|
|
201
|
+
],
|
|
202
|
+
{ scope: viewport }
|
|
203
|
+
);
|
|
204
|
+
const SHORTCUTS = [
|
|
205
|
+
{ keys: ["←", "→", "↑", "↓"], label: "Pan (Shift for a bigger step)" },
|
|
206
|
+
{ keys: ["+", "−"], label: "Zoom" },
|
|
207
|
+
{ keys: ["R"], label: "Rotate a quarter turn (Shift reverses)" },
|
|
208
|
+
{ keys: ["Alt", "wheel"], label: "Turn freely (Shift snaps to 15°)" },
|
|
209
|
+
{ keys: ["0"], label: "Reset" }
|
|
210
|
+
];
|
|
211
|
+
__expose({ export: exportImage, reset, edited, shortcuts: SHORTCUTS });
|
|
212
|
+
return (_ctx, _cache) => {
|
|
213
|
+
return openBlock(), createElementBlock("div", {
|
|
214
|
+
ref_key: "revealAnchor",
|
|
215
|
+
ref: revealAnchor,
|
|
216
|
+
class: "flex flex-col gap-3"
|
|
217
|
+
}, [
|
|
218
|
+
createElementVNode("div", {
|
|
219
|
+
ref_key: "viewport",
|
|
220
|
+
ref: viewport,
|
|
221
|
+
tabindex: "0",
|
|
222
|
+
role: "application",
|
|
223
|
+
"aria-label": text.value.canvas,
|
|
224
|
+
class: "relative w-full overflow-hidden rounded-card border border-line bg-surface-soft focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
225
|
+
style: normalizeStyle(frameStyle.value),
|
|
226
|
+
onWheel: withModifiers(onWheel, ["prevent"]),
|
|
227
|
+
onPointerdown: _cache[0] || (_cache[0] = //@ts-ignore
|
|
228
|
+
(...args) => unref(zoom).onPointerDown && unref(zoom).onPointerDown(...args)),
|
|
229
|
+
onPointermove: _cache[1] || (_cache[1] = //@ts-ignore
|
|
230
|
+
(...args) => unref(zoom).onPointerMove && unref(zoom).onPointerMove(...args)),
|
|
231
|
+
onPointerup: _cache[2] || (_cache[2] = //@ts-ignore
|
|
232
|
+
(...args) => unref(zoom).onPointerUp && unref(zoom).onPointerUp(...args)),
|
|
233
|
+
onPointercancel: _cache[3] || (_cache[3] = //@ts-ignore
|
|
234
|
+
(...args) => unref(zoom).onPointerUp && unref(zoom).onPointerUp(...args))
|
|
235
|
+
}, [
|
|
236
|
+
objectUrl.value ? (openBlock(), createElementBlock("img", {
|
|
237
|
+
key: 0,
|
|
238
|
+
src: objectUrl.value,
|
|
239
|
+
alt: "",
|
|
240
|
+
draggable: "false",
|
|
241
|
+
class: normalizeClass(["lp-editor__image absolute inset-0 size-full select-none object-cover motion-reduce:transition-none", [
|
|
242
|
+
unref(zoom).panning.value ? "cursor-grabbing" : "cursor-grab",
|
|
243
|
+
__props.animate && !unref(revealed) ? "lp-editor__image--waiting" : ""
|
|
244
|
+
]]),
|
|
245
|
+
style: normalizeStyle(imageStyle.value)
|
|
246
|
+
}, null, 14, _hoisted_2)) : createCommentVNode("", true),
|
|
247
|
+
createVNode(Transition, {
|
|
248
|
+
"enter-active-class": "transition-opacity duration-[var(--duration-fast)] motion-reduce:transition-none",
|
|
249
|
+
"leave-active-class": "transition-opacity duration-[var(--duration-fast)] motion-reduce:transition-none",
|
|
250
|
+
"enter-from-class": "opacity-0",
|
|
251
|
+
"leave-to-class": "opacity-0"
|
|
252
|
+
}, {
|
|
253
|
+
default: withCtx(() => [
|
|
254
|
+
unref(zoom).panning.value ? (openBlock(), createElementBlock("div", _hoisted_3, [
|
|
255
|
+
(openBlock(), createElementBlock(Fragment, null, renderList(9, (i) => {
|
|
256
|
+
return createElementVNode("div", {
|
|
257
|
+
key: i,
|
|
258
|
+
class: normalizeClass(["border-white/15", [i % 3 !== 0 ? "border-r" : "", i < 7 ? "border-b" : ""]])
|
|
259
|
+
}, null, 2);
|
|
260
|
+
}), 64))
|
|
261
|
+
])) : createCommentVNode("", true)
|
|
262
|
+
]),
|
|
263
|
+
_: 1
|
|
264
|
+
})
|
|
265
|
+
], 44, _hoisted_1),
|
|
266
|
+
createElementVNode("div", _hoisted_4, [
|
|
267
|
+
createElementVNode("div", _hoisted_5, [
|
|
268
|
+
createVNode(_sfc_main$1, {
|
|
269
|
+
size: "sm",
|
|
270
|
+
variant: "ghost",
|
|
271
|
+
title: text.value.rotateLeft,
|
|
272
|
+
"aria-label": text.value.rotateLeft,
|
|
273
|
+
onClick: _cache[4] || (_cache[4] = ($event) => rotate(-90))
|
|
274
|
+
}, {
|
|
275
|
+
default: withCtx(() => [
|
|
276
|
+
createVNode(_sfc_main$2, {
|
|
277
|
+
name: "lucide:rotate-ccw",
|
|
278
|
+
size: 16
|
|
279
|
+
})
|
|
280
|
+
]),
|
|
281
|
+
_: 1
|
|
282
|
+
}, 8, ["title", "aria-label"]),
|
|
283
|
+
createVNode(_sfc_main$1, {
|
|
284
|
+
size: "sm",
|
|
285
|
+
variant: "ghost",
|
|
286
|
+
title: text.value.rotateRight,
|
|
287
|
+
"aria-label": text.value.rotateRight,
|
|
288
|
+
onClick: _cache[5] || (_cache[5] = ($event) => rotate(90))
|
|
289
|
+
}, {
|
|
290
|
+
default: withCtx(() => [
|
|
291
|
+
createVNode(_sfc_main$2, {
|
|
292
|
+
name: "lucide:rotate-cw",
|
|
293
|
+
size: 16
|
|
294
|
+
})
|
|
295
|
+
]),
|
|
296
|
+
_: 1
|
|
297
|
+
}, 8, ["title", "aria-label"])
|
|
298
|
+
]),
|
|
299
|
+
createElementVNode("div", _hoisted_6, [
|
|
300
|
+
createVNode(_sfc_main$1, {
|
|
301
|
+
size: "sm",
|
|
302
|
+
variant: "ghost",
|
|
303
|
+
disabled: unref(zoom).scale.value <= 1,
|
|
304
|
+
title: text.value.zoomOut,
|
|
305
|
+
"aria-label": text.value.zoomOut,
|
|
306
|
+
onClick: _cache[6] || (_cache[6] = ($event) => unref(zoom).zoomBy(1 / 1.2))
|
|
307
|
+
}, {
|
|
308
|
+
default: withCtx(() => [
|
|
309
|
+
createVNode(_sfc_main$2, {
|
|
310
|
+
name: "lucide:minus",
|
|
311
|
+
size: 16
|
|
312
|
+
})
|
|
313
|
+
]),
|
|
314
|
+
_: 1
|
|
315
|
+
}, 8, ["disabled", "title", "aria-label"]),
|
|
316
|
+
createElementVNode("span", _hoisted_7, toDisplayString(Math.round(unref(zoom).scale.value * 100)) + "% ", 1),
|
|
317
|
+
createVNode(_sfc_main$1, {
|
|
318
|
+
size: "sm",
|
|
319
|
+
variant: "ghost",
|
|
320
|
+
title: text.value.zoomIn,
|
|
321
|
+
"aria-label": text.value.zoomIn,
|
|
322
|
+
onClick: _cache[7] || (_cache[7] = ($event) => unref(zoom).zoomBy(1.2))
|
|
323
|
+
}, {
|
|
324
|
+
default: withCtx(() => [
|
|
325
|
+
createVNode(_sfc_main$2, {
|
|
326
|
+
name: "lucide:plus",
|
|
327
|
+
size: 16
|
|
328
|
+
})
|
|
329
|
+
]),
|
|
330
|
+
_: 1
|
|
331
|
+
}, 8, ["title", "aria-label"])
|
|
332
|
+
]),
|
|
333
|
+
edited.value ? (openBlock(), createBlock(_sfc_main$1, {
|
|
334
|
+
key: 0,
|
|
335
|
+
size: "sm",
|
|
336
|
+
variant: "ghost",
|
|
337
|
+
class: "ml-auto",
|
|
338
|
+
onClick: reset
|
|
339
|
+
}, {
|
|
340
|
+
default: withCtx(() => [
|
|
341
|
+
createVNode(_sfc_main$2, {
|
|
342
|
+
name: "lucide:undo-2",
|
|
343
|
+
size: 15
|
|
344
|
+
}),
|
|
345
|
+
createTextVNode(" " + toDisplayString(text.value.reset), 1)
|
|
346
|
+
]),
|
|
347
|
+
_: 1
|
|
348
|
+
})) : createCommentVNode("", true)
|
|
349
|
+
])
|
|
350
|
+
], 512);
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
const LpImageEditor = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-7d404124"]]);
|
|
355
|
+
export {
|
|
356
|
+
LpImageEditor as default
|
|
357
|
+
};
|
|
@@ -35,6 +35,9 @@ type __VLS_Props = {
|
|
|
35
35
|
/** Fixed height. Anything CSS-valid; defaults to a comfortable terminal. */
|
|
36
36
|
height?: string;
|
|
37
37
|
emptyLabel?: string;
|
|
38
|
+
/** Shown while the stream is held. Defaulted in English; apps with i18n
|
|
39
|
+
* pass their own. */
|
|
40
|
+
frozenLabel?: string;
|
|
38
41
|
/**
|
|
39
42
|
* When true, a "loading older…" row is shown pinned at the top — set it while
|
|
40
43
|
* fetching a previous page in response to `reach-top`, clear it when done.
|
|
@@ -85,6 +88,7 @@ declare const __VLS_export: import('vue').DefineComponent<__VLS_Props, {}, {}, {
|
|
|
85
88
|
showTime: boolean;
|
|
86
89
|
showLevel: boolean;
|
|
87
90
|
tail: boolean;
|
|
91
|
+
frozenLabel: string;
|
|
88
92
|
rowMenu: boolean;
|
|
89
93
|
parse: boolean;
|
|
90
94
|
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
|
|
@@ -4,6 +4,7 @@ type __VLS_Props = {
|
|
|
4
4
|
max?: number;
|
|
5
5
|
step?: number;
|
|
6
6
|
disabled?: boolean;
|
|
7
|
+
unit?: string;
|
|
7
8
|
};
|
|
8
9
|
declare const __VLS_export: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {} & {
|
|
9
10
|
"update:modelValue": (value: number | null) => any;
|
|
@@ -42,4 +42,19 @@ export declare function appleModifierLabel(key: string): string;
|
|
|
42
42
|
* may come and go with the state that owns them — a getter is the point, not a
|
|
43
43
|
* formality.
|
|
44
44
|
*/
|
|
45
|
-
export
|
|
45
|
+
export interface UseHotkeysOptions {
|
|
46
|
+
/**
|
|
47
|
+
* Only act while the focus is inside this element.
|
|
48
|
+
*
|
|
49
|
+
* The default is the whole window, which is right for a command palette and
|
|
50
|
+
* wrong for a control: two of the same component on one page would both
|
|
51
|
+
* answer the same key, and a control beside a form would answer keys aimed
|
|
52
|
+
* at the form. Scoping it here rather than in each component because every
|
|
53
|
+
* one of them was about to write the same `document.activeElement` check and
|
|
54
|
+
* decline by hand.
|
|
55
|
+
*
|
|
56
|
+
* Undefined or null means the window, so the palette case stays the default.
|
|
57
|
+
*/
|
|
58
|
+
scope?: MaybeRefOrGetter<HTMLElement | null | undefined>;
|
|
59
|
+
}
|
|
60
|
+
export declare function useHotkeys(bindings: MaybeRefOrGetter<HotkeyBinding[]>, options?: UseHotkeysOptions): void;
|
|
@@ -31,9 +31,16 @@ function matches(event, binding) {
|
|
|
31
31
|
if (event.altKey !== Boolean(binding.alt)) return false;
|
|
32
32
|
return event.key.toLowerCase() === binding.key.toLowerCase() || binding.code !== void 0 && event.code === binding.code;
|
|
33
33
|
}
|
|
34
|
-
function useHotkeys(bindings) {
|
|
34
|
+
function useHotkeys(bindings, options = {}) {
|
|
35
|
+
function inScope(target) {
|
|
36
|
+
const el = toValue(options.scope);
|
|
37
|
+
if (!el) return true;
|
|
38
|
+
const node = target instanceof Node ? target : null;
|
|
39
|
+
return !!node && (el === node || el.contains(node));
|
|
40
|
+
}
|
|
35
41
|
function onKeydown(event) {
|
|
36
42
|
if (event.isComposing) return;
|
|
43
|
+
if (!inScope(event.target)) return;
|
|
37
44
|
const inTextEntry = isTextEntry(event.target);
|
|
38
45
|
for (const binding of toValue(bindings)) {
|
|
39
46
|
if (inTextEntry && !binding.allowInInput) continue;
|
|
@@ -11,6 +11,30 @@ export interface UseZoomPanOptions {
|
|
|
11
11
|
}
|
|
12
12
|
export interface UseZoomPan {
|
|
13
13
|
scale: Ref<number>;
|
|
14
|
+
/**
|
|
15
|
+
* Pan offset in CONTENT pixels — what `style` puts inside `translate()`,
|
|
16
|
+
* before the scale magnifies it. Exposed because anything that has to know
|
|
17
|
+
* *which part* is visible needs the number, not the CSS string: an exporter
|
|
18
|
+
* writing the cropped region, a minimap drawing the viewport rectangle, a
|
|
19
|
+
* caller restoring a saved position. Parsing it back out of `transform` is
|
|
20
|
+
* the alternative, and that breaks the moment this composable changes how it
|
|
21
|
+
* composes the transform.
|
|
22
|
+
*/
|
|
23
|
+
offset: ComputedRef<{
|
|
24
|
+
x: number;
|
|
25
|
+
y: number;
|
|
26
|
+
}>;
|
|
27
|
+
/**
|
|
28
|
+
* Put the view somewhere — restoring a saved position, or carrying one
|
|
29
|
+
* through a change that re-frames the content. Clamped like any other pan,
|
|
30
|
+
* so a caller cannot place the content off the viewport. Omitting `scale`
|
|
31
|
+
* keeps the current one.
|
|
32
|
+
*/
|
|
33
|
+
setView: (view: {
|
|
34
|
+
x: number;
|
|
35
|
+
y: number;
|
|
36
|
+
scale?: number;
|
|
37
|
+
}) => void;
|
|
14
38
|
/** True once zoomed past the minimum — the point at which panning matters. */
|
|
15
39
|
zoomed: ComputedRef<boolean>;
|
|
16
40
|
/** True while a drag is in flight, so the caller can swap the cursor. */
|
|
@@ -113,8 +113,19 @@ function useZoomPan(options = {}) {
|
|
|
113
113
|
// Settle smoothly on discrete zooms, but follow a drag or pinch exactly.
|
|
114
114
|
transition: panning.value || pinchStartDistance > 0 ? "none" : void 0
|
|
115
115
|
}));
|
|
116
|
+
const offset = computed(() => ({ x: x.value, y: y.value }));
|
|
117
|
+
function setView(view) {
|
|
118
|
+
if (view.scale !== void 0) {
|
|
119
|
+
scale.value = Math.min(max, Math.max(min, view.scale));
|
|
120
|
+
}
|
|
121
|
+
x.value = view.x;
|
|
122
|
+
y.value = view.y;
|
|
123
|
+
clampPan();
|
|
124
|
+
}
|
|
116
125
|
return {
|
|
117
126
|
scale,
|
|
127
|
+
offset,
|
|
128
|
+
setView,
|
|
118
129
|
zoomed,
|
|
119
130
|
panning,
|
|
120
131
|
style,
|
package/dist/index.d.ts
CHANGED
|
@@ -32,6 +32,7 @@ export { default as LpDivider } from './components/LpDivider.vue';
|
|
|
32
32
|
export { default as LpDrawer } from './components/LpDrawer.vue';
|
|
33
33
|
export { default as LpDropdownMenu } from './components/LpDropdownMenu.vue';
|
|
34
34
|
export type { MenuItem } from './components/LpDropdownMenu.vue';
|
|
35
|
+
export { default as LpDropzone } from './components/LpDropzone.vue';
|
|
35
36
|
export { default as LpEmptyState } from './components/LpEmptyState.vue';
|
|
36
37
|
export { default as LpFileTree } from './components/LpFileTree.vue';
|
|
37
38
|
export type { FileTreeSummary } from './components/LpFileTree.vue';
|
|
@@ -39,6 +40,7 @@ export type { CheckState, FileNode, NodeStats } from './components/fileTree';
|
|
|
39
40
|
export { ancestorIds, checkedCount, checkStateOf, computeStats, fileIcon, formatModified, formatSize, sortNodes, subtreeIds, subtreeSize, } from './components/fileTree';
|
|
40
41
|
export { default as LpFormField } from './components/LpFormField.vue';
|
|
41
42
|
export { default as LpIcon } from './components/LpIcon.vue';
|
|
43
|
+
export { default as LpImageEditor } from './components/LpImageEditor.vue';
|
|
42
44
|
export { default as LpInfraNode } from './components/LpInfraNode.vue';
|
|
43
45
|
export type { InfraNodeData } from './components/LpInfraNode.vue';
|
|
44
46
|
export { default as LpInput } from './components/LpInput.vue';
|
package/dist/index.js
CHANGED
|
@@ -23,20 +23,22 @@ import { _ as _19 } from "./chunks/LpDisclosure-BnwP4Q-b.js";
|
|
|
23
23
|
import { _ as _20 } from "./chunks/LpDivider-CUX46KR0.js";
|
|
24
24
|
import { _ as _21 } from "./chunks/LpDrawer-DAJN5rGK.js";
|
|
25
25
|
import { _ as _22 } from "./chunks/LpDropdownMenu-DJpoqRCb.js";
|
|
26
|
+
import { default as default3 } from "./components/LpDropzone.vue.js";
|
|
26
27
|
import { _ as _23 } from "./chunks/LpEmptyState-BV3q7Qyi.js";
|
|
27
28
|
import { _ as _24 } from "./chunks/LpFileTree-BaV7f0O7.js";
|
|
28
29
|
import { ancestorIds, checkStateOf, checkedCount, computeStats, fileIcon, formatModified, formatSize, sortNodes, subtreeIds, subtreeSize } from "./components/fileTree.js";
|
|
29
30
|
import { _ as _25 } from "./chunks/LpFormField-CesaPWPO.js";
|
|
30
31
|
import { _ as _26 } from "./chunks/LpIcon-CCnX5_2j.js";
|
|
32
|
+
import { default as default4 } from "./components/LpImageEditor.vue.js";
|
|
31
33
|
import { _ as _27 } from "./chunks/LpInfraNode-qIL3l7IO.js";
|
|
32
34
|
import { _ as _28 } from "./chunks/LpInput-jwqXlAy0.js";
|
|
33
35
|
import { _ as _29 } from "./chunks/LpLightbox-Ci35Y_xP.js";
|
|
34
36
|
import { fileNameOf } from "./components/lightbox.js";
|
|
35
37
|
import { _ as _30 } from "./chunks/LpLink-BHZ1CLlF.js";
|
|
36
|
-
import { _ as _31 } from "./chunks/LpLogViewer-
|
|
38
|
+
import { _ as _31 } from "./chunks/LpLogViewer-CKmEQYnm.js";
|
|
37
39
|
import { _ as _32 } from "./chunks/LpModal-D_o-56To.js";
|
|
38
40
|
import { _ as _33 } from "./chunks/LpNotificationBell-Dvu-b6Vn.js";
|
|
39
|
-
import { _ as _34 } from "./chunks/LpNumberField-
|
|
41
|
+
import { _ as _34 } from "./chunks/LpNumberField-NPE8Bumx.js";
|
|
40
42
|
import { _ as _35 } from "./chunks/LpNumberFlow-amWYaeuA.js";
|
|
41
43
|
import { _ as _36 } from "./chunks/LpOtpInput-DqaT0Z75.js";
|
|
42
44
|
import { _ as _37 } from "./chunks/LpPagination-CeGuNe_q.js";
|
|
@@ -45,7 +47,7 @@ import { _ as _39 } from "./chunks/LpPie-BkjWlfSw.js";
|
|
|
45
47
|
import { _ as _40 } from "./chunks/LpPhoneInput-NXXjciJJ.js";
|
|
46
48
|
import { flagEmoji, loadCountries, matchCountryByValue } from "./components/countries.js";
|
|
47
49
|
import { _ as _41 } from "./chunks/LpPopover-8268UmlC.js";
|
|
48
|
-
import { _ as _42 } from "./chunks/LpProgress-
|
|
50
|
+
import { _ as _42 } from "./chunks/LpProgress-Btq5W4Ut.js";
|
|
49
51
|
import { _ as _43 } from "./chunks/LpRadio-CL-pI0_O.js";
|
|
50
52
|
import { _ as _44 } from "./chunks/LpRadioGroup-B4yDTmi6.js";
|
|
51
53
|
import { _ as _45 } from "./chunks/LpRollingText-CM5gkSeF.js";
|
|
@@ -55,7 +57,7 @@ import { _ as _48 } from "./chunks/LpSection-aM6J_u51.js";
|
|
|
55
57
|
import { _ as _49 } from "./chunks/LpSelect-Cw8-gL5F.js";
|
|
56
58
|
import { _ as _50 } from "./chunks/LpServiceNode-D8uJ8sGk.js";
|
|
57
59
|
import { _ as _51 } from "./chunks/LpShift-DXNsauQZ.js";
|
|
58
|
-
import { default as
|
|
60
|
+
import { default as default5 } from "./components/LpShortcutsDialog.vue.js";
|
|
59
61
|
import { _ as _52 } from "./chunks/LpSidebar-7Vpy2IJc.js";
|
|
60
62
|
import { _ as _53 } from "./chunks/LpSkeleton-CTI-vBgW.js";
|
|
61
63
|
import { _ as _54 } from "./chunks/LpSlider-B1SF3pRV.js";
|
|
@@ -143,10 +145,12 @@ export {
|
|
|
143
145
|
_20 as LpDivider,
|
|
144
146
|
_21 as LpDrawer,
|
|
145
147
|
_22 as LpDropdownMenu,
|
|
148
|
+
default3 as LpDropzone,
|
|
146
149
|
_23 as LpEmptyState,
|
|
147
150
|
_24 as LpFileTree,
|
|
148
151
|
_25 as LpFormField,
|
|
149
152
|
_26 as LpIcon,
|
|
153
|
+
default4 as LpImageEditor,
|
|
150
154
|
_27 as LpInfraNode,
|
|
151
155
|
_28 as LpInput,
|
|
152
156
|
_29 as LpLightbox,
|
|
@@ -173,7 +177,7 @@ export {
|
|
|
173
177
|
_49 as LpSelect,
|
|
174
178
|
_50 as LpServiceNode,
|
|
175
179
|
_51 as LpShift,
|
|
176
|
-
|
|
180
|
+
default5 as LpShortcutsDialog,
|
|
177
181
|
_52 as LpSidebar,
|
|
178
182
|
_53 as LpSkeleton,
|
|
179
183
|
_54 as LpSlider,
|
package/package.json
CHANGED
package/src/component-names.ts
CHANGED
|
@@ -25,11 +25,13 @@ export const COMPONENT_NAMES = [
|
|
|
25
25
|
"LpDivider",
|
|
26
26
|
"LpDrawer",
|
|
27
27
|
"LpDropdownMenu",
|
|
28
|
+
"LpDropzone",
|
|
28
29
|
"LpEmptyState",
|
|
29
30
|
"LpFileTree",
|
|
30
31
|
"LpFileTreeNode",
|
|
31
32
|
"LpFormField",
|
|
32
33
|
"LpIcon",
|
|
34
|
+
"LpImageEditor",
|
|
33
35
|
"LpInfraNode",
|
|
34
36
|
"LpInput",
|
|
35
37
|
"LpLaneNode",
|