Dashboard final version, TODO: update main sidebar menu
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import { reactiveOmit } from "@vueuse/core";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Separator } from '@/Components/ui/separator';
|
||||
|
||||
const props = defineProps({
|
||||
orientation: { type: String, required: false, default: "vertical" },
|
||||
|
||||
@@ -3,7 +3,7 @@ import { cva } from "class-variance-authority";
|
||||
export { default as Button } from "./Button.vue";
|
||||
|
||||
export const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer",
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
||||
@@ -8,8 +8,12 @@ const props = defineProps({
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="card"
|
||||
:class="
|
||||
cn('rounded-xl border bg-card text-card-foreground shadow', props.class)
|
||||
cn(
|
||||
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup>
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps({
|
||||
class: { type: null, required: false },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="card-action"
|
||||
:class="
|
||||
cn(
|
||||
'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -7,7 +7,7 @@ const props = defineProps({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('p-6 pt-0', props.class)">
|
||||
<div data-slot="card-content" :class="cn('px-6', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -7,7 +7,10 @@ const props = defineProps({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p :class="cn('text-sm text-muted-foreground', props.class)">
|
||||
<p
|
||||
data-slot="card-description"
|
||||
:class="cn('text-muted-foreground text-sm', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</p>
|
||||
</template>
|
||||
|
||||
@@ -7,7 +7,10 @@ const props = defineProps({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('flex items-center p-6 pt-0', props.class)">
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
:class="cn('flex items-center px-6 [.border-t]:pt-6', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -7,7 +7,15 @@ const props = defineProps({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('flex flex-col gap-y-1.5 p-6', props.class)">
|
||||
<div
|
||||
data-slot="card-header"
|
||||
:class="
|
||||
cn(
|
||||
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -7,7 +7,10 @@ const props = defineProps({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h3 :class="cn('font-semibold leading-none tracking-tight', props.class)">
|
||||
<h3
|
||||
data-slot="card-title"
|
||||
:class="cn('leading-none font-semibold', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</h3>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export { default as Card } from "./Card.vue";
|
||||
export { default as CardAction } from "./CardAction.vue";
|
||||
export { default as CardContent } from "./CardContent.vue";
|
||||
export { default as CardDescription } from "./CardDescription.vue";
|
||||
export { default as CardFooter } from "./CardFooter.vue";
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { useChartContext } from './interface'
|
||||
|
||||
const props = defineProps({
|
||||
order: { type: Array, required: false }, // explicit ordering of keys
|
||||
modelValue: { type: Array, required: false }, // deprecated alias
|
||||
activeKeys: { type: Array, required: false }, // v-model:activeKeys target
|
||||
class: { type: [String, Array, Object], required: false },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:activeKeys'])
|
||||
|
||||
const { config } = useChartContext()
|
||||
|
||||
// Derive ordered keys from config
|
||||
const allKeys = computed(() => {
|
||||
const keys = Object.keys(config)
|
||||
if (props.order && props.order.length) {
|
||||
return props.order.filter(k => keys.includes(k))
|
||||
}
|
||||
return keys
|
||||
})
|
||||
|
||||
// Internal active state (if parent not controlling)
|
||||
const internalActive = ref(allKeys.value.reduce((acc, k) => { acc[k] = true; return acc }, {}))
|
||||
|
||||
const activeMap = computed(() => {
|
||||
// If parent passes controlled array use that
|
||||
if (props.activeKeys && props.activeKeys.length) {
|
||||
return props.activeKeys.reduce((acc, k) => { acc[k] = true; return acc }, {})
|
||||
}
|
||||
return internalActive.value
|
||||
})
|
||||
|
||||
const items = computed(() => allKeys.value.map(k => {
|
||||
const series = config[k] || {}
|
||||
const color = series.color || (series.theme && (series.theme.light || series.theme.dark)) || 'var(--foreground)'
|
||||
return { key: k, label: series.label || k, color, active: !!activeMap.value[k] }
|
||||
}))
|
||||
|
||||
function toggle(key) {
|
||||
// controlled mode
|
||||
if (props.activeKeys) {
|
||||
const next = items.value.filter(i => i.key === key ? !i.active : i.active).map(i => i.key)
|
||||
// If item was active we remove it, else add it
|
||||
const wasActive = activeMap.value[key]
|
||||
const result = wasActive
|
||||
? props.activeKeys.filter(k => k !== key)
|
||||
: [...props.activeKeys, key]
|
||||
emit('update:activeKeys', result)
|
||||
return
|
||||
}
|
||||
// uncontrolled mode
|
||||
internalActive.value[key] = !internalActive.value[key]
|
||||
const result = Object.entries(internalActive.value).filter(([, v]) => v).map(([k]) => k)
|
||||
emit('update:activeKeys', result)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['flex items-center justify-center flex-wrap gap-4 text-xs select-none', props.class]">
|
||||
<button
|
||||
v-for="item in items"
|
||||
:key="item.key"
|
||||
type="button"
|
||||
:class="[
|
||||
'flex items-center gap-2 transition-colors',
|
||||
item.active ? 'opacity-100' : 'opacity-40',
|
||||
'focus:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm'
|
||||
]"
|
||||
@click="toggle(item.key)"
|
||||
>
|
||||
<span class="h-2.5 w-2.5 rounded-[3px] border border-border" :style="{ background: item.color }" />
|
||||
<span>{{ item.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { provideChartContext } from './interface';
|
||||
|
||||
const props = defineProps({
|
||||
config: { type: Object, required: false, default: () => ({}) },
|
||||
class: { type: [String, Array, Object], required: false },
|
||||
cursor: { type: Boolean, default: true },
|
||||
id: { type: String, required: false },
|
||||
});
|
||||
|
||||
// Provide context (even if empty) so descendants can attempt to read series config.
|
||||
const ctx = provideChartContext(props.config, props.id);
|
||||
const chartDomId = computed(() => `chart-${ctx.id}`);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-chart-container
|
||||
:data-chart="chartDomId"
|
||||
:class="['relative w-full flex flex-col', props.class]"
|
||||
:style="{
|
||||
// Default color variables; series components can override via inline style or CSS theme logic.
|
||||
'--vis-primary-color': 'hsl(var(--primary))',
|
||||
'--vis-secondary-color': 'hsl(var(--secondary))',
|
||||
'--vis-crosshair-line-stroke-width': props.cursor ? '1px' : '0px',
|
||||
'--vis-font-family': 'var(--font-sans)',
|
||||
}"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,54 @@
|
||||
<script setup>
|
||||
import { omit } from "@unovis/ts";
|
||||
import { VisCrosshair, VisTooltip } from "@unovis/vue";
|
||||
import { createApp } from "vue";
|
||||
import { ChartTooltip } from ".";
|
||||
|
||||
const props = defineProps({
|
||||
colors: { type: Array, required: false, default: () => [] },
|
||||
index: { type: String, required: true },
|
||||
// items now optional when using external template factory
|
||||
items: { type: Array, required: false, default: () => [] },
|
||||
customTooltip: { type: null, required: false },
|
||||
labelFormatter: { type: Function, required: false },
|
||||
// template override (e.g., componentToString(...))
|
||||
template: { type: Function, required: false },
|
||||
});
|
||||
|
||||
// Use weakmap to store reference to each datapoint for Tooltip
|
||||
const wm = new WeakMap();
|
||||
function internalTemplate(d) {
|
||||
// If we have cached markup and no custom formatter altering title, reuse.
|
||||
if (wm.has(d) && !props.labelFormatter && !props.template) {
|
||||
return wm.get(d);
|
||||
}
|
||||
// If external template provided, delegate directly
|
||||
if (props.template) {
|
||||
const html = props.template(d, d[props.index]);
|
||||
wm.set(d, html);
|
||||
return html;
|
||||
}
|
||||
const componentDiv = document.createElement("div");
|
||||
const omittedData = Object.entries(omit(d, [props.index])).map(([key, value]) => {
|
||||
const legendReference = props.items.find((i) => i.name === key);
|
||||
return { ...legendReference, value };
|
||||
});
|
||||
const TooltipComponent = props.customTooltip ?? ChartTooltip;
|
||||
createApp(TooltipComponent, {
|
||||
title: d[props.index],
|
||||
data: omittedData,
|
||||
labelFormatter: props.labelFormatter,
|
||||
}).mount(componentDiv);
|
||||
wm.set(d, componentDiv.innerHTML);
|
||||
return componentDiv.innerHTML;
|
||||
}
|
||||
|
||||
function color(d, i) {
|
||||
return props.colors[i] ?? "transparent";
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VisTooltip :horizontal-shift="20" :vertical-shift="20" />
|
||||
<VisCrosshair :template="internalTemplate" :color="color" />
|
||||
</template>
|
||||
@@ -0,0 +1,66 @@
|
||||
<script setup>
|
||||
import { BulletLegend } from "@unovis/ts";
|
||||
import { VisBulletLegend } from "@unovis/vue";
|
||||
import { nextTick, onMounted, ref } from "vue";
|
||||
import { buttonVariants } from '@/Components/ui/button';
|
||||
|
||||
const props = defineProps({
|
||||
items: { type: Array, required: true, default: () => [] },
|
||||
});
|
||||
|
||||
const emits = defineEmits(["legendItemClick", "update:items"]);
|
||||
|
||||
const elRef = ref();
|
||||
|
||||
function keepStyling() {
|
||||
const selector = `.${BulletLegend.selectors.item}`;
|
||||
nextTick(() => {
|
||||
const elements = elRef.value?.querySelectorAll(selector);
|
||||
const classes = buttonVariants({ variant: "ghost", size: "xs" }).split(" ");
|
||||
|
||||
elements?.forEach((el) =>
|
||||
el.classList.add(...classes, "!inline-flex", "!mr-2"),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
keepStyling();
|
||||
});
|
||||
|
||||
function onLegendItemClick(d, i) {
|
||||
emits("legendItemClick", d, i);
|
||||
const isBulletActive = !props.items[i].inactive;
|
||||
const isFilterApplied = props.items.some((i) => i.inactive);
|
||||
if (isFilterApplied && isBulletActive) {
|
||||
// reset filter
|
||||
emits(
|
||||
"update:items",
|
||||
props.items.map((item) => ({ ...item, inactive: false })),
|
||||
);
|
||||
} else {
|
||||
// apply selection, set other item as inactive
|
||||
emits(
|
||||
"update:items",
|
||||
props.items.map((item) =>
|
||||
item.name === d.name
|
||||
? { ...d, inactive: false }
|
||||
: { ...item, inactive: true },
|
||||
),
|
||||
);
|
||||
}
|
||||
keepStyling();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="elRef"
|
||||
class="w-max"
|
||||
:style="{
|
||||
'--vis-legend-bullet-size': '16px',
|
||||
}"
|
||||
>
|
||||
<VisBulletLegend :items="items" :on-legend-item-click="onLegendItemClick" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,73 @@
|
||||
<script setup>
|
||||
import { omit } from "@unovis/ts";
|
||||
import { VisTooltip } from "@unovis/vue";
|
||||
import { createApp } from "vue";
|
||||
import { ChartTooltip } from ".";
|
||||
|
||||
const props = defineProps({
|
||||
selector: { type: String, required: true },
|
||||
index: { type: String, required: true },
|
||||
items: { type: Array, required: false },
|
||||
valueFormatter: { type: Function, required: false },
|
||||
customTooltip: { type: null, required: false },
|
||||
});
|
||||
|
||||
// Use weakmap to store reference to each datapoint for Tooltip
|
||||
const wm = new WeakMap();
|
||||
function template(d, i, elements) {
|
||||
const valueFormatter = props.valueFormatter ?? ((tick) => `${tick}`);
|
||||
if (props.index in d) {
|
||||
if (wm.has(d)) {
|
||||
return wm.get(d);
|
||||
} else {
|
||||
const componentDiv = document.createElement("div");
|
||||
const omittedData = Object.entries(omit(d, [props.index])).map(
|
||||
([key, value]) => {
|
||||
const legendReference = props.items?.find((i) => i.name === key);
|
||||
return { ...legendReference, value: valueFormatter(value) };
|
||||
},
|
||||
);
|
||||
const TooltipComponent = props.customTooltip ?? ChartTooltip;
|
||||
createApp(TooltipComponent, {
|
||||
title: d[props.index],
|
||||
data: omittedData,
|
||||
}).mount(componentDiv);
|
||||
wm.set(d, componentDiv.innerHTML);
|
||||
return componentDiv.innerHTML;
|
||||
}
|
||||
} else {
|
||||
const data = d.data;
|
||||
|
||||
if (wm.has(data)) {
|
||||
return wm.get(data);
|
||||
} else {
|
||||
const style = getComputedStyle(elements[i]);
|
||||
const omittedData = [
|
||||
{
|
||||
name: data.name,
|
||||
value: valueFormatter(data[props.index]),
|
||||
color: style.fill,
|
||||
},
|
||||
];
|
||||
const componentDiv = document.createElement("div");
|
||||
const TooltipComponent = props.customTooltip ?? ChartTooltip;
|
||||
createApp(TooltipComponent, {
|
||||
title: d[props.index],
|
||||
data: omittedData,
|
||||
}).mount(componentDiv);
|
||||
wm.set(d, componentDiv.innerHTML);
|
||||
return componentDiv.innerHTML;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VisTooltip
|
||||
:horizontal-shift="20"
|
||||
:vertical-shift="20"
|
||||
:triggers="{
|
||||
[selector]: template,
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,41 @@
|
||||
<script setup>
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/Components/ui/card';
|
||||
|
||||
defineProps({
|
||||
title: { type: String, required: false },
|
||||
data: { type: Array, required: true },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card class="text-sm">
|
||||
<CardHeader v-if="title" class="p-3 border-b">
|
||||
<CardTitle>
|
||||
{{ title }}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="p-3 min-w-[180px] flex flex-col gap-1">
|
||||
<div v-for="(item, key) in data" :key="key" class="flex justify-between">
|
||||
<div class="flex items-center">
|
||||
<span class="w-2.5 h-2.5 mr-2">
|
||||
<svg width="100%" height="100%" viewBox="0 0 30 30">
|
||||
<path
|
||||
d=" M 15 15 m -14, 0 a 14,14 0 1,1 28,0 a 14,14 0 1,1 -28,0"
|
||||
:stroke="item.color"
|
||||
:fill="item.color"
|
||||
stroke-width="1"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>{{ item.name }}</span>
|
||||
</div>
|
||||
<span class="font-semibold ml-4">{{ item.value }}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</template>
|
||||
@@ -0,0 +1,85 @@
|
||||
<script setup>
|
||||
// Advanced tooltip component (original implementation) inspired by external patterns.
|
||||
import { computed } from 'vue';
|
||||
const props = defineProps({
|
||||
hideLabel: { type: Boolean, default: false },
|
||||
hideIndicator: { type: Boolean, default: false },
|
||||
indicator: { type: String, default: 'dot' }, // 'dot' | 'line' | 'dashed'
|
||||
nameKey: { type: String, required: false },
|
||||
labelKey: { type: String, required: false },
|
||||
labelFormatter: { type: Function, required: false },
|
||||
payload: { type: Object, required: false, default: () => ({}) },
|
||||
config: { type: Object, required: false, default: () => ({}) },
|
||||
class: { type: [String, Array, Object], required: false },
|
||||
color: { type: String, required: false },
|
||||
x: { type: [Number, Date, String], required: false },
|
||||
});
|
||||
|
||||
// Build array of entries referencing config for label & color
|
||||
const entries = computed(() => {
|
||||
return Object.entries(props.payload)
|
||||
.map(([key, value]) => {
|
||||
const seriesKey = props.nameKey || key;
|
||||
const itemConfig = props.config[seriesKey] || props.config[key] || {};
|
||||
const indicatorColor = itemConfig.color || props.color;
|
||||
return { key, value, itemConfig, indicatorColor };
|
||||
})
|
||||
.filter(e => e.itemConfig && (e.itemConfig.label || e.value !== undefined));
|
||||
});
|
||||
|
||||
const singleSeries = computed(() => entries.value.length === 1 && props.indicator !== 'dot');
|
||||
|
||||
const formattedLabel = computed(() => {
|
||||
if (props.hideLabel) return null;
|
||||
if (props.labelFormatter && props.x !== undefined) {
|
||||
return props.labelFormatter(props.x);
|
||||
}
|
||||
if (props.labelKey) {
|
||||
const cfg = props.config[props.labelKey];
|
||||
return cfg?.label || props.payload[props.labelKey];
|
||||
}
|
||||
return props.x instanceof Date ? props.x.toLocaleDateString() : props.x;
|
||||
});
|
||||
|
||||
function formatValue(v) {
|
||||
if (v == null) return '';
|
||||
if (typeof v === 'number') return v.toLocaleString();
|
||||
return v;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['border border-border/50 bg-background min-w-32 rounded-lg px-2.5 py-1.5 text-xs shadow-xl', props.class]">
|
||||
<div v-if="!singleSeries && formattedLabel" class="font-medium mb-1">{{ formattedLabel }}</div>
|
||||
<div class="grid gap-1.5">
|
||||
<div
|
||||
v-for="{ key, value, itemConfig, indicatorColor } in entries"
|
||||
:key="key"
|
||||
class="flex w-full flex-wrap items-stretch gap-2"
|
||||
:class="indicator === 'dot' ? 'items-center' : 'items-start'"
|
||||
>
|
||||
<!-- Indicator -->
|
||||
<template v-if="!hideIndicator">
|
||||
<div
|
||||
:class="[
|
||||
'shrink-0 rounded-[2px] border-border',
|
||||
indicator === 'dot' && 'h-2.5 w-2.5',
|
||||
indicator === 'line' && 'w-1 h-4',
|
||||
indicator === 'dashed' && 'w-0 h-4 border-[1.5px] border-dashed bg-transparent',
|
||||
singleSeries && indicator === 'dashed' && 'my-0.5'
|
||||
]"
|
||||
:style="{ '--color-bg': indicatorColor, '--color-border': indicatorColor }"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div :class="['flex flex-1 justify-between leading-none', singleSeries ? 'items-end' : 'items-center']">
|
||||
<div class="grid gap-1.5">
|
||||
<div v-if="singleSeries && formattedLabel" class="font-medium">{{ formattedLabel }}</div>
|
||||
<span class="text-muted-foreground">{{ itemConfig.label || formatValue(value) }}</span>
|
||||
</div>
|
||||
<span v-if="value !== undefined" class="font-mono font-medium tabular-nums">{{ formatValue(value) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,38 @@
|
||||
import { createApp, h } from 'vue'
|
||||
|
||||
// Simple cache map to avoid re-rendering identical payloads.
|
||||
const _cache = new Map()
|
||||
|
||||
function serializeKey(obj) {
|
||||
try {
|
||||
return JSON.stringify(obj, Object.keys(obj).sort())
|
||||
} catch (e) {
|
||||
return Math.random().toString(36)
|
||||
}
|
||||
}
|
||||
|
||||
// Factory returning template function for Unovis Crosshair.
|
||||
// config: chart series configuration
|
||||
// Component: Vue component to render
|
||||
// extraProps: static props (e.g. labelKey, labelFormatter)
|
||||
export function componentToString(config, Component, extraProps = {}) {
|
||||
return function (_data, x) {
|
||||
const row = _data && _data.data ? _data.data : _data
|
||||
// Build series-only payload (exclude non-config fields like date/dateLabel)
|
||||
const seriesPayload = {}
|
||||
Object.keys(config).forEach(k => {
|
||||
if (row && row[k] !== undefined) seriesPayload[k] = row[k]
|
||||
})
|
||||
const cacheKeyBase = { ...seriesPayload, __x: x }
|
||||
const key = serializeKey(cacheKeyBase)
|
||||
if (_cache.has(key)) return _cache.get(key)
|
||||
|
||||
const el = document.createElement('div')
|
||||
const app = createApp(Component, { ...extraProps, payload: seriesPayload, config, x: row?.date ?? x })
|
||||
app.mount(el)
|
||||
const html = el.innerHTML
|
||||
app.unmount()
|
||||
_cache.set(key, html)
|
||||
return html
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
export { default as ChartCrosshair } from "./ChartCrosshair.vue";
|
||||
export { default as ChartLegend } from "./ChartLegend.vue";
|
||||
export { default as ChartSingleTooltip } from "./ChartSingleTooltip.vue";
|
||||
export { default as ChartTooltip } from "./ChartTooltip.vue";
|
||||
export { default as ChartContainer } from "./ChartContainer.vue";
|
||||
export { default as ChartTooltipContent } from "./ChartTooltipContent.vue";
|
||||
export { componentToString } from "./componentToString";
|
||||
export { provideChartContext, useChartContext } from "./interface";
|
||||
export { default as ChartAutoLegend } from "./ChartAutoLegend.vue";
|
||||
|
||||
export function defaultColors(count = 3) {
|
||||
const quotient = Math.floor(count / 2);
|
||||
const remainder = count % 2;
|
||||
|
||||
const primaryCount = quotient + remainder;
|
||||
const secondaryCount = quotient;
|
||||
return [
|
||||
...Array.from(new Array(primaryCount).keys()).map(
|
||||
(i) => `hsl(var(--vis-primary-color) / ${1 - (1 / primaryCount) * i})`,
|
||||
),
|
||||
...Array.from(new Array(secondaryCount).keys()).map(
|
||||
(i) =>
|
||||
`hsl(var(--vis-secondary-color) / ${1 - (1 / secondaryCount) * i})`,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
export * from "./interface";
|
||||
@@ -0,0 +1,49 @@
|
||||
// Chart interface and context helpers
|
||||
// This is a fresh, original implementation inspired conceptually by patterns
|
||||
// observed in external registries. No code copied.
|
||||
|
||||
import { inject, provide, reactive } from 'vue';
|
||||
|
||||
/**
|
||||
* @typedef {Object} ChartSeriesConfig
|
||||
* @property {string|import('vue').Component} [label] Display label or component
|
||||
* @property {import('vue').Component} [icon] Optional icon component
|
||||
* @property {string} [color] Static CSS color value (e.g. 'var(--chart-1)')
|
||||
* @property {Object} [theme] Optional theme map: { light: string, dark: string }
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object.<string, ChartSeriesConfig>} ChartConfig
|
||||
* Keys are series identifiers. Each value declares label/icon and either a
|
||||
* static color or a theme object with light/dark variants.
|
||||
*/
|
||||
|
||||
const ChartContextSymbol = Symbol('ChartContext');
|
||||
let _idCounter = 0;
|
||||
|
||||
/**
|
||||
* Provide chart context for descendants.
|
||||
* @param {ChartConfig} config Reactive or plain config object.
|
||||
* @param {string} [explicitId] Optional id override.
|
||||
* @returns {{ id: string, config: ChartConfig }}
|
||||
*/
|
||||
export function provideChartContext(config, explicitId) {
|
||||
const id = explicitId || `c${Date.now().toString(36)}${(++_idCounter).toString(36)}`;
|
||||
const ctx = { id, config: reactive(config) };
|
||||
provide(ChartContextSymbol, ctx);
|
||||
return ctx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject previously provided chart context.
|
||||
* @returns {{ id: string, config: ChartConfig }}
|
||||
*/
|
||||
export function useChartContext() {
|
||||
const ctx = inject(ChartContextSymbol, null);
|
||||
if (!ctx) {
|
||||
throw new Error('useChartContext() called without a provider. Wrap in <ChartContainer>.');
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
export {}; // preserve module boundaries
|
||||
@@ -0,0 +1,24 @@
|
||||
<script setup>
|
||||
import { Primitive } from "reka-ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { itemVariants } from ".";
|
||||
|
||||
const props = defineProps({
|
||||
asChild: { type: Boolean, required: false },
|
||||
as: { type: null, required: false, default: "div" },
|
||||
class: { type: null, required: false },
|
||||
variant: { type: null, required: false },
|
||||
size: { type: null, required: false },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive
|
||||
data-slot="item"
|
||||
:as="as"
|
||||
:as-child="asChild"
|
||||
:class="cn(itemVariants({ variant, size }), props.class)"
|
||||
>
|
||||
<slot />
|
||||
</Primitive>
|
||||
</template>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup>
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps({
|
||||
class: { type: null, required: false },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="item-actions"
|
||||
:class="cn('flex items-center gap-2', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup>
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps({
|
||||
class: { type: null, required: false },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="item-content"
|
||||
:class="
|
||||
cn(
|
||||
'flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script setup>
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps({
|
||||
class: { type: null, required: false },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p
|
||||
data-slot="item-description"
|
||||
:class="
|
||||
cn(
|
||||
'text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance',
|
||||
'[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</p>
|
||||
</template>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script setup>
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps({
|
||||
class: { type: null, required: false },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="item-footer"
|
||||
:class="
|
||||
cn('flex basis-full items-center justify-between gap-2', props.class)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script setup>
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps({
|
||||
class: { type: null, required: false },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
role="list"
|
||||
data-slot="item-group"
|
||||
:class="cn('group/item-group flex flex-col', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script setup>
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps({
|
||||
class: { type: null, required: false },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="item-header"
|
||||
:class="
|
||||
cn('flex basis-full items-center justify-between gap-2', props.class)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script setup>
|
||||
import { cn } from "@/lib/utils";
|
||||
import { itemMediaVariants } from ".";
|
||||
|
||||
const props = defineProps({
|
||||
class: { type: null, required: false },
|
||||
variant: { type: null, required: false },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="item-media"
|
||||
:data-variant="props.variant"
|
||||
:class="cn(itemMediaVariants({ variant }), props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script setup>
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Separator } from '@/Components/ui/separator';
|
||||
|
||||
const props = defineProps({
|
||||
orientation: { type: String, required: false },
|
||||
decorative: { type: Boolean, required: false },
|
||||
asChild: { type: Boolean, required: false },
|
||||
as: { type: null, required: false },
|
||||
class: { type: null, required: false },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Separator
|
||||
data-slot="item-separator"
|
||||
orientation="horizontal"
|
||||
:class="cn('my-0', props.class)"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup>
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps({
|
||||
class: { type: null, required: false },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="item-title"
|
||||
:class="
|
||||
cn(
|
||||
'flex w-fit items-center gap-2 text-sm leading-snug font-medium',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,50 @@
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
export { default as Item } from "./Item.vue";
|
||||
export { default as ItemActions } from "./ItemActions.vue";
|
||||
export { default as ItemContent } from "./ItemContent.vue";
|
||||
export { default as ItemDescription } from "./ItemDescription.vue";
|
||||
export { default as ItemFooter } from "./ItemFooter.vue";
|
||||
export { default as ItemGroup } from "./ItemGroup.vue";
|
||||
export { default as ItemHeader } from "./ItemHeader.vue";
|
||||
export { default as ItemMedia } from "./ItemMedia.vue";
|
||||
export { default as ItemSeparator } from "./ItemSeparator.vue";
|
||||
export { default as ItemTitle } from "./ItemTitle.vue";
|
||||
|
||||
export const itemVariants = cva(
|
||||
"group/item flex items-center border border-transparent text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring focus-visible:ring-1",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline: "border-border",
|
||||
muted: "bg-muted/50",
|
||||
},
|
||||
size: {
|
||||
default: "p-4 gap-4 ",
|
||||
sm: "py-3 px-4 gap-2.5",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export const itemMediaVariants = cva(
|
||||
"flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none group-has-[[data-slot=item-description]]/item:translate-y-0.5",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
icon: "size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4",
|
||||
image:
|
||||
"size-10 rounded-sm overflow-hidden [&_img]:size-full [&_img]:object-cover",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup>
|
||||
import { reactiveOmit } from "@vueuse/core";
|
||||
import { ScrollAreaCorner, ScrollAreaRoot, ScrollAreaViewport } from "reka-ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
import ScrollBar from "./ScrollBar.vue";
|
||||
|
||||
const props = defineProps({
|
||||
type: { type: String, required: false },
|
||||
dir: { type: String, required: false },
|
||||
scrollHideDelay: { type: Number, required: false },
|
||||
asChild: { type: Boolean, required: false },
|
||||
as: { type: null, required: false },
|
||||
class: { type: null, required: false },
|
||||
});
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ScrollAreaRoot
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('relative overflow-hidden', props.class)"
|
||||
>
|
||||
<ScrollAreaViewport class="h-full w-full rounded-[inherit]">
|
||||
<slot />
|
||||
</ScrollAreaViewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaCorner />
|
||||
</ScrollAreaRoot>
|
||||
</template>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup>
|
||||
import { reactiveOmit } from "@vueuse/core";
|
||||
import { ScrollAreaScrollbar, ScrollAreaThumb } from "reka-ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps({
|
||||
orientation: { type: String, required: false, default: "vertical" },
|
||||
forceMount: { type: Boolean, required: false },
|
||||
asChild: { type: Boolean, required: false },
|
||||
as: { type: null, required: false },
|
||||
class: { type: null, required: false },
|
||||
});
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ScrollAreaScrollbar
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
cn(
|
||||
'flex touch-none select-none transition-colors',
|
||||
orientation === 'vertical' &&
|
||||
'h-full w-2.5 border-l border-l-transparent p-px',
|
||||
orientation === 'horizontal' &&
|
||||
'h-2.5 flex-col border-t border-t-transparent p-px',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<ScrollAreaThumb class="relative flex-1 rounded-full bg-border" />
|
||||
</ScrollAreaScrollbar>
|
||||
</template>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as ScrollArea } from "./ScrollArea.vue";
|
||||
export { default as ScrollBar } from "./ScrollBar.vue";
|
||||
Reference in New Issue
Block a user