@leavepulse/ui 0.16.0 → 0.17.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.
Files changed (38) hide show
  1. package/dist/chunks/LpAddressInput-D29vSoNd.js +366 -0
  2. package/dist/chunks/{LpConfirmDialog-C-iOXJIM.js → LpConfirmDialog-DnReh5pF.js} +1 -1
  3. package/dist/chunks/LpMapPicker-DFKrd1Ne.js +509 -0
  4. package/dist/chunks/{LpModal-DeZlvK1B.js → LpModal-CezA8eJ3.js} +9 -6
  5. package/dist/chunks/{LpNotificationBell-BtfRoL2i.js → LpNotificationBell-CDixx44E.js} +25 -12
  6. package/dist/chunks/{LpSelect-DznhPEWX.js → LpSelect-D5mU4cZO.js} +5 -1
  7. package/dist/chunks/{LpThemeSwitcher-BgWwfkxs.js → LpThemeSwitcher-DC6QygnU.js} +11 -3
  8. package/dist/component-names.d.ts +1 -1
  9. package/dist/components/LpAddressInput.vue.d.ts +53 -0
  10. package/dist/components/LpAddressInput.vue.js +4 -0
  11. package/dist/components/LpAlert.vue.d.ts +1 -1
  12. package/dist/components/LpConfirmDialog.vue.js +1 -1
  13. package/dist/components/LpFileTree.vue.d.ts +2 -2
  14. package/dist/components/LpLightbox.vue.d.ts +1 -1
  15. package/dist/components/LpMapPicker.vue.d.ts +44 -0
  16. package/dist/components/LpMapPicker.vue.js +4 -0
  17. package/dist/components/LpModal.vue.js +1 -1
  18. package/dist/components/LpNotificationBell.vue.d.ts +22 -0
  19. package/dist/components/LpNotificationBell.vue.js +1 -1
  20. package/dist/components/LpSelect.vue.js +1 -1
  21. package/dist/components/LpThemeSwitcher.vue.d.ts +7 -0
  22. package/dist/components/LpThemeSwitcher.vue.js +1 -1
  23. package/dist/components/address.d.ts +107 -0
  24. package/dist/components/address.js +96 -0
  25. package/dist/index.d.ts +5 -1
  26. package/dist/index.js +129 -117
  27. package/package.json +1 -1
  28. package/src/component-names.ts +2 -0
  29. package/src/components/LpAddressInput.vue +377 -0
  30. package/src/components/LpMapPicker.vue +665 -0
  31. package/src/components/LpModal.vue +15 -0
  32. package/src/components/LpNotificationBell.vue +47 -9
  33. package/src/components/LpSelect.vue +0 -0
  34. package/src/components/LpThemeSwitcher.vue +24 -2
  35. package/src/components/address.ts +250 -0
  36. package/src/index.ts +26 -1
  37. package/src/theme/presets.ts +8 -2
  38. package/src/tokens/tokens.css +18 -0
