@necrolab/dashboard 0.5.21 → 0.5.23

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,90 @@
1
+ const manipulateHexColor = (p, c0, c1, l) => {
2
+ let r,
3
+ g,
4
+ b,
5
+ P,
6
+ f,
7
+ t,
8
+ h,
9
+ i = parseInt,
10
+ m = Math.round,
11
+ a = typeof c1 == "string";
12
+ if (
13
+ typeof p != "number" ||
14
+ p < -1 ||
15
+ p > 1 ||
16
+ typeof c0 != "string" ||
17
+ (c0[0] != "r" && c0[0] != "#") ||
18
+ (c1 && !a)
19
+ )
20
+ return null;
21
+ if (!this.pSBCr)
22
+ this.pSBCr = (d) => {
23
+ let n = d.length,
24
+ x = {};
25
+ if (n > 9) {
26
+ ([r, g, b, a] = d = d.split(",")), (n = d.length);
27
+ if (n < 3 || n > 4) return null;
28
+ (x.r = i(r[3] == "a" ? r.slice(5) : r.slice(4))),
29
+ (x.g = i(g)),
30
+ (x.b = i(b)),
31
+ (x.a = a ? parseFloat(a) : -1);
32
+ } else {
33
+ if (n == 8 || n == 6 || n < 4) return null;
34
+ if (n < 6) d = "#" + d[1] + d[1] + d[2] + d[2] + d[3] + d[3] + (n > 4 ? d[4] + d[4] : "");
35
+ d = i(d.slice(1), 16);
36
+ if (n == 9 || n == 5)
37
+ (x.r = (d >> 24) & 255),
38
+ (x.g = (d >> 16) & 255),
39
+ (x.b = (d >> 8) & 255),
40
+ (x.a = m((d & 255) / 0.255) / 1000);
41
+ else (x.r = d >> 16), (x.g = (d >> 8) & 255), (x.b = d & 255), (x.a = -1);
42
+ }
43
+ return x;
44
+ };
45
+ (h = c0.length > 9),
46
+ (h = a ? (c1.length > 9 ? true : c1 == "c" ? !h : false) : h),
47
+ (f = this.pSBCr(c0)),
48
+ (P = p < 0),
49
+ (t = c1 && c1 != "c" ? this.pSBCr(c1) : P ? { r: 0, g: 0, b: 0, a: -1 } : { r: 255, g: 255, b: 255, a: -1 }),
50
+ (p = P ? p * -1 : p),
51
+ (P = 1 - p);
52
+ if (!f || !t) return null;
53
+ if (l) (r = m(P * f.r + p * t.r)), (g = m(P * f.g + p * t.g)), (b = m(P * f.b + p * t.b));
54
+ else
55
+ (r = m((P * f.r ** 2 + p * t.r ** 2) ** 0.5)),
56
+ (g = m((P * f.g ** 2 + p * t.g ** 2) ** 0.5)),
57
+ (b = m((P * f.b ** 2 + p * t.b ** 2) ** 0.5));
58
+ (a = f.a), (t = t.a), (f = a >= 0 || t >= 0), (a = f ? (a < 0 ? t : t < 0 ? a : a * P + t * p) : 0);
59
+ if (h) return "rgb" + (f ? "a(" : "(") + r + "," + g + "," + b + (f ? "," + m(a * 1000) / 1000 : "") + ")";
60
+ else
61
+ return (
62
+ "#" +
63
+ (4294967296 + r * 16777216 + g * 65536 + b * 256 + (f ? m(a * 255) : 0))
64
+ .toString(16)
65
+ .slice(1, f ? undefined : -2)
66
+ );
67
+ };
68
+
69
+ const pinIcon = `
70
+ <g>
71
+ <path d="m 0,0 c -1.25,1.34 -2,3.3 -2,5.14 0,3.85 1.8,5.3 4.56,10.6 1,2.32 2,4.8 3,8.89 0.137,0.6 0.27,1.16 0.33,1.2 0,0 0.2,-0.51 0.33,-1.11 1,-4 2,-6.54 3,-8.87 2.77,-5.3 4.5,-6.75 4.56,-10.6 0,-1.83 -0.75,-3.8 -2,-5.14 -1.43,-1.53 -3.6,-2.66 -5.9,-2.71 -2.31,0 -4.5,1 -5.92,2.62 z" style="display:inline;opacity:1;fill:#ff4646;fill-opacity:1;stroke:#d73534;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"></path>
72
+ <circle r="3.5" cy="5.3" cx="5.7" style="display:inline;opacity:1;fill:#590000;fill-opacity:1;stroke-width:0"></circle>
73
+ </g>
74
+ `;
75
+
76
+ const marshalAttributes = (attrs) => {
77
+ let out = [];
78
+ Object.entries(attrs).forEach(([k, v]) => k && v && out.push(`${k}="${v}"`));
79
+ return out.join(" ");
80
+ };
81
+
82
+ export const createElement = (tag, attrs, body = "") => `<${tag} ${marshalAttributes(attrs)}>${body}</${tag}>\n`;
83
+ export { manipulateHexColor };
84
+ export { pinIcon };
85
+ export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
86
+ export const binder = (functionsObject, thisClass) => {
87
+ for (let [functionKey, functionValue] of Object.entries(functionsObject)) {
88
+ thisClass[functionKey] = functionValue.bind(thisClass);
89
+ }
90
+ };
@@ -1,18 +1,19 @@
1
1
  <template>
