@leavepulse/ui 0.28.1 → 0.28.3

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.
@@ -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>;
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpLogViewer-CcQ3s3Fp.js";
1
+ import { _ as _sfc_main } from "../chunks/LpLogViewer-CKmEQYnm.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpProgress-DoIP4jQR.js";
1
+ import { _ as _sfc_main } from "../chunks/LpProgress-Btq5W4Ut.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
@@ -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 declare function useHotkeys(bindings: MaybeRefOrGetter<HotkeyBinding[]>): void;
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
@@ -11,7 +11,7 @@ import { _ as _9 } from "./chunks/LpBreadcrumbs-5uvViCXi.js";
11
11
  import { _ as _10 } from "./chunks/LpButton-5VXXCjwF.js";
12
12
  import { _ as _11 } from "./chunks/LpCalendar-B7ZL0ty3.js";
13
13
  import { _ as _12 } from "./chunks/LpCard-gu11ZmSt.js";
14
- import { _ as _13 } from "./chunks/LpChart-tNXs1ttr.js";
14
+ import { _ as _13 } from "./chunks/LpChart-UiU4RhXP.js";
15
15
  import { CHART_PALETTE, axisTicks, formatTick, niceScale, resolveSlices } from "./components/chart.js";
16
16
  import { _ as _14 } from "./chunks/LpCheckbox-BO0zuuPh.js";
17
17
  import { _ as _15 } from "./chunks/LpCodeBlock-rS_6mO3D.js";
@@ -23,17 +23,19 @@ 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-CcQ3s3Fp.js";
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
41
  import { _ as _34 } from "./chunks/LpNumberField-NPE8Bumx.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-DoIP4jQR.js";
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 default3 } from "./components/LpShortcutsDialog.vue.js";
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
- default3 as LpShortcutsDialog,
180
+ default5 as LpShortcutsDialog,
177
181
  _52 as LpSidebar,
178
182
  _53 as LpSkeleton,
179
183
  _54 as LpSlider,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leavepulse/ui",
3
- "version": "0.28.1",
3
+ "version": "0.28.3",
4
4
  "type": "module",
5
5
  "description": "LeavePulse token-driven Vue component kit.",
6
6
  "repository": {
@@ -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",
@@ -553,6 +553,25 @@ watch(
553
553
  )
554
554
 
555
555
  const clipId = `lp-chart-${Math.random().toString(36).slice(2, 9)}`
556
+
557
+ /*
558
+ * The gradient id for a series, with the series NAME reduced to id-safe
559
+ * characters.
560
+ *
561
+ * A name is a label — "used %", "in eth0", "read sda" — and putting it verbatim
562
+ * into an id produced `url(#lp-chart-x-g-used %)`, which is not a valid
563
+ * reference: the browser fails to resolve it and falls back to painting the
564
+ * shape black. The gradient was there, correct, and simply never reached, so an
565
+ * area chart of any series whose name held a space or a percent sign rendered
566
+ * as a solid black block under its line — while "cpu" and "memory" on the same
567
+ * page kept their colour, which is what made it look like a data problem rather
568
+ * than an escaping one.
569
+ *
570
+ * Index-suffixed so two names that differ only in punctuation ("in eth0" and
571
+ * "in-eth0") cannot collapse onto one id and share a colour.
572
+ */
573
+ const gradientId = (name: string, index: number) =>
574
+ `${clipId}-g-${index}-${name.replace(/[^a-zA-Z0-9_-]/g, "-")}`
556
575
  </script>
557
576
 
558
577
  <template>
@@ -618,8 +637,8 @@ const clipId = `lp-chart-${Math.random().toString(36).slice(2, 9)}`
618
637
  >
619
638
  <defs>
620
639
  <linearGradient
621
- v-for="s in lines"
622
- :id="`${clipId}-g-${s.name}`"
640
+ v-for="(s, i) in lines"
641
+ :id="gradientId(s.name, i)"
623
642
  :key="s.name"
624
643
  x1="0"
625
644
  y1="0"
@@ -728,11 +747,11 @@ const clipId = `lp-chart-${Math.random().toString(36).slice(2, 9)}`
728
747
 
729
748
  <!-- area fills -->
730
749
  <path
731
- v-for="s in lines"
750
+ v-for="(s, i) in lines"
732
751
  v-show="type === 'area'"
733
752
  :key="`f-${s.name}`"
734
753
  :d="s.fill"
735
- :fill="`url(#${clipId}-g-${s.name})`"
754
+ :fill="`url(#${gradientId(s.name, i)})`"
736
755
  />
737
756
 
738
757
  <!-- lines -->