Skip to content

Commit 84969c3

Browse files
committed
next: updated to shadcn-svelte to 1.0
1 parent 4d69044 commit 84969c3

File tree

17 files changed

+397
-23
lines changed

17 files changed

+397
-23
lines changed

src/Exceptionless.Web/ClientApp/components.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"$schema": "https://next.shadcn-svelte.com/schema.json",
2+
"$schema": "https://shadcn-svelte.com/schema.json",
33
"tailwind": {
44
"css": "src/app.css",
55
"baseColor": "zinc"
@@ -12,5 +12,5 @@
1212
"lib": "$lib"
1313
},
1414
"typescript": true,
15-
"registry": "https://next.shadcn-svelte.com/registry"
15+
"registry": "https://shadcn-svelte.com/registry"
1616
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<script lang="ts">
2+
import { cn, type WithElementRef } from "$lib/utils.js";
3+
import type { HTMLAttributes } from "svelte/elements";
4+
import ChartStyle from "./chart-style.svelte";
5+
import { setChartContext, type ChartConfig } from "./chart-utils.js";
6+
7+
const uid = $props.id();
8+
9+
let {
10+
ref = $bindable(null),
11+
id = uid,
12+
class: className,
13+
children,
14+
config,
15+
...restProps
16+
}: WithElementRef<HTMLAttributes<HTMLElement>> & {
17+
config: ChartConfig;
18+
} = $props();
19+
20+
const chartId = `chart-${id || uid.replace(/:/g, "")}`;
21+
22+
setChartContext({
23+
get config() {
24+
return config;
25+
},
26+
});
27+
</script>
28+
29+
<div
30+
bind:this={ref}
31+
data-chart={chartId}
32+
data-slot="chart"
33+
class={cn(
34+
"flex aspect-video justify-center overflow-visible text-xs",
35+
// Overrides
36+
//
37+
// Stroke around dots/marks when hovering
38+
"[&_.stroke-white]:stroke-transparent",
39+
// override the default stroke color of lines
40+
"[&_.lc-line]:stroke-border/50",
41+
42+
// by default, layerchart shows a line intersecting the point when hovering, this hides that
43+
"[&_.lc-highlight-line]:stroke-0",
44+
45+
// by default, when you hover a point on a stacked series chart, it will drop the opacity
46+
// of the other series, this overrides that
47+
"[&_.lc-area-path]:opacity-100 [&_.lc-highlight-line]:opacity-100 [&_.lc-highlight-point]:opacity-100 [&_.lc-spline-path]:opacity-100 [&_.lc-text-svg]:overflow-visible [&_.lc-text]:text-xs",
48+
49+
// We don't want the little tick lines between the axis labels and the chart, so we remove
50+
// the stroke. The alternative is to manually disable `tickMarks` on the x/y axis of every
51+
// chart.
52+
"[&_.lc-axis-tick]:stroke-0",
53+
54+
// We don't want to display the rule on the x/y axis, as there is already going to be
55+
// a grid line there and rule ends up overlapping the marks because it is rendered after
56+
// the marks
57+
"[&_.lc-rule-x-line:not(.lc-grid-x-rule)]:stroke-0 [&_.lc-rule-y-line:not(.lc-grid-y-rule)]:stroke-0",
58+
"[&_.lc-grid-x-radial-line]:stroke-border [&_.lc-grid-x-radial-circle]:stroke-border",
59+
"[&_.lc-grid-y-radial-line]:stroke-border [&_.lc-grid-y-radial-circle]:stroke-border",
60+
61+
// Legend adjustments
62+
"[&_.lc-legend-swatch-button]:items-center [&_.lc-legend-swatch-button]:gap-1.5",
63+
"[&_.lc-legend-swatch-group]:items-center [&_.lc-legend-swatch-group]:gap-4",
64+
"[&_.lc-legend-swatch]:size-2.5 [&_.lc-legend-swatch]:rounded-[2px]",
65+
66+
// Labels
67+
"[&_.lc-labels-text:not([fill])]:fill-foreground [&_text]:stroke-transparent",
68+
69+
// Tick labels on th x/y axes
70+
"[&_.lc-axis-tick-label]:fill-muted-foreground [&_.lc-axis-tick-label]:font-normal",
71+
"[&_.lc-tooltip-rects-g]:fill-transparent",
72+
"[&_.lc-layout-svg-g]:fill-transparent",
73+
"[&_.lc-root-container]:w-full",
74+
className
75+
)}
76+
{...restProps}
77+
>
78+
<ChartStyle id={chartId} {config} />
79+
{@render children?.()}
80+
</div>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<script lang="ts">
2+
import { THEMES, type ChartConfig } from "./chart-utils.js";
3+
4+
let { id, config }: { id: string; config: ChartConfig } = $props();
5+
6+
const colorConfig = $derived(
7+
config ? Object.entries(config).filter(([, config]) => config.theme || config.color) : null
8+
);
9+
10+
const styleOpen = ">elyts<".split("").reverse().join("");
11+
const styleClose = ">elyts/<".split("").reverse().join("");
12+
</script>
13+
14+
{#if colorConfig && colorConfig.length}
15+
{@const themeContents = Object.entries(THEMES)
16+
.map(
17+
([theme, prefix]) => `
18+
${prefix} [data-chart=${id}] {
19+
${colorConfig
20+
.map(([key, itemConfig]) => {
21+
const color =
22+
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color;
23+
return color ? ` --color-${key}: ${color};` : null;
24+
})
25+
.join("\n")}
26+
}
27+
`
28+
)
29+
.join("\n")}
30+
31+
{#key id}
32+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
33+
{@html `${styleOpen}
34+
${themeContents}
35+
${styleClose}`}
36+
{/key}
37+
{/if}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
<script lang="ts">
2+
import { cn, type WithElementRef, type WithoutChildren } from "$lib/utils.js";
3+
import type { HTMLAttributes } from "svelte/elements";
4+
import { getPayloadConfigFromPayload, useChart, type TooltipPayload } from "./chart-utils.js";
5+
import { getTooltipContext, Tooltip as TooltipPrimitive } from "layerchart";
6+
import type { Snippet } from "svelte";
7+
8+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
9+
function defaultFormatter(value: any, _payload: TooltipPayload[]) {
10+
return `${value}`;
11+
}
12+
13+
let {
14+
ref = $bindable(null),
15+
class: className,
16+
hideLabel = false,
17+
indicator = "dot",
18+
hideIndicator = false,
19+
labelKey,
20+
label,
21+
labelFormatter = defaultFormatter,
22+
labelClassName,
23+
formatter,
24+
nameKey,
25+
color,
26+
...restProps
27+
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> & {
28+
hideLabel?: boolean;
29+
label?: string;
30+
indicator?: "line" | "dot" | "dashed";
31+
nameKey?: string;
32+
labelKey?: string;
33+
hideIndicator?: boolean;
34+
labelClassName?: string;
35+
labelFormatter?: // eslint-disable-next-line @typescript-eslint/no-explicit-any
36+
((value: any, payload: TooltipPayload[]) => string | number | Snippet) | null;
37+
formatter?: Snippet<
38+
[
39+
{
40+
value: unknown;
41+
name: string;
42+
item: TooltipPayload;
43+
index: number;
44+
payload: TooltipPayload[];
45+
},
46+
]
47+
>;
48+
} = $props();
49+
50+
const chart = useChart();
51+
const tooltipCtx = getTooltipContext();
52+
53+
const formattedLabel = $derived.by(() => {
54+
if (hideLabel || !tooltipCtx.payload?.length) return null;
55+
56+
const [item] = tooltipCtx.payload;
57+
const key = labelKey || item?.label || item?.name || "value";
58+
59+
const itemConfig = getPayloadConfigFromPayload(chart.config, item, key);
60+
61+
const value =
62+
!labelKey && typeof label === "string"
63+
? chart.config[label as keyof typeof chart.config]?.label || label
64+
: (itemConfig?.label ?? item.label);
65+
66+
if (!value) return null;
67+
if (!labelFormatter) return value;
68+
return labelFormatter(value, tooltipCtx.payload);
69+
});
70+
71+
const nestLabel = $derived(tooltipCtx.payload.length === 1 && indicator !== "dot");
72+
</script>
73+
74+
{#snippet TooltipLabel()}
75+
{#if formattedLabel}
76+
<div class={cn("font-medium", labelClassName)}>
77+
{#if typeof formattedLabel === "function"}
78+
{@render formattedLabel()}
79+
{:else}
80+
{formattedLabel}
81+
{/if}
82+
</div>
83+
{/if}
84+
{/snippet}
85+
86+
<TooltipPrimitive.Root variant="none">
87+
<div
88+
class={cn(
89+
"border-border/50 bg-background grid min-w-[9rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
90+
className
91+
)}
92+
{...restProps}
93+
>
94+
{#if !nestLabel}
95+
{@render TooltipLabel()}
96+
{/if}
97+
<div class="grid gap-1.5">
98+
{#each tooltipCtx.payload as item, i (item.key + i)}
99+
{@const key = `${nameKey || item.key || item.name || "value"}`}
100+
{@const itemConfig = getPayloadConfigFromPayload(chart.config, item, key)}
101+
{@const indicatorColor = color || item.payload?.color || item.color}
102+
<div
103+
class={cn(
104+
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:size-2.5",
105+
indicator === "dot" && "items-center"
106+
)}
107+
>
108+
{#if formatter && item.value !== undefined && item.name}
109+
{@render formatter({
110+
value: item.value,
111+
name: item.name,
112+
item,
113+
index: i,
114+
payload: tooltipCtx.payload,
115+
})}
116+
{:else}
117+
{#if itemConfig?.icon}
118+
<itemConfig.icon />
119+
{:else if !hideIndicator}
120+
<div
121+
style="--color-bg: {indicatorColor}; --color-border: {indicatorColor};"
122+
class={cn(
123+
"border-(--color-border) bg-(--color-bg) shrink-0 rounded-[2px]",
124+
{
125+
"size-2.5": indicator === "dot",
126+
"h-full w-1": indicator === "line",
127+
"w-0 border-[1.5px] border-dashed bg-transparent":
128+
indicator === "dashed",
129+
"my-0.5": nestLabel && indicator === "dashed",
130+
}
131+
)}
132+
></div>
133+
{/if}
134+
<div
135+
class={cn(
136+
"flex flex-1 shrink-0 justify-between leading-none",
137+
nestLabel ? "items-end" : "items-center"
138+
)}
139+
>
140+
<div class="grid gap-1.5">
141+
{#if nestLabel}
142+
{@render TooltipLabel()}
143+
{/if}
144+
<span class="text-muted-foreground">
145+
{itemConfig?.label || item.name}
146+
</span>
147+
</div>
148+
{#if item.value}
149+
<span class="text-foreground font-mono font-medium tabular-nums">
150+
{item.value.toLocaleString()}
151+
</span>
152+
{/if}
153+
</div>
154+
{/if}
155+
</div>
156+
{/each}
157+
</div>
158+
</div>
159+
</TooltipPrimitive.Root>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import type { Tooltip } from "layerchart";
2+
import { getContext, setContext, type Component, type ComponentProps, type Snippet } from "svelte";
3+
4+
export const THEMES = { light: "", dark: ".dark" } as const;
5+
6+
export type ChartConfig = {
7+
[k in string]: {
8+
label?: string;
9+
icon?: Component;
10+
} & (
11+
| { color?: string; theme?: never }
12+
| { color?: never; theme: Record<keyof typeof THEMES, string> }
13+
);
14+
};
15+
16+
export type ExtractSnippetParams<T> = T extends Snippet<[infer P]> ? P : never;
17+
18+
export type TooltipPayload = ExtractSnippetParams<
19+
ComponentProps<typeof Tooltip.Root>["children"]
20+
>["payload"][number];
21+
22+
// Helper to extract item config from a payload.
23+
export function getPayloadConfigFromPayload(
24+
config: ChartConfig,
25+
payload: TooltipPayload,
26+
key: string
27+
) {
28+
if (typeof payload !== "object" || payload === null) return undefined;
29+
30+
const payloadPayload =
31+
"payload" in payload && typeof payload.payload === "object" && payload.payload !== null
32+
? payload.payload
33+
: undefined;
34+
35+
let configLabelKey: string = key;
36+
37+
if (payload.key === key) {
38+
configLabelKey = payload.key;
39+
} else if (payload.name === key) {
40+
configLabelKey = payload.name;
41+
} else if (key in payload && typeof payload[key as keyof typeof payload] === "string") {
42+
configLabelKey = payload[key as keyof typeof payload] as string;
43+
} else if (
44+
payloadPayload &&
45+
key in payloadPayload &&
46+
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
47+
) {
48+
configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;
49+
}
50+
51+
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config];
52+
}
53+
54+
type ChartContextValue = {
55+
config: ChartConfig;
56+
};
57+
58+
const chartContextKey = Symbol("chart-context");
59+
60+
export function setChartContext(value: ChartContextValue) {
61+
return setContext(chartContextKey, value);
62+
}
63+
64+
export function useChart() {
65+
return getContext<ChartContextValue>(chartContextKey);
66+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import ChartContainer from "./chart-container.svelte";
2+
import ChartTooltip from "./chart-tooltip.svelte";
3+
4+
export { getPayloadConfigFromPayload, type ChartConfig } from "./chart-utils.js";
5+
6+
export { ChartContainer, ChartTooltip, ChartContainer as Container, ChartTooltip as Tooltip };

src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/ui/collapsible/collapsible.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@
88
}: CollapsiblePrimitive.RootProps = $props();
99
</script>
1010

11-
<CollapsiblePrimitive.Root bind:ref data-slot="collapsible" {...restProps} />
11+
<CollapsiblePrimitive.Root bind:ref bind:open data-slot="collapsible" {...restProps} />

src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/ui/collapsible/index.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
import { Collapsible as CollapsiblePrimitive } from "bits-ui";
2-
3-
const Root = CollapsiblePrimitive.Root;
4-
const Trigger = CollapsiblePrimitive.Trigger;
5-
const Content = CollapsiblePrimitive.Content;
1+
import Root from "./collapsible.svelte";
2+
import Trigger from "./collapsible-trigger.svelte";
3+
import Content from "./collapsible-content.svelte";
64

75
export {
86
Root,

0 commit comments

Comments
 (0)