2
2
  <div class="flex w-full flex-col">
3
- <div class="page-header">
3
+ <div class="page-header flex-wrap gap-3">
4
4
  <div class="page-header-card flex-shrink-0">
5
5
  <FilterIcon />
6
6
  <h4>Filter creator</h4>
7
7
  </div>
8
- <div class="unified-search-group flex w-auto items-center">
8
+ <div class="unified-search-group flex w-full flex-1 items-center md:w-auto md:flex-initial md:min-w-64">
9
9
  <input
10
- class="h-10 w-32 flex-1 px-3 text-sm text-white placeholder-light-500 sm:w-48 md:w-64"
10
+ class="flex-1 h-10 px-3 text-sm text-white placeholder-light-500"
11
11
  placeholder="Event ID"
12
12
  v-model="eventId"
13
13
  aria-label="Event ID" />
14
14
  <button
15
- class="transition-standard hover:bg-dark-450 flex h-10 w-9 flex-shrink-0 items-center justify-center bg-dark-400 text-white"
15
+ class="hover:bg-dark-450 flex h-10 w-10 flex-shrink-0 items-center justify-center bg-dark-400 text-white"
16
+ style="transition: all 0.2s ease"
16
17
  @click="updateShownVenue"
17
18
  aria-label="Load venue">
18
19
  <ReloadIcon class="icon-md" />
@@ -21,26 +22,29 @@
21
22
  </div>
22
23
 
23
24
  <div
24
- class="mb-3 flex flex-1 flex-col overflow-hidden rounded border border-dark-650 bg-dark-400 p-3 shadow-sm md:mb-4">
25
- <div class="flex h-full w-full flex-col gap-3 lg:flex-row lg:gap-4">
25
+ class="mb-3 flex flex-1 flex-col overflow-hidden rounded border border-dark-650 bg-dark-400 p-2 shadow-sm md:p-3 md:mb-4"
26
+ style="max-height: calc(100vh - 200px);">
27
+ <div class="flex h-full w-full flex-col gap-2 md:gap-3 lg:flex-row lg:gap-4">
26
28
  <div
27
- class="min-h-75 lg:min-h-125 relative flex w-full min-w-0 flex-col overflow-hidden rounded-lg lg:w-3/5">
28
- <div v-if="svg" class="flex-gap-2 mb-2 items-center">
29
+ class="relative flex w-full min-w-0 flex-col overflow-hidden rounded-lg lg:flex-1 lg:w-3/5 svg-container"
30
+ :class="{ 'has-svg': svg }">
31
+ <div v-if="svg" class="flex-gap-2 mb-2 flex-shrink-0 items-center">
29
32
  <button @click="handleZoom(true)" class="btn-icon-small" aria-label="Zoom in">+</button>
30
33
  <button @click="handleZoom(false)" class="btn-icon-small" aria-label="Zoom out">-</button>
31
34
  <button @click="handleZoom('r')" class="btn-icon-small" aria-label="Reset zoom">
32
35
  <ReloadIcon class="icon-md text-white" />
33
36
  </button>
34
37
  </div>
35
- <div class="selecto-wrapper flex-1 overflow-hidden">
38
+ <div class="selecto-wrapper flex-1 overflow-hidden" style="max-height: 100%;">
36
39
  <div
37
40
  v-if="svg"
38
- class="hidden-scrollbars min-h-87.5 relative h-full w-full overflow-auto rounded border border-dark-550 bg-dark-500 p-2 shadow">
41
+ class="hidden-scrollbars relative h-full w-full overflow-auto rounded border border-dark-550 bg-dark-500 p-1 md:p-2 shadow">
39
42
  <div class="svg-wrapper" id="svg-wrapper" v-html="svg"></div>
