@tekkare/auriga 0.2.192 → 0.2.198

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,631 @@
1
+ <template>
2
+ <div class="arg_dashboard_container">
3
+ <slot name="toolbar" />
4
+
5
+ <!-- Loading state -->
6
+ <div v-if="loading" class="arg_dashboard_loading">
7
+ <div v-for="n in 4" :key="n" class="arg_dashboard_skeleton">
8
+ <div class="arg_dashboard_skeleton_header">
9
+ <div class="arg_dashboard_skeleton_bar" style="width: 140px"></div>
10
+ <div class="arg_dashboard_skeleton_bar" style="width: 40px"></div>
11
+ </div>
12
+ <div class="arg_dashboard_skeleton_body">
13
+ <div class="arg_dashboard_skeleton_bar" style="width: 100%; height: 12px"></div>
14
+ <div class="arg_dashboard_skeleton_bar" style="width: 80%; height: 12px"></div>
15
+ <div class="arg_dashboard_skeleton_bar" style="width: 60%; height: 12px"></div>
16
+ </div>
17
+ </div>
18
+ </div>
19
+
20
+ <!-- Dashboard grid -->
21
+ <div v-else-if="visibleSections.length > 0" ref="gridContainer" class="arg_dashboard_grid">
22
+ <div
23
+ v-for="section in visibleSections"
24
+ :key="section.id"
25
+ class="arg_dashboard_item"
26
+ :class="getWidthClass(section.id)"
27
+ :data-section-id="section.id"
28
+ >
29
+ <div class="arg_dashboard_item_content">
30
+ <arg-dashboard-section
31
+ :title="section.title"
32
+ :icon="section.icon"
33
+ :count="section.data.length"
34
+ :default-collapsed="section.defaultCollapsed || false"
35
+ :max-height="section.maxHeight || 300"
36
+ @collapse-change="onCollapseChange(section.id, $event)"
37
+ >
38
+ <template #headerActions>
39
+ <!-- View mode toggles -->
40
+ <div class="arg_dashboard_view_toggles">
41
+ <button
42
+ v-for="mode in getViewModes(section)"
43
+ :key="mode"
44
+ class="arg_dashboard_toggle_btn"
45
+ :class="{ arg_dashboard_toggle_active: getSectionState(section.id).viewMode === mode }"
46
+ :title="mode"
47
+ @click="setViewMode(section.id, mode)"
48
+ >
49
+ <arg-icon :name="viewModeIcon(mode)" size="16" color="currentColor" />
50
+ </button>
51
+ </div>
52
+
53
+ <!-- Width toggles -->
54
+ <div class="arg_dashboard_width_toggles">
55
+ <button
56
+ class="arg_dashboard_toggle_btn"
57
+ :class="{ arg_dashboard_toggle_active: getSectionState(section.id).width === 'full' }"
58
+ title="Full width"
59
+ @click="setWidth(section.id, 'full')"
60
+ >
61
+ <arg-icon name="ArrowsOutSimple" size="16" color="currentColor" />
62
+ </button>
63
+ <button
64
+ class="arg_dashboard_toggle_btn"
65
+ :class="{ arg_dashboard_toggle_active: getSectionState(section.id).width === 'half' }"
66
+ title="Half width"
67
+ @click="setWidth(section.id, 'half')"
68
+ >
69
+ <arg-icon name="SplitHorizontal" size="16" color="currentColor" />
70
+ </button>
71
+ </div>
72
+ </template>
73
+
74
+ <!-- Table view -->
75
+ <arg-resource-table
76
+ v-if="getSectionState(section.id).viewMode === 'table'"
77
+ :data="section.data"
78
+ :variant="section.variant || 'default'"
79
+ :show-source-tag="section.showSourceTag || false"
80
+ :max-height="section.maxHeight || 300"
81
+ @on-remove="(item) => emit('onRemove', item)"
82
+ @on-annotate="(item) => emit('onAnnotate', item)"
83
+ @on-open="(item) => emit('onOpen', item)"
84
+ />
85
+
86
+ <!-- Cards view -->
87
+ <div
88
+ v-else-if="getSectionState(section.id).viewMode === 'cards'"
89
+ class="arg_dashboard_cards_grid"
90
+ >
91
+ <arg-resource-card
92
+ v-for="item in section.data"
93
+ :key="item.id"
94
+ :data="item"
95
+ :hide-provider="section.hideProviderOnCards || false"
96
+ @on-remove="(item) => emit('onRemove', item)"
97
+ @on-annotate="(item) => emit('onAnnotate', item)"
98
+ @on-open="(item) => emit('onOpen', item)"
99
+ />
100
+ </div>
101
+
102
+ <!-- Chart view (grouped) -->
103
+ <div
104
+ v-else-if="getSectionState(section.id).viewMode === 'chart' && section.chartMode !== 'carousel'"
105
+ class="arg_dashboard_chart_view"
106
+ >
107
+ <arg-chart
108
+ v-if="section.chartData"
109
+ type="bar"
110
+ :categories="section.chartData.categories"
111
+ :series="section.chartData.series"
112
+ height="300"
113
+ />
114
+ <arg-table
115
+ v-if="section.chartData"
116
+ :heads="['Name', ...section.chartData.categories]"
117
+ :contents="chartTableContents(section)"
118
+ :sort="true"
119
+ :full-table-scroll="true"
120
+ :max-height="200"
121
+ />
122
+ </div>
123
+
124
+ <!-- Chart view (carousel) -->
125
+ <div
126
+ v-else-if="getSectionState(section.id).viewMode === 'chart' && section.chartMode === 'carousel'"
127
+ class="arg_dashboard_chart_view"
128
+ >
129
+ <div v-if="section.carouselItems && section.carouselItems.length" class="arg_dashboard_carousel">
130
+ <div class="arg_dashboard_carousel_nav">
131
+ <button
132
+ class="arg_dashboard_carousel_btn"
133
+ :disabled="getSectionState(section.id).carouselIndex <= 0"
134
+ @click="carouselPrev(section.id)"
135
+ >
136
+ <arg-icon name="CaretLeft" size="16" color="currentColor" />
137
+ </button>
138
+ <span class="arg_dashboard_carousel_label">
139
+ {{ section.carouselItems[getSectionState(section.id).carouselIndex]?.label || '' }}
140
+ <span class="arg_dashboard_carousel_counter">
141
+ ({{ getSectionState(section.id).carouselIndex + 1 }}/{{ section.carouselItems.length }})
142
+ </span>
143
+ </span>
144
+ <button
145
+ class="arg_dashboard_carousel_btn"
146
+ :disabled="getSectionState(section.id).carouselIndex >= section.carouselItems.length - 1"
147
+ @click="carouselNext(section.id, section.carouselItems.length)"
148
+ >
149
+ <arg-icon name="CaretRight" size="16" color="currentColor" />
150
+ </button>
151
+ </div>
152
+ <arg-chart
153
+ type="bar"
154
+ :categories="currentCarouselItem(section)?.chartData?.categories || []"
155
+ :series="currentCarouselItem(section)?.chartData?.series || []"
156
+ height="300"
157
+ />
158
+ <arg-table
159
+ v-if="currentCarouselItem(section)?.chartData"
160
+ :heads="['Name', ...(currentCarouselItem(section)?.chartData?.categories || [])]"
161
+ :contents="carouselTableContents(section)"
162
+ :sort="true"
163
+ :full-table-scroll="true"
164
+ :max-height="200"
165
+ />
166
+ </div>
167
+ </div>
168
+ </arg-dashboard-section>
169
+ </div>
170
+ </div>
171
+ </div>
172
+
173
+ <!-- Empty state -->
174
+ <div v-else class="arg_dashboard_empty">
175
+ <slot name="empty">
176
+ <div class="arg_dashboard_empty_content">
177
+ <arg-icon name="FolderOpen" size="48" color="var(--medium)" />
178
+ <h3 class="arg_dashboard_empty_title">{{ emptyTitle }}</h3>
179
+ <p class="arg_dashboard_empty_text">{{ emptyText }}</p>
180
+ </div>
181
+ </slot>
182
+ </div>
183
+ </div>
184
+ </template>
185
+
186
+ <script setup lang="ts">
187
+ import { ref, computed, watch, onMounted, onBeforeUnmount, reactive, nextTick } from "vue";
188
+
189
+ interface ResourceItem {
190
+ id: string;
191
+ title: string;
192
+ identification?: string;
193
+ publication_date?: string;
194
+ source?: { flag: string; label: string };
195
+ resourceType?: string;
196
+ card_link?: string;
197
+ resource_link?: string;
198
+ countAnnotation?: number;
199
+ countryName?: string;
200
+ price?: number;
201
+ priceVariation?: number;
202
+ currency?: string;
203
+ currencySymbol?: string;
204
+ atcCode?: string;
205
+ manufacturer?: string;
206
+ diagnosisLabel?: string;
207
+ diagnosisType?: string;
208
+ year?: string;
209
+ hospitalizations?: number | null;
210
+ cancerCode?: string;
211
+ cases?: number | null;
212
+ rate?: number | null;
213
+ metricType?: string;
214
+ geneName?: string;
215
+ linkedCount?: number;
216
+ linkedLabel?: string;
217
+ icd10Codes?: string[];
218
+ provider?: string;
219
+ icon?: string;
220
+ }
221
+
222
+ export interface DashboardSectionConfig {
223
+ id: string;
224
+ title: string;
225
+ icon: string;
226
+ variant?: string;
227
+ data: ResourceItem[];
228
+ viewModes?: ("table" | "cards" | "chart")[];
229
+ showSourceTag?: boolean;
230
+ hideProviderOnCards?: boolean;
231
+ defaultWidth?: "full" | "half";
232
+ defaultCollapsed?: boolean;
233
+ maxHeight?: number;
234
+ chartData?: {
235
+ categories: string[];
236
+ series: { name: string; data: number[]; resourceRef?: any }[];
237
+ };
238
+ chartMode?: "grouped" | "carousel";
239
+ carouselItems?: {
240
+ label: string;
241
+ chartData: { categories: string[]; series: any[] };
242
+ resourceRef?: any;
243
+ }[];
244
+ }
245
+
246
+ interface SectionState {
247
+ viewMode: "table" | "cards" | "chart";
248
+ width: "full" | "half";
249
+ carouselIndex: number;
250
+ }
251
+
252
+ const props = withDefaults(
253
+ defineProps<{
254
+ sections?: DashboardSectionConfig[];
255
+ loading?: boolean;
256
+ emptyTitle?: string;
257
+ emptyText?: string;
258
+ dragEnabled?: boolean;
259
+ }>(),
260
+ {
261
+ sections: () => [],
262
+ loading: false,
263
+ emptyTitle: "No resource found",
264
+ emptyText: "Please add resources to the project",
265
+ dragEnabled: true,
266
+ }
267
+ );
268
+
269
+ const emit = defineEmits<{
270
+ onRemove: [item: ResourceItem];
271
+ onAnnotate: [item: ResourceItem];
272
+ onOpen: [item: ResourceItem];
273
+ onOrderChange: [order: string[]];
274
+ }>();
275
+
276
+ const gridContainer = ref<HTMLElement | null>(null);
277
+ let muuriInstance: any = null;
278
+
279
+ const sectionStates = reactive<Record<string, SectionState>>({});
280
+
281
+ const visibleSections = computed(() =>
282
+ props.sections.filter((s) => s.data && s.data.length > 0)
283
+ );
284
+
285
+ function getSectionState(id: string): SectionState {
286
+ if (!sectionStates[id]) {
287
+ const section = props.sections.find((s) => s.id === id);
288
+ sectionStates[id] = {
289
+ viewMode: section?.viewModes?.[0] || "table",
290
+ width: section?.defaultWidth || "full",
291
+ carouselIndex: 0,
292
+ };
293
+ }
294
+ return sectionStates[id];
295
+ }
296
+
297
+ function getViewModes(section: DashboardSectionConfig): string[] {
298
+ return section.viewModes || ["table", "cards"];
299
+ }
300
+
301
+ function viewModeIcon(mode: string): string {
302
+ switch (mode) {
303
+ case "table":
304
+ return "Table";
305
+ case "cards":
306
+ return "SquaresFour";
307
+ case "chart":
308
+ return "ChartBar";
309
+ default:
310
+ return "Table";
311
+ }
312
+ }
313
+
314
+ function setViewMode(sectionId: string, mode: "table" | "cards" | "chart") {
315
+ getSectionState(sectionId).viewMode = mode;
316
+ refreshLayout();
317
+ }
318
+
319
+ function setWidth(sectionId: string, width: "full" | "half") {
320
+ getSectionState(sectionId).width = width;
321
+ refreshLayout();
322
+ }
323
+
324
+ function getWidthClass(sectionId: string): string {
325
+ return getSectionState(sectionId).width === "half"
326
+ ? "arg_dashboard_item_half"
327
+ : "arg_dashboard_item_full";
328
+ }
329
+
330
+ function onCollapseChange(_sectionId: string, _collapsed: boolean) {
331
+ refreshLayout();
332
+ }
333
+
334
+ // Carousel
335
+ function carouselPrev(sectionId: string) {
336
+ const state = getSectionState(sectionId);
337
+ if (state.carouselIndex > 0) state.carouselIndex--;
338
+ }
339
+
340
+ function carouselNext(sectionId: string, total: number) {
341
+ const state = getSectionState(sectionId);
342
+ if (state.carouselIndex < total - 1) state.carouselIndex++;
343
+ }
344
+
345
+ function currentCarouselItem(section: DashboardSectionConfig) {
346
+ if (!section.carouselItems?.length) return null;
347
+ const idx = getSectionState(section.id).carouselIndex;
348
+ return section.carouselItems[idx] || null;
349
+ }
350
+
351
+ // Chart table helpers
352
+ function chartTableContents(section: DashboardSectionConfig) {
353
+ if (!section.chartData) return [];
354
+ return section.chartData.series.map((s) => ({
355
+ values: [s.name, ...s.data],
356
+ }));
357
+ }
358
+
359
+ function carouselTableContents(section: DashboardSectionConfig) {
360
+ const item = currentCarouselItem(section);
361
+ if (!item?.chartData) return [];
362
+ return item.chartData.series.map((s: any) => ({
363
+ values: [s.name, ...s.data],
364
+ }));
365
+ }
366
+
367
+ // Muuri integration
368
+ async function initMuuri() {
369
+ if (!props.dragEnabled || !gridContainer.value) return;
370
+
371
+ const Muuri = (await import("muuri")).default;
372
+ muuriInstance = new Muuri(gridContainer.value, {
373
+ dragEnabled: true,
374
+ dragHandle: ".arg_dashboard_drag_handle",
375
+ dragContainer: document.body,
376
+ layoutDuration: 300,
377
+ layoutEasing: "ease",
378
+ dragStartPredicate: { distance: 10, delay: 0 },
379
+ dragPlaceholder: {
380
+ enabled: true,
381
+ createElement: (item: any) => {
382
+ const el = item.getElement().cloneNode(true);
383
+ el.style.opacity = "0.3";
384
+ return el;
385
+ },
386
+ },
387
+ });
388
+
389
+ muuriInstance.on("dragEnd", () => {
390
+ const items = muuriInstance.getItems();
391
+ const order = items
392
+ .map((item: any) => item.getElement()?.getAttribute("data-section-id"))
393
+ .filter(Boolean) as string[];
394
+ emit("onOrderChange", order);
395
+ });
396
+ }
397
+
398
+ function refreshLayout() {
399
+ nextTick(() => {
400
+ muuriInstance?.refreshItems?.();
401
+ muuriInstance?.layout?.();
402
+ });
403
+ }
404
+
405
+ watch(
406
+ () => props.sections,
407
+ () => {
408
+ nextTick(() => {
409
+ refreshLayout();
410
+ });
411
+ },
412
+ { deep: true }
413
+ );
414
+
415
+ onMounted(() => {
416
+ initMuuri();
417
+ });
418
+
419
+ onBeforeUnmount(() => {
420
+ muuriInstance?.destroy?.();
421
+ muuriInstance = null;
422
+ });
423
+ </script>
424
+
425
+ <!-- Unscoped: Muuri manipulates DOM outside the component (dragContainer: body)
426
+ and adds its own classes (muuri-item-*). These must not be scoped. -->
427
+ <style>
428
+ .arg_dashboard_grid{position:relative}.arg_dashboard_item{box-sizing:border-box;padding:8px;position:absolute;z-index:1}.arg_dashboard_item_full{width:100%}.arg_dashboard_item_half{width:50%}.arg_dashboard_item_content{position:relative}.arg_dashboard_item.muuri-item-dragging{cursor:grabbing;z-index:3}.arg_dashboard_item.muuri-item-releasing{z-index:2}.arg_dashboard_item.muuri-item-hidden{z-index:0}
429
+ </style>
430
+
431
+ <style scoped>
432
+ .arg_dashboard_container {
433
+ display: flex;
434
+ flex-direction: column;
435
+ gap: 16px;
436
+ width: 100%;
437
+ }
438
+
439
+ /* Loading skeleton */
440
+ .arg_dashboard_loading {
441
+ display: grid;
442
+ grid-template-columns: repeat(2, 1fr);
443
+ gap: 16px;
444
+ }
445
+
446
+ .arg_dashboard_skeleton {
447
+ background: var(--base-white, #fff);
448
+ border: 1px solid var(--light, #ececf2);
449
+ border-radius: 10px;
450
+ overflow: hidden;
451
+ }
452
+
453
+ .arg_dashboard_skeleton_header {
454
+ display: flex;
455
+ align-items: center;
456
+ justify-content: space-between;
457
+ padding: 16px;
458
+ background: var(--lightest, #f5f5fa);
459
+ border-bottom: 1px solid var(--light, #ececf2);
460
+ }
461
+
462
+ .arg_dashboard_skeleton_body {
463
+ display: flex;
464
+ flex-direction: column;
465
+ gap: 10px;
466
+ padding: 16px;
467
+ }
468
+
469
+ .arg_dashboard_skeleton_bar {
470
+ height: 16px;
471
+ background: linear-gradient(90deg, var(--light, #ececf2) 25%, var(--lightest, #f5f5fa) 50%, var(--light, #ececf2) 75%);
472
+ background-size: 200% 100%;
473
+ border-radius: 4px;
474
+ animation: arg_dashboard_shimmer 1.5s ease-in-out infinite;
475
+ }
476
+
477
+ @keyframes arg_dashboard_shimmer {
478
+ 0% {
479
+ background-position: -200% 0;
480
+ }
481
+ 100% {
482
+ background-position: 200% 0;
483
+ }
484
+ }
485
+
486
+ /* View mode toggles */
487
+ .arg_dashboard_view_toggles,
488
+ .arg_dashboard_width_toggles {
489
+ display: flex;
490
+ align-items: center;
491
+ gap: 2px;
492
+ background: var(--base-white, #fff);
493
+ border-radius: 6px;
494
+ padding: 2px;
495
+ border: 1px solid var(--light, #ececf2);
496
+ }
497
+
498
+ .arg_dashboard_toggle_btn {
499
+ display: flex;
500
+ align-items: center;
501
+ justify-content: center;
502
+ width: 28px;
503
+ height: 28px;
504
+ border: none;
505
+ background: transparent;
506
+ border-radius: 4px;
507
+ cursor: pointer;
508
+ color: var(--medium, #9999b0);
509
+ transition: all 0.15s ease;
510
+ }
511
+
512
+ .arg_dashboard_toggle_btn:hover {
513
+ color: var(--dark, #6c6c8a);
514
+ background: var(--lightest, #f5f5fa);
515
+ }
516
+
517
+ .arg_dashboard_toggle_active {
518
+ color: var(--primary, #2176ff);
519
+ background: rgba(33, 118, 255, 0.1);
520
+ }
521
+
522
+ .arg_dashboard_toggle_active:hover {
523
+ color: var(--primary, #2176ff);
524
+ background: rgba(33, 118, 255, 0.15);
525
+ }
526
+
527
+ /* Cards grid */
528
+ .arg_dashboard_cards_grid {
529
+ display: grid;
530
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
531
+ gap: 12px;
532
+ padding: 12px;
533
+ }
534
+
535
+ /* Chart view */
536
+ .arg_dashboard_chart_view {
537
+ padding: 12px;
538
+ display: flex;
539
+ flex-direction: column;
540
+ gap: 12px;
541
+ }
542
+
543
+ /* Carousel */
544
+ .arg_dashboard_carousel {
545
+ display: flex;
546
+ flex-direction: column;
547
+ gap: 12px;
548
+ }
549
+
550
+ .arg_dashboard_carousel_nav {
551
+ display: flex;
552
+ align-items: center;
553
+ justify-content: center;
554
+ gap: 12px;
555
+ padding: 8px 0;
556
+ }
557
+
558
+ .arg_dashboard_carousel_btn {
559
+ display: flex;
560
+ align-items: center;
561
+ justify-content: center;
562
+ width: 32px;
563
+ height: 32px;
564
+ border: 1px solid var(--light, #ececf2);
565
+ background: var(--base-white, #fff);
566
+ border-radius: 8px;
567
+ cursor: pointer;
568
+ color: var(--dark, #6c6c8a);
569
+ transition: all 0.15s ease;
570
+ }
571
+
572
+ .arg_dashboard_carousel_btn:hover:not(:disabled) {
573
+ background: var(--lightest, #f5f5fa);
574
+ border-color: var(--medium, #9999b0);
575
+ }
576
+
577
+ .arg_dashboard_carousel_btn:disabled {
578
+ opacity: 0.4;
579
+ cursor: not-allowed;
580
+ }
581
+
582
+ .arg_dashboard_carousel_label {
583
+ font-size: 14px;
584
+ font-weight: 600;
585
+ color: var(--darkest, #1a1a2e);
586
+ }
587
+
588
+ .arg_dashboard_carousel_counter {
589
+ font-weight: 400;
590
+ color: var(--medium, #9999b0);
591
+ }
592
+
593
+ /* Empty state */
594
+ .arg_dashboard_empty {
595
+ display: flex;
596
+ justify-content: center;
597
+ padding: 64px 24px;
598
+ }
599
+
600
+ .arg_dashboard_empty_content {
601
+ display: flex;
602
+ flex-direction: column;
603
+ align-items: center;
604
+ gap: 12px;
605
+ text-align: center;
606
+ }
607
+
608
+ .arg_dashboard_empty_title {
609
+ font-size: 18px;
610
+ font-weight: 600;
611
+ color: var(--darkest, #1a1a2e);
612
+ margin: 0;
613
+ }
614
+
615
+ .arg_dashboard_empty_text {
616
+ font-size: 14px;
617
+ color: var(--dark, #6c6c8a);
618
+ margin: 0;
619
+ }
620
+
621
+ /* Responsive */
622
+ @media (max-width: 768px) {
623
+ .arg_dashboard_cards_grid {
624
+ grid-template-columns: 1fr;
625
+ }
626
+
627
+ .arg_dashboard_loading {
628
+ grid-template-columns: 1fr;
629
+ }
630
+ }
631
+ </style>