@@ -0,0 +1,509 @@
1
+ import { defineComponent, computed, ref, watch, onMounted, onBeforeUnmount, openBlock, createElementBlock, normalizeStyle, createElementVNode, normalizeClass, Fragment, renderList, createVNode, createCommentVNode, createBlock, toDisplayString } from "vue";
2
+ import { OSM_TILES, project, tileUrl, unproject } from "../components/address.js";
3
+ import { _ as _sfc_main$1 } from "./LpIcon-CCnX5_2j.js";
4
+ const _hoisted_1 = ["tabindex", "aria-label"];
5
+ const _hoisted_2 = ["src", "onLoad"];
6
+ const _hoisted_3 = { class: "absolute right-2 top-2 flex flex-col gap-1.5" };
7
+ const _hoisted_4 = ["disabled", "title", "aria-label"];
8
+ const _hoisted_5 = ["disabled", "title", "aria-label"];
9
+ const _hoisted_6 = ["title", "aria-label"];
10
+ const _hoisted_7 = ["disabled", "title", "aria-label"];
11
+ const _hoisted_8 = {
12
+ key: 1,
13
+ class: "pointer-events-none absolute bottom-0 right-0 rounded-tl-control bg-black/45 px-1.5 py-0.5 text-[10px] text-white/75 backdrop-blur-sm"
14
+ };
15
+ const PAN_STEP = 80;
16
+ const ZOOM_MS = 220;
17
+ const CONTROL = "flex size-8 items-center justify-center rounded-control bg-black/45 text-white/90 outline-none backdrop-blur-sm transition-[background-color,scale] duration-[var(--duration-fast)] hover:bg-black/65 hover:scale-105 focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-40";
18
+ const _sfc_main = /* @__PURE__ */ defineComponent({
19
+ __name: "LpMapPicker",
20
+ props: {
21
+ modelValue: {},
22
+ tiles: { default: () => OSM_TILES },
23
+ zoom: { default: 13 },
24
+ center: { default: () => ({ lat: 50.11, lon: 14.44 }) },
25
+ disabled: { type: Boolean },
26
+ height: { default: "20rem" },
27
+ locatable: { type: Boolean, default: true },
28
+ zoomInLabel: { default: "Zoom in" },
29
+ zoomOutLabel: { default: "Zoom out" },
30
+ locateLabel: { default: "Use my location" },
31
+ recentreLabel: { default: "Back to the pin" }
32
+ },
33
+ emits: ["update:modelValue"],
34
+ setup(__props, { expose: __expose, emit: __emit }) {
35
+ const props = __props;
36
+ const emit = __emit;
37
+ const seenTiles = /* @__PURE__ */ new Set();
38
+ const tileSize = computed(() => props.tiles.tileSize ?? 256);
39
+ const minZoom = computed(() => props.tiles.minZoom ?? 1);
40
+ const maxZoom = computed(() => props.tiles.maxZoom ?? 19);
41
+ const viewport = ref(null);
42
+ const viewWidth = ref(0);
43
+ const viewHeight = ref(0);
44
+ const level = ref(clampZoom(props.zoom));
45
+ const mapCenter = ref(props.modelValue ?? props.center);
46
+ const zooming = ref(false);
47
+ let zoomTimer;
48
+ let pruneTimer;
49
+ function clampZoom(value) {
50
+ return Math.round(Math.min(maxZoom.value, Math.max(minZoom.value, value)));
51
+ }
52
+ const origin = computed(() => {
53
+ const point = project(mapCenter.value.lat, mapCenter.value.lon, level.value, tileSize.value);
54
+ return { x: point.x - viewWidth.value / 2, y: point.y - viewHeight.value / 2 };
55
+ });
56
+ const layers = ref([]);
57
+ let nextLayerId = 1;
58
+ const active = computed(() => layers.value[layers.value.length - 1]);
59
+ function anchorFor(atLevel) {
60
+ const size = tileSize.value;
61
+ const factor = 2 ** (atLevel - level.value);
62
+ return {
63
+ x: Math.floor(origin.value.x * factor / size) * size,
64
+ y: Math.floor(origin.value.y * factor / size) * size
65
+ };
66
+ }
67
+ function pushLayer() {
68
+ layers.value = [
69
+ ...layers.value,
70
+ {
71
+ id: nextLayerId++,
72
+ level: level.value,
73
+ anchor: anchorFor(level.value),
74
+ tiles: [],
75
+ range: { firstX: 0, firstY: 0, lastX: -1, lastY: -1 }
76
+ }
77
+ ];
78
+ refreshTiles();
79
+ }
80
+ function pruneLayers() {
81
+ if (layers.value.length < 2) return;
82
+ layers.value = [layers.value[layers.value.length - 1]];
83
+ }
84
+ function reanchor() {
85
+ const layer = active.value;
86
+ if (!layer) return;
87
+ const next = anchorFor(layer.level);
88
+ const dx = layer.anchor.x - next.x;
89
+ const dy = layer.anchor.y - next.y;
90
+ if (!dx && !dy) return;
91
+ layer.anchor = next;
92
+ for (const tile of layer.tiles) {
93
+ tile.left += dx;
94
+ tile.top += dy;
95
+ }
96
+ }
97
+ function refreshTiles() {
98
+ const layer = active.value;
99
+ if (!layer || !viewWidth.value || !viewHeight.value) return;
100
+ const size = tileSize.value;
101
+ const count = 2 ** layer.level;
102
+ const firstX = Math.floor(origin.value.x / size) - 1;
103
+ const firstY = Math.floor(origin.value.y / size) - 1;
104
+ const lastX = Math.floor((origin.value.x + viewWidth.value) / size) + 1;
105
+ const lastY = Math.floor((origin.value.y + viewHeight.value) / size) + 1;
106
+ const seen = layer.range;
107
+ if (seen.firstX === firstX && seen.firstY === firstY && seen.lastX === lastX && seen.lastY === lastY) {
108
+ return;
109
+ }
110
+ layer.range = { firstX, firstY, lastX, lastY };
111
+ const previous = new Map(layer.tiles.map((tile) => [tile.key, tile]));
112
+ const next = [];
113
+ for (let y = firstY; y <= lastY; y++) {
114
+ if (y < 0 || y >= count) continue;
115
+ for (let x = firstX; x <= lastX; x++) {
116
+ const wrapped = (x % count + count) % count;
117
+ const key = `${layer.level}/${x}/${y}`;
118
+ const existing = previous.get(key);
119
+ if (existing) {
120
+ next.push(existing);
121
+ continue;
122
+ }
123
+ const url = tileUrl(props.tiles, layer.level, wrapped, y);
124
+ next.push({
125
+ key,
126
+ url,
127
+ left: x * size - layer.anchor.x,
128
+ top: y * size - layer.anchor.y,
129
+ // Anything fetched before paints at once: replaying the fade on a
130
+ // cached tile is what makes a zoom look like a reload.
131
+ loaded: seenTiles.has(url)
132
+ });
133
+ }
134
+ }
135
+ layer.tiles = next;
136
+ }
137
+ const loadingTiles = computed(() => active.value?.tiles.some((tile) => !tile.loaded) ?? false);
138
+ function onTileLoad(tile) {
139
+ tile.loaded = true;
140
+ seenTiles.add(tile.url);
141
+ if (!loadingTiles.value) pruneLayers();
142
+ }
143
+ function layerStyle(layer) {
144
+ const factor = 2 ** (layer.level - level.value);
145
+ const originX = origin.value.x * factor;
146
+ const originY = origin.value.y * factor;
147
+ return {
148
+ transform: `scale(${1 / factor}) translate3d(${layer.anchor.x - originX}px, ${layer.anchor.y - originY}px, 0)`,
149
+ transformOrigin: "0 0",
150
+ transition: zooming.value ? "transform var(--duration-medium) var(--ease-emphasized)" : void 0
151
+ };
152
+ }
153
+ const pin = computed(() => {
154
+ if (!props.modelValue) return null;
155
+ const point = project(props.modelValue.lat, props.modelValue.lon, level.value, tileSize.value);
156
+ return { left: point.x - origin.value.x, top: point.y - origin.value.y };
157
+ });
158
+ function viewportPoint(e) {
159
+ const rect = viewport.value?.getBoundingClientRect();
160
+ return rect ? { x: e.clientX - rect.left, y: e.clientY - rect.top } : { x: 0, y: 0 };
161
+ }
162
+ function toLatLon(at) {
163
+ return unproject(origin.value.x + at.x, origin.value.y + at.y, level.value, tileSize.value);
164
+ }
165
+ const panning = ref(false);
166
+ let moved = false;
167
+ let last = { x: 0, y: 0 };
168
+ let velocity = { x: 0, y: 0 };
169
+ let lastMove = 0;
170
+ let glideFrame;
171
+ function stopGlide() {
172
+ if (glideFrame !== void 0) cancelAnimationFrame(glideFrame);
173
+ glideFrame = void 0;
174
+ velocity = { x: 0, y: 0 };
175
+ }
176
+ function onPointerDown(e) {
177
+ if (props.disabled || e.button !== 0 || zooming.value) return;
178
+ stopGlide();
179
+ panning.value = true;
180
+ moved = false;
181
+ last = { x: e.clientX, y: e.clientY };
182
+ lastMove = performance.now();
183
+ e.currentTarget.setPointerCapture(e.pointerId);
184
+ }
185
+ function onPointerMove(e) {
186
+ if (!panning.value) return;
187
+ const dx = e.clientX - last.x;
188
+ const dy = e.clientY - last.y;
189
+ if (Math.abs(dx) > 2 || Math.abs(dy) > 2) moved = true;
190
+ const now = performance.now();
191
+ const dt = now - lastMove;
192
+ if (dt > 0) {
193
+ velocity = {
194
+ x: velocity.x * 0.7 + dx / dt * 0.3,
195
+ y: velocity.y * 0.7 + dy / dt * 0.3
196
+ };
197
+ }
198
+ lastMove = now;
199
+ last = { x: e.clientX, y: e.clientY };
200
+ panBy(-dx, -dy);
201
+ }
202
+ function onPointerUp(e) {
203
+ if (!panning.value) return;
204
+ panning.value = false;
205
+ e.currentTarget.releasePointerCapture?.(e.pointerId);
206
+ if (!moved && !props.disabled) {
207
+ emit("update:modelValue", toLatLon(viewportPoint(e)));
208
+ return;
209
+ }
210
+ if (performance.now() - lastMove > 90) velocity = { x: 0, y: 0 };
211
+ glide();
212
+ }
213
+ function glide() {
214
+ if (reducedMotion() || Math.hypot(velocity.x, velocity.y) < 0.05) {
215
+ velocity = { x: 0, y: 0 };
216
+ return;
217
+ }
218
+ let previous = performance.now();
219
+ const step = (now) => {
220
+ const dt = Math.min(now - previous, 32);
221
+ previous = now;
222
+ const friction = Math.exp(-dt / 240);
223
+ velocity = { x: velocity.x * friction, y: velocity.y * friction };
224
+ panBy(-velocity.x * dt, -velocity.y * dt);
225
+ if (Math.hypot(velocity.x, velocity.y) > 0.015) {
226
+ glideFrame = requestAnimationFrame(step);
227
+ } else {
228
+ stopGlide();
229
+ }
230
+ };
231
+ glideFrame = requestAnimationFrame(step);
232
+ }
233
+ function reducedMotion() {
234
+ return typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
235
+ }
236
+ function panBy(dx, dy) {
237
+ mapCenter.value = unproject(
238
+ origin.value.x + dx + viewWidth.value / 2,
239
+ origin.value.y + dy + viewHeight.value / 2,
240
+ level.value,
241
+ tileSize.value
242
+ );
243
+ refreshTiles();
244
+ }
245
+ function zoomBy(delta, at) {
246
+ if (props.disabled) return;
247
+ const target = clampZoom(level.value + delta);
248
+ if (target === level.value) return;
249
+ stopGlide();
250
+ const point = at ?? { x: viewWidth.value / 2, y: viewHeight.value / 2 };
251
+ const ground = toLatLon(point);
252
+ const world = project(ground.lat, ground.lon, target, tileSize.value);
253
+ level.value = target;
254
+ mapCenter.value = unproject(
255
+ world.x - point.x + viewWidth.value / 2,
256
+ world.y - point.y + viewHeight.value / 2,
257
+ target,
258
+ tileSize.value
259
+ );
260
+ zooming.value = true;
261
+ pushLayer();
262
+ clearTimeout(zoomTimer);
263
+ zoomTimer = setTimeout(() => zooming.value = false, ZOOM_MS);
264
+ clearTimeout(pruneTimer);
265
+ pruneTimer = setTimeout(pruneLayers, 1200);
266
+ }
267
+ function onWheel(e) {
268
+ if (props.disabled) return;
269
+ e.preventDefault();
270
+ zoomBy(e.deltaY < 0 ? 1 : -1, viewportPoint(e));
271
+ }
272
+ function onDoubleClick(e) {
273
+ zoomBy(1, viewportPoint(e));
274
+ }
275
+ function onKeydown(e) {
276
+ if (props.disabled) return;
277
+ switch (e.key) {
278
+ case "ArrowUp":
279
+ e.preventDefault();
280
+ panBy(0, -PAN_STEP);
281
+ break;
282
+ case "ArrowDown":
283
+ e.preventDefault();
284
+ panBy(0, PAN_STEP);
285
+ break;
286
+ case "ArrowLeft":
287
+ e.preventDefault();
288
+ panBy(-PAN_STEP, 0);
289
+ break;
290
+ case "ArrowRight":
291
+ e.preventDefault();
292
+ panBy(PAN_STEP, 0);
293
+ break;
294
+ case "+":
295
+ case "=":
296
+ e.preventDefault();
297
+ zoomBy(1);
298
+ break;
299
+ case "-":
300
+ e.preventDefault();
301
+ zoomBy(-1);
302
+ break;
303
+ case "0":
304
+ e.preventDefault();
305
+ recentre();
306
+ break;
307
+ }
308
+ }
309
+ const canRecentre = computed(() => {
310
+ if (!pin.value) return false;
311
+ const { left, top } = pin.value;
312
+ return left < 0 || top < 0 || left > viewWidth.value || top > viewHeight.value;
313
+ });
314
+ function recentre() {
315
+ if (!props.modelValue) return;
316
+ stopGlide();
317
+ mapCenter.value = props.modelValue;
318
+ reanchor();
319
+ refreshTiles();
320
+ }
321
+ const locating = ref(false);
322
+ function locate() {
323
+ if (!navigator.geolocation || locating.value) return;
324
+ locating.value = true;
325
+ navigator.geolocation.getCurrentPosition(
326
+ (position) => {
327
+ locating.value = false;
328
+ const next = { lat: position.coords.latitude, lon: position.coords.longitude };
329
+ emit("update:modelValue", next);
330
+ mapCenter.value = next;
331
+ reanchor();
332
+ refreshTiles();
333
+ },
334
+ () => {
335
+ locating.value = false;
336
+ },
337
+ { enableHighAccuracy: true, timeout: 1e4 }
338
+ );
339
+ }
340
+ watch(
341
+ () => props.modelValue,
342
+ (value) => {
343
+ if (!value || panning.value) return;
344
+ mapCenter.value = value;
345
+ if (level.value < props.zoom) {
346
+ level.value = clampZoom(props.zoom);
347
+ pushLayer();
348
+ return;
349
+ }
350
+ reanchor();
351
+ refreshTiles();
352
+ }
353
+ );
354
+ watch(
355
+ () => props.tiles,
356
+ () => {
357
+ layers.value = [];
358
+ pushLayer();
359
+ }
360
+ );
361
+ let observer;
362
+ onMounted(() => {
363
+ const el = viewport.value;
364
+ if (!el) return;
365
+ observer = new ResizeObserver(([entry]) => {
366
+ viewWidth.value = entry.contentRect.width;
367
+ viewHeight.value = entry.contentRect.height;
368
+ if (!layers.value.length) {
369
+ pushLayer();
370
+ return;
371
+ }
372
+ reanchor();
373
+ refreshTiles();
374
+ });
375
+ observer.observe(el);
376
+ });
377
+ onBeforeUnmount(() => {
378
+ observer?.disconnect();
379
+ clearTimeout(zoomTimer);
380
+ clearTimeout(pruneTimer);
381
+ stopGlide();
382
+ });
383
+ __expose({ zoomIn: () => zoomBy(1), zoomOut: () => zoomBy(-1), recentre, locate });
384
+ return (_ctx, _cache) => {
385
+ return openBlock(), createElementBlock("div", {
386
+ class: "relative overflow-hidden rounded-control border border-line bg-surface-soft",
387
+ style: normalizeStyle({ height: __props.height })
388
+ }, [
389
+ createElementVNode("div", {
390
+ ref_key: "viewport",
391
+ ref: viewport,
392
+ class: normalizeClass(["absolute inset-0 touch-none select-none outline-none focus-visible:ring-2 focus-visible:ring-ring", __props.disabled ? "cursor-not-allowed opacity-55" : panning.value ? "cursor-grabbing" : "cursor-grab"]),
393
+ tabindex: __props.disabled ? -1 : 0,
394
+ role: "application",
395
+ "aria-label": __props.recentreLabel,
396
+ onPointerdown: onPointerDown,
397
+ onPointermove: onPointerMove,
398
+ onPointerup: onPointerUp,
399
+ onPointercancel: onPointerUp,
400
+ onWheel,
401
+ onDblclick: onDoubleClick,
402
+ onKeydown
403
+ }, [
404
+ (openBlock(true), createElementBlock(Fragment, null, renderList(layers.value, (layer) => {
405
+ return openBlock(), createElementBlock("div", {
406
+ key: layer.id,
407
+ class: "absolute left-0 top-0 will-change-transform motion-reduce:transition-none",
408
+ style: normalizeStyle(layerStyle(layer))
409
+ }, [
410
+ (openBlock(true), createElementBlock(Fragment, null, renderList(layer.tiles, (tile) => {
411
+ return openBlock(), createElementBlock("img", {
412
+ key: tile.key,
413
+ src: tile.url,
414
+ alt: "",
415
+ draggable: "false",
416
+ class: normalizeClass(["absolute max-w-none", tile.loaded ? "" : "animate-[map-tile-in_var(--duration-medium)_ease] motion-reduce:animate-none"]),
417
+ style: normalizeStyle({
418
+ left: `${tile.left}px`,
419
+ top: `${tile.top}px`,
420
+ width: `${tileSize.value}px`,
421
+ height: `${tileSize.value}px`
422
+ }),
423
+ onLoad: ($event) => onTileLoad(tile)
424
+ }, null, 46, _hoisted_2);
425
+ }), 128))
426
+ ], 4);
427
+ }), 128)),
428
+ pin.value ? (openBlock(), createElementBlock("span", {
429
+ key: 0,
430
+ class: "pointer-events-none absolute",
431
+ style: normalizeStyle({ left: `${pin.value.left}px`, top: `${pin.value.top}px`, transform: "translate(-50%, -100%)" })
432
+ }, [
433
+ createVNode(_sfc_main$1, {
434
+ name: "lucide:map-pin",
435
+ size: 30,
436
+ class: "block animate-[map-pin-in_var(--duration-medium)_var(--ease-emphasized)] text-danger drop-shadow-[0_2px_4px_rgba(0,0,0,0.45)] motion-reduce:animate-none"
437
+ })
438
+ ], 4)) : createCommentVNode("", true)
439
+ ], 42, _hoisted_1),
440
+ createElementVNode("div", _hoisted_3, [
441
+ createElementVNode("button", {
442
+ class: normalizeClass(CONTROL),
443
+ type: "button",
444
+ disabled: __props.disabled || level.value >= maxZoom.value,
445
+ title: __props.zoomInLabel,
446
+ "aria-label": __props.zoomInLabel,
447
+ onClick: _cache[0] || (_cache[0] = ($event) => zoomBy(1))
448
+ }, [
449
+ createVNode(_sfc_main$1, {
450
+ name: "lucide:plus",
451
+ size: 16
452
+ })
453
+ ], 8, _hoisted_4),
454
+ createElementVNode("button", {
455
+ class: normalizeClass(CONTROL),
456
+ type: "button",
457
+ disabled: __props.disabled || level.value <= minZoom.value,
458
+ title: __props.zoomOutLabel,
459
+ "aria-label": __props.zoomOutLabel,
460
+ onClick: _cache[1] || (_cache[1] = ($event) => zoomBy(-1))
461
+ }, [
462
+ createVNode(_sfc_main$1, {
463
+ name: "lucide:minus",
464
+ size: 16
465
+ })
466
+ ], 8, _hoisted_5),
467
+ canRecentre.value ? (openBlock(), createElementBlock("button", {
468
+ key: 0,
469
+ class: normalizeClass(CONTROL),
470
+ type: "button",
471
+ title: __props.recentreLabel,
472
+ "aria-label": __props.recentreLabel,
473
+ onClick: recentre
474
+ }, [
475
+ createVNode(_sfc_main$1, {
476
+ name: "lucide:locate-fixed",
477
+ size: 16
478
+ })
479
+ ], 8, _hoisted_6)) : createCommentVNode("", true),
480
+ __props.locatable ? (openBlock(), createElementBlock("button", {
481
+ key: 1,
482
+ class: normalizeClass(CONTROL),
483
+ type: "button",
484
+ disabled: __props.disabled || locating.value,
485
+ title: __props.locateLabel,
486
+ "aria-label": __props.locateLabel,
487
+ onClick: locate
488
+ }, [
489
+ createVNode(_sfc_main$1, {
490
+ name: locating.value ? "lucide:loader-circle" : "lucide:crosshair",
491
+ size: 16,
492
+ class: normalizeClass(locating.value ? "animate-spin" : "")
493
+ }, null, 8, ["name", "class"])
494
+ ], 8, _hoisted_7)) : createCommentVNode("", true)
495
+ ]),
496
+ loadingTiles.value ? (openBlock(), createBlock(_sfc_main$1, {
497
+ key: 0,
498
+ name: "lucide:loader-circle",
499
+ size: 16,
500
+ class: "pointer-events-none absolute left-2 top-2 animate-spin text-white/70 drop-shadow-[0_1px_2px_rgba(0,0,0,0.6)]"
501
+ })) : createCommentVNode("", true),
502
+ props.tiles.attribution ? (openBlock(), createElementBlock("p", _hoisted_8, toDisplayString(props.tiles.attribution), 1)) : createCommentVNode("", true)
503
+ ], 4);
504
+ };
505
+ }
506
+ });
507
+ export {
508
+ _sfc_main as _
509
+ };
@@ -1,4 +1,4 @@
1
- import { defineComponent, computed, useSlots, openBlock, createBlock, unref, withCtx, createVNode, createElementVNode, normalizeStyle, normalizeClass, createElementBlock, renderSlot, createTextVNode, toDisplayString, createCommentVNode } from "vue";
1
+ import { defineComponent, computed, useSlots, 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";
@@ -26,6 +26,9 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
26
26
  emits: ["update:open"],
27
27
  setup(__props) {
28
28
  const props = __props;
29
+ const describedByAttrs = computed(
30
+ () => props.description ? {} : { "aria-describedby": void 0 }
31
+ );
29
32
  const widthClass = computed(() => {
30
33
  if (props.width) return "";
31
34
  return {
@@ -56,10 +59,10 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
56
59
  default: withCtx(() => [
57
60
  createVNode(unref(DialogOverlay), { class: "fixed inset-0 z-(--z-overlay) bg-black/50 backdrop-blur-sm data-[state=open]:animate-[fade-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[fade-out_120ms_ease]" }),
58
61
  createElementVNode("div", _hoisted_1, [
59
- createVNode(unref(DialogContent), {
60
- class: normalizeClass(["pointer-events-auto flex max-h-[min(90vh,calc(100dvh-2rem))] min-h-0 flex-col 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]),
61
- style: normalizeStyle(__props.width ? { width: __props.width } : void 0)
62
- }, {
62
+ createVNode(unref(DialogContent), mergeProps({
63
+ class: ["pointer-events-auto flex max-h-[min(90vh,calc(100dvh-2rem))] min-h-0 flex-col 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],
64
+ style: __props.width ? { width: __props.width } : void 0
65
+ }, describedByAttrs.value), {
63
66
  default: withCtx(() => [
64
67
  __props.title || _ctx.$slots.title ? (openBlock(), createElementBlock("header", _hoisted_2, [
65
68
  createElementVNode("div", _hoisted_3, [
@@ -115,7 +118,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
115
118
  ])) : createCommentVNode("", true)
116
119
  ]),
117
120
  _: 3
118
- }, 8, ["class", "style"])
121
+ }, 16, ["class", "style"])
119
122
  ])
120
123
  ]),
121
124
  _: 3
@@ -1,4 +1,4 @@
1
- import { defineComponent, useModel, computed, openBlock, createBlock, withCtx, createElementVNode, toDisplayString, createElementBlock, createCommentVNode, Fragment, renderList, resolveDynamicComponent, normalizeClass, createVNode, createTextVNode, mergeModels } from "vue";
1
+ import { defineComponent, computed, useModel, openBlock, createBlock, withCtx, createElementVNode, toDisplayString, createElementBlock, createCommentVNode, Fragment, renderList, resolveDynamicComponent, normalizeClass, createVNode, createTextVNode, mergeModels } from "vue";
2
2
  import { _ as _sfc_main$7 } from "./LpBadge-CXzBPnwO.js";
3
3
  import { _ as _sfc_main$6 } from "./LpButton-5VXXCjwF.js";
4
4
  import { _ as _sfc_main$4 } from "./LpContextMenu-D0mzXqA7.js";
@@ -35,7 +35,9 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
35
35
  emptyLabel: {},
36
36
  title: {},
37
37
  maxBadge: { default: 9 },
38
- menuItems: {}
38
+ menuItems: {},
39
+ labels: {},
40
+ formatTime: {}
39
41
  }, {
40
42
  "open": { type: Boolean, ...{ default: false } },
41
43
  "openModifiers": {}
@@ -43,6 +45,16 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
43
45
  emits: /* @__PURE__ */ mergeModels(["markRead", "markUnread", "markAllRead", "dismiss", "select"], ["update:open"]),
44
46
  setup(__props, { emit: __emit }) {
45
47
  const props = __props;
48
+ const l = computed(() => ({
49
+ title: props.title ?? props.labels?.title ?? "Notifications",
50
+ markAllRead: props.labels?.markAllRead ?? "Mark all read",
51
+ markAsRead: props.labels?.markAsRead ?? "Mark as read",
52
+ markAsUnread: props.labels?.markAsUnread ?? "Mark as unread",
53
+ open: props.labels?.open ?? "Open",
54
+ dismiss: props.labels?.dismiss ?? "Dismiss",
55
+ empty: props.emptyLabel ?? props.labels?.empty ?? "No notifications",
56
+ ariaLabel: props.labels?.ariaLabel ?? props.labels?.title ?? "Notifications"
57
+ }));
46
58
  const open = useModel(__props, "open");
47
59
  const emit = __emit;
48
60
  const unread = computed(
@@ -55,7 +67,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
55
67
  const contextMenu = computed(
56
68
  () => props.menuItems ?? [
57
69
  {
58
- label: "Mark all read",
70
+ label: l.value.markAllRead,
59
71
  icon: "lucide:check-check",
60
72
  disabled: !hasUnread.value,
61
73
  onSelect: () => emit("markAllRead")
@@ -70,26 +82,26 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
70
82
  const actions = [];
71
83
  if (item.read) {
72
84
  actions.push({
73
- label: "Mark as unread",
85
+ label: l.value.markAsUnread,
74
86
  icon: "lucide:dot",
75
87
  onSelect: () => emit("markUnread", item.id)
76
88
  });
77
89
  } else {
78
90
  actions.push({
79
- label: "Mark as read",
91
+ label: l.value.markAsRead,
80
92
  icon: "lucide:check",
81
93
  onSelect: () => emit("markRead", item.id)
82
94
  });
83
95
  }
84
96
  if (item.link) {
85
97
  actions.push({
86
- label: "Open",
98
+ label: l.value.open,
87
99
  icon: "lucide:external-link",
88
100
  onSelect: () => emit("select", item)
89
101
  });
90
102
  }
91
103
  actions.push({
92
- label: "Dismiss",
104
+ label: l.value.dismiss,
93
105
  icon: "lucide:x",
94
106
  danger: true,
95
107
  separatorBefore: true,
@@ -99,6 +111,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
99
111
  }
100
112
  function timeAgo(iso) {
101
113
  if (!iso) return "";
114
+ if (props.formatTime) return props.formatTime(iso);
102
115
  const then = new Date(iso).getTime();
103
116
  if (Number.isNaN(then)) return "";
104
117
  const secs = Math.max(0, Math.round((Date.now() - then) / 1e3));
@@ -127,7 +140,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
127
140
  variant: "ghost",
128
141
  size: "sm",
129
142
  class: "relative",
130
- "aria-label": "Notifications",
143
+ "aria-label": l.value.ariaLabel,
131
144
  onClick: _cache[0] || (_cache[0] = ($event) => open.value = !open.value)
132
145
  }, {
133
146
  default: withCtx(() => [
@@ -147,20 +160,20 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
147
160
  })) : createCommentVNode("", true)
148
161
  ]),
149
162
  _: 1
150
- })
163
+ }, 8, ["aria-label"])
151
164
  ]),
152
165
  _: 1
153
166
  }, 8, ["items"])
154
167
  ]),
155
168
  default: withCtx(() => [
156
169
  createElementVNode("header", _hoisted_1, [
157
- createElementVNode("span", _hoisted_2, toDisplayString(__props.title ?? "Notifications"), 1),
170
+ createElementVNode("span", _hoisted_2, toDisplayString(l.value.title), 1),
158
171
  hasUnread.value ? (openBlock(), createElementBlock("button", {
159
172
  key: 0,
160
173
  type: "button",
161
174
  class: "rounded-md px-1.5 py-0.5 text-xs font-medium text-brand outline-none transition-colors hover:bg-brand-soft focus-visible:bg-brand-soft",
162
175
  onClick: _cache[1] || (_cache[1] = ($event) => emit("markAllRead"))
163
- }, " Mark all read ")) : createCommentVNode("", true)
176
+ }, toDisplayString(l.value.markAllRead), 1)) : createCommentVNode("", true)
164
177
  ]),
165
178
  __props.loading ? (openBlock(), createElementBlock("div", _hoisted_3, [
166
179
  (openBlock(), createElementBlock(Fragment, null, renderList(4, (i) => {
@@ -172,7 +185,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
172
185
  ])) : !__props.items.length ? (openBlock(), createBlock(_sfc_main$2, {
173
186
  key: 1,
174
187
  icon: "lucide:bell-off",
175
- title: __props.emptyLabel ?? "No notifications",
188
+ title: l.value.empty,
176
189
  class: "px-3 py-10"
177
190
  }, null, 8, ["title"])) : (openBlock(), createBlock(_sfc_main$3, {
178
191
  key: 2,
@@ -60,6 +60,9 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
60
60
  else localQuery.value = v;
61
61
  }
62
62
  });
63
+ function onOpenChange(_open) {
64
+ query.value = "";
65
+ }
63
66
  function encode(v) {
64
67
  return v === "" ? EMPTY_SENTINEL : v;
65
68
  }
@@ -108,7 +111,8 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
108
111
  disabled: __props.disabled,
109
112
  "ignore-filter": true,
110
113
  class: "relative",
111
- "onUpdate:modelValue": onUpdate
114
+ "onUpdate:modelValue": onUpdate,
115
+ "onUpdate:open": onOpenChange
112
116
  }, {
113
117
  default: withCtx(() => [
114
118
  createVNode(unref(ComboboxAnchor), {