40
43
  </div>
41
44
  <div
42
45
  v-else
43
- class="min-h-87.5 relative flex h-full w-full items-center justify-center rounded border border-dark-550 bg-dark-500 p-2 shadow">
46
+ class="relative flex h-full w-full items-center justify-center rounded border border-dark-550 bg-dark-500 p-2 shadow"
47
+ style="min-height: 200px;">
44
48
  <div class="text-center">
45
49
  <FilterIcon class="empty-state-icon mx-auto" />
46
50
  <p class="text-sm text-light-400">No Map</p>
@@ -51,8 +55,8 @@
51
55
  </div>
52
56
  </div>
53
57
  </div>
54
- <div class="min-h-75 lg:min-h-125 flex w-full min-w-0 flex-col lg:w-2/5">
55
- <div class="mb-2 flex flex-shrink-0 flex-wrap items-center gap-2 text-white" v-if="hasLoaded">
58
+ <div class="flex w-full min-w-0 flex-col lg:flex-1 lg:w-2/5 filters-container">
59
+ <div class="mb-2 flex flex-shrink-0 flex-wrap items-center gap-1.5 md:gap-2 text-white text-xs md:text-sm" v-if="hasLoaded">
56
60
  <PriceSortToggle
57
61
  :current="filterBuilder.globalFilter.priceSort"
58
62
  class="smooth-hover h-8"
@@ -71,8 +75,8 @@
71
75
  placeholder="999" />
72
76
  </div>
73
77
  <div
74
- class="flex min-h-0 flex-1 flex-col overflow-hidden rounded-lg border border-dark-550 bg-dark-500 shadow-sm">
75
- <div class="flex-shrink-0 border-b border-dark-550 bg-dark-300 px-4 py-3 text-xs text-white">
78
+ class="flex min-h-0 flex-1 flex-col overflow-hidden rounded-lg border border-dark-550 bg-dark-500 shadow-sm filters-card">
79
+ <div class="flex-shrink-0 border-b border-dark-550 bg-dark-300 px-2 md:px-4 py-2 md:py-3 text-xs text-white">
76
80
  <div class="flex w-full items-center justify-between gap-2">
77
81
  <div class="flex-gap-2 items-center">
78
82
  <span class="text-sm font-medium text-white">Filters</span>
@@ -98,26 +102,21 @@
98
102
  </div>
99
103
  </div>
100
104
  </div>
101
- <div class="hidden-scrollbars flex-1 overflow-auto bg-dark-400">
102
- <draggable
105
+ <div class="hidden-scrollbars flex-1 overflow-y-auto overflow-x-hidden bg-dark-400" style="max-height: 100%; min-height: 150px;">
106
+ <div
103
107
  v-if="filterBuilder.filters.length"
104
- v-model="draggableFilters"
105
- handle=".handle"
106
- item-key="id"
107
- tag="div"
108
- class="space-y-0 p-1"
109
- ghost-class="opacity-30 border border-dark-550 bg-dark-550/10"
110
- drag-class="z-50 shadow-xl"
111
- :animation="200">
112
- <template #item="{ element: f, index: i }">
113
- <Filter
114
- v-show="doesFilterShow(f)"
115
- :filter="f"
116
- :index="i"
117
- :filterBuilder="filterBuilder"
118
- class="!p-1 !text-xs" />
119
- </template>
120
- </draggable>
108
+ ref="filtersListRef"
109
+ class="space-y-0 p-1">
110
+ <Filter
111
+ v-for="(f, i) in filterBuilder.filters"
112
+ v-show="doesFilterShow(f)"
113
+ :key="f.id"
114
+ :filter="f"
115
+ :index="i"
116
+ :filterBuilder="filterBuilder"
117
+ :color="getFilterColor(i)"
118
+ class="!p-1 !text-xs" />
119
+ </div>
121
120
  <div v-else class="empty-state flex flex-col items-center justify-center py-8 text-center">
122
121
  <FilterIcon class="empty-state-icon" />
123
122
  <p class="text-sm text-light-400">No filters yet</p>
@@ -129,9 +128,10 @@
129
128
  <button
130
129
  @click="addWildcardFilter"
131
130
  :disabled="hasWildcardFilter"
132
- class="filter-wildcard-btn"
131
+ class="filter-action-btn"
133
132
  :title="hasWildcardFilter ? 'Wildcard filter already exists' : 'Add wildcard filter'">
134
- * Wildcard
133
+ <WildcardIcon class="h-3 w-3 flex-shrink-0" />
134
+ <span>* Wildcard</span>
135
135
  </button>
136
136
  <button @click="ui.toggleModal('preview-filter')" class="filter-action-btn">
137
137
  <CameraIcon class="h-3 w-3" />
@@ -149,17 +149,19 @@
149
149
  </template>
150
150
 
151
151
  <script setup>
152
- import draggable from "vuedraggable";
153
- import { ref, computed, defineAsyncComponent, watch, nextTick } from "vue";
152
+ import Draggable from "vuedraggable";
153
+ import Sortable from "sortablejs";
154
+ import { ref, computed, defineAsyncComponent, watch, nextTick, onMounted } from "vue";
154
155
  import { useFilterCSS } from "@/composables/useFilterCSS";
155
156
  import Filter from "@/components/Filter/Filter.vue";
156
- import { FilterIcon } from "@/components/icons";
157
+ import { FilterIcon, WildcardIcon } from "@/components/icons";
157
158
  import { ReloadIcon, TrashIcon, EditIcon, CameraIcon } from "@/components/icons";
158
159
  import { sendSaveFilter } from "@/stores/requests";
159
160
  import PriceSortToggle from "@/components/Filter/PriceSortToggle.vue";
160
161
  import { useUIStore } from "@/stores/ui";
161
162
  import FilterBuilder from "@/libs/Filter";
162
163
  import panzoom from "@/libs/panzoom.js";
164
+ import RendererFactory from "@/libs/tm-renderer/index.js";
163
165
 
164
166
  // Lazy-loaded modal component
165
167
  const FilterPreview = defineAsyncComponent(() => import("@/components/Filter/FilterPreview.vue"));
@@ -190,6 +192,35 @@ const draggableFilters = computed({
190
192
  get: () => filterBuilder.value.filters,
191
193
  set: (value) => {
192
194
  filterBuilder.value.filters = value;
195
+ filterBuilder.value.updateCss();
196
+ filterBuilder.value.updateHooks.forEach((fn) => fn());
197
+ }
198
+ });
199
+
200
+ const filtersListRef = ref(null);
201
+ let sortableInstance = null;
202
+
203
+ // Initialize Sortable on the filters list
204
+ watch(filtersListRef, (el) => {
205
+ if (el && !sortableInstance) {
206
+ sortableInstance = new Sortable(el, {
207
+ handle: '.handle',
208
+ animation: 200,
209
+ ghostClass: 'sortable-ghost',
210
+ dragClass: 'sortable-drag',
211
+ onEnd: (evt) => {
212
+ const { oldIndex, newIndex } = evt;
213
+ if (oldIndex !== newIndex) {
214
+ // Reorder the filters array
215
+ const filters = [...filterBuilder.value.filters];
216
+ const [movedItem] = filters.splice(oldIndex, 1);
217
+ filters.splice(newIndex, 0, movedItem);
218
+ filterBuilder.value.filters = filters;
219
+ filterBuilder.value.updateCss();
220
+ filterBuilder.value.updateHooks.forEach((fn) => fn());
221
+ }
222
+ }
223
+ });
193
224
  }
194
225
  });
195
226
 
@@ -205,8 +236,6 @@ const addWildcardFilter = () => {
205
236
  }
206
237
  };
207
238
 
208
- let RendererFactory = import("@necrolab/tm-renderer");
209
-
210
239
  useFilterCSS(filterBuilder, svg);
211
240
 
212
241
  const doesFilterShow = (filter) => {
@@ -216,13 +245,30 @@ const doesFilterShow = (filter) => {
216
245
  return shownFilters.value === filterState;
217
246
  };
218
247
 
219
- let rendererFactory;
220
- RendererFactory.then((r) => {
221
- rendererFactory = new r.default();
222
- rendererFactory.init();
223
- }).catch((error) => {
224
- ui.logger.Error("Failed to initialize renderer:", error);
225
- });
248
+ // Color palette for unique filter colors (same as Filter.js)
249
+ const filterColorPalette = [
250
+ "#7bc999", "#6ba8d6", "#d4a876", "#a88bc9", "#e89ba3",
251
+ "#7dc9c9", "#c9a87b", "#8bc99a", "#9ba8c9", "#c9b88b"
252
+ ];
253
+
254
+ const getFilterColor = (index) => {
255
+ return filterColorPalette[index % filterColorPalette.length];
256
+ };
257
+
258
+ // Initialize renderer factory
259
+ const rendererFactory = new RendererFactory();
260
+ let rendererReady = false;
261
+
262
+ // Initialize renderer and mark as ready
263
+ (async () => {
264
+ try {
265
+ await rendererFactory.init();
266
+ rendererReady = true;
267
+ ui.logger.Info("Renderer initialized successfully");
268
+ } catch (error) {
269
+ ui.logger.Error("Failed to initialize renderer:", error);
270
+ }
271
+ })();
226
272
 
227
273
  const updateShownVenue = async () => {
228
274
  eventId.value = eventId.value.trim();
@@ -254,19 +300,26 @@ const updateShownVenue = async () => {
254
300
  }
255
301
 
256
302
  try {
257
- if (!rendererFactory) {
258
- ui.showError("Renderer not initialized yet. Please wait and try again.");
303
+ if (!rendererFactory || !rendererReady) {
304
+ ui.showError("Renderer not initialized yet. Please wait a moment and try again.");
259
305
  return;
260
306
  }
261
307
 
308
+ ui.logger.Info(`Creating renderer for event ${eventId.value} with country: ${country}`);
309
+
262
310
  renderer = rendererFactory.createRenderer(eventId.value, {
263
311
  proxy: "",
264
312
  country: country,
265
- seatColor: "#0557ae",
266
- nonAvSeatColor: "#dadcde",
267
- highlightedSeatColor: "#d0006f"
313
+ seatColor: "#b8e6d5",
314
+ nonAvSeatColor: "#707070",
315
+ highlightedSeatColor: "#7bc999"
268
316
  });
269
317
 
318
+ if (!renderer) {
319
+ throw new Error("Failed to create renderer instance");
320
+ }
321
+
322
+ ui.logger.Info("Renderer created, setting config...");
270
323
  filterBuilder.value.highlightedSeatColor = renderer.config.highlightedSeatColor;
271
324
  renderer.setCustomConfig({
272
325
  logFullError: true,
@@ -274,20 +327,46 @@ const updateShownVenue = async () => {
274
327
  renderRowBlocks: true
275
328
  });
276
329
 
330
+ ui.logger.Info("Starting render...");
277
331
  await renderer.render();
278
- svg.value = renderer.svg;
279
-
280
- if (!svg.value) {
281
- throw new Error("Renderer returned empty SVG");
332
+ ui.logger.Info("Render completed");
333
+
334
+ // Add extra validation
335
+ if (!renderer.svg || renderer.svg.trim() === '') {
336
+ ui.logger.Error("Renderer SVG is empty or null:", {
337
+ hasSvg: !!renderer.svg,
338
+ svgLength: renderer.svg?.length,
339
+ country,
340
+ eventId: eventId.value
341
+ });
342
+ throw new Error(`Renderer returned empty SVG. Country: ${country}, EventID: ${eventId.value}`);
282
343
  }
344
+
345
+ ui.logger.Info(`SVG generated successfully, length: ${renderer.svg.length}`);
346
+ svg.value = renderer.svg;
283
347
  } catch (e) {
284
348
  ui.logger.Error("COULD NOT RENDER SVG", e);
349
+ ui.logger.Error("Error details:", {
350
+ message: e?.message,
351
+ stack: e?.stack,
352
+ name: e?.name,
353
+ cause: e?.cause,
354
+ renderer: !!renderer,
355
+ rendererFactory: !!rendererFactory,
356
+ rendererReady,
357
+ userAgent: navigator.userAgent
358
+ });
359
+
285
360
  const errorMsg = e?.message || "Unknown rendering error";
286
361
  ui.showError(`Failed to render venue: ${errorMsg}`);
287
362
 
288
363
  // Try to load existing filter even if rendering fails
289
- await loadFilter();
290
- filterBuilder.value.reload(eventId.value);
364
+ try {
365
+ await loadFilter();
366
+ filterBuilder.value.reload(eventId.value);
367
+ } catch (loadErr) {
368
+ ui.logger.Error("Could not load filter either", loadErr);
369
+ }
291
370
  return;
292
371
  }
293
372
 
@@ -414,13 +493,139 @@ watch(renderSeats, async () => {
414
493
  }
415
494
 
416
495
  .svg-wrapper path[generaladmission] {
417
- pointer-events: auto;
418
- cursor: pointer;
496
+ pointer-events: auto !important;
497
+ cursor: pointer !important;
498
+ }
499
+
500
+ /* Hover effects */
501
+ .svg-wrapper path:hover {
502
+ opacity: 0.8;
419
503
  }
420
504
 
421
- /* Hover effects for general admission seats */
422
505
  .svg-wrapper path[generaladmission]:hover {
423
- pointer-events: all;
424
- cursor: pointer;
506
+ opacity: 1;
507
+ fill: #7ee8fa !important;
508
+ }
509
+
510
+ /* Mobile responsiveness - bulletproof on all devices */
511
+ @media (max-width: 768px) {
512
+ .page-header {
513
+ flex-direction: column;
514
+ align-items: stretch !important;
515
+ gap: 0.75rem;
516
+ }
517
+
518
+ .unified-search-group {
519
+ width: 100% !important;
520
+ min-width: 100% !important;
521
+ }
522
+ }
523
+
524
+ @media (max-width: 768px) {
525
+ .page-header {
526
+ flex-direction: column !important;
527
+ }
528
+ }
529
+
530
+ @media (max-width: 480px) {
531
+ .page-header {
532
+ padding-bottom: 0.75rem;
533
+ }
534
+
535
+ .unified-search-group input {
536
+ min-width: 0;
537
+ font-size: 13px;
538
+ }
539
+
540
+ .filter-action-btn {
541
+ font-size: 11px;
542
+ padding: 0.25rem 0.5rem;
543
+ }
544
+
545
+ .btn-icon-small {
546
+ width: 1.75rem;
547
+ height: 1.75rem;
548
+ font-size: 12px;
549
+ }
550
+
551
+ }
552
+
553
+ /* Force filters list to be constrained and scrollable */
554
+ .hidden-scrollbars.flex-1.overflow-y-auto {
555
+ max-height: 100%;
556
+ min-height: 150px;
557
+ overflow-y: auto !important;
558
+ }
559
+
560
+ /* SVG and Filters container responsive sizing */
561
+ .svg-container {
562
+ min-height: 200px;
563
+ max-height: 250px;
564
+ flex-shrink: 0;
565
+ }
566
+
567
+ .svg-container.has-svg {
568
+ min-height: 240px;
569
+ max-height: 280px;
570
+ flex-shrink: 0;
571
+ }
572
+
573
+ .filters-container {
574
+ min-height: 260px;
575
+ max-height: 320px;
576
+ flex-shrink: 1;
577
+ flex-grow: 1;
578
+ display: flex;
579
+ flex-direction: column;
580
+ overflow: hidden;
581
+ }
582
+
583
+ /* Ensure filters card wrapper doesn't grow too large */
584
+ .filters-card {
585
+ max-height: calc(100% - 3.5rem);
586
+ min-height: 200px;
587
+ }
588
+
589
+ /* Add spacing for bottom buttons */
590
+ .filters-container > div:last-child {
591
+ margin-bottom: 0.5rem;
592
+ }
593
+
594
+ @media (min-width: 1024px) {
595
+ .svg-container {
596
+ min-height: 400px;
597
+ max-height: 100%;
598
+ flex-shrink: 1;
599
+ }
600
+
601
+ .svg-container.has-svg {
602
+ min-height: 400px;
603
+ max-height: 100%;
604
+ flex-shrink: 1;
605
+ }
606
+
607
+ .filters-container {
608
+ min-height: 400px;
609
+ max-height: 100%;
610
+ flex-shrink: 1;
611
+ }
612
+ }
613
+
614
+ /* Ensure filters list always scrolls */
615
+ .hidden-scrollbars::-webkit-scrollbar {
616
+ width: 4px;
617
+ }
618
+
619
+ .hidden-scrollbars::-webkit-scrollbar-track {
620
+ background: transparent;
621
+ }
622
+
623
+ .hidden-scrollbars::-webkit-scrollbar-thumb {
624
+ background: oklch(0.35 0 0);
625
+ border-radius: 4px;
626
+ }
627
+
628
+ .hidden-scrollbars::-webkit-scrollbar-thumb:hover {
629
+ background: oklch(0.45 0 0);
425
630
  }
426
631
  </style>
package/vite.config.js CHANGED
@@ -10,6 +10,7 @@ export default defineConfig({
10
10
  cacheDir: "./node_modules/.vite",
11
11
  build: {
12
12
  rollupOptions: {
13
+ external: ['sharp', 'node-persist', 'undici'],
13
14
  output: {
14
15
  manualChunks: {
15
16
  vue: ["vue", "vue-router", "pinia"],