@caqophony/ui 0.0.0-stage → 0.2.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.
- package/LICENSE +21 -0
- package/README.md +68 -2
- package/dist/charts.d.mts +117 -0
- package/dist/charts.js +367 -0
- package/dist/chunk-4YA6BZDJ.js +72 -0
- package/dist/index.d.mts +2029 -0
- package/dist/index.js +5224 -0
- package/dist/markdown.d.mts +27 -0
- package/dist/markdown.js +8 -0
- package/package.json +74 -4
- package/styles.css +256 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Caqophony
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,69 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @caqophony/ui
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Generic React UI components built on Gossamer semantic design tokens
|
|
4
|
+
(Tailwind). Maintained centrally by Caqophony and MIT licensed.
|
|
5
|
+
|
|
6
|
+
**This library is optional.** Apps generated by Caqophony may use it so that
|
|
7
|
+
common UI (buttons, cards, dialogs, tables, page layout) is not regenerated
|
|
8
|
+
from scratch for every app, which saves tokens. It is a courtesy, not an SDK:
|
|
9
|
+
nothing in a generated app requires it. If reuse is turned off for a project,
|
|
10
|
+
the app is built without this dependency and its components are written from
|
|
11
|
+
scratch. It contains only generic UI: no platform, product, auth or
|
|
12
|
+
data-access code.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pnpm add @caqophony/ui # or npm i / yarn add
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Peer dependencies: `react` and `react-dom` (`^18 || ^19`). `@caqophony/ui/charts`
|
|
21
|
+
requires the optional peers `chart.js` and `react-chartjs-2`: install them
|
|
22
|
+
yourself if you import that subpath. No `next` dependency.
|
|
23
|
+
|
|
24
|
+
The package is ESM-only. Subpath types (`@caqophony/ui/charts`) resolve through
|
|
25
|
+
the `exports` map, so TypeScript needs `moduleResolution` of `bundler` (or
|
|
26
|
+
`node16`/`nodenext`).
|
|
27
|
+
|
|
28
|
+
`DiceBearAvatar` renders an image from `api.dicebear.com` at runtime, so it
|
|
29
|
+
makes a network request from the user's browser; do not use it where that is
|
|
30
|
+
unacceptable.
|
|
31
|
+
|
|
32
|
+
## Styles (Tailwind v4)
|
|
33
|
+
|
|
34
|
+
Components use Gossamer token classes (`bg-primary`, `text-muted-foreground`,
|
|
35
|
+
`border-intent-error-border`, ...). Tailwind v4 does not scan `node_modules`
|
|
36
|
+
and has no mapping for these names, so import the stylesheet this package
|
|
37
|
+
ships, after Tailwind, in your global CSS:
|
|
38
|
+
|
|
39
|
+
```css
|
|
40
|
+
@import 'tailwindcss';
|
|
41
|
+
@import '@caqophony/ui/styles.css';
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`styles.css` defines the token variables (light, and dark under
|
|
45
|
+
`@media (prefers-color-scheme: dark)`, the same OS setting Tailwind v4's `dark:`
|
|
46
|
+
variant follows), maps them to
|
|
47
|
+
Tailwind v4 colour/radius/z-index/opacity/duration utilities and registers the
|
|
48
|
+
package's built files as a Tailwind `@source`. Override any `--token` after the
|
|
49
|
+
import to re-theme.
|
|
50
|
+
|
|
51
|
+
Tailwind v3 consumers: add `./node_modules/@caqophony/ui/dist/**/*.js` to
|
|
52
|
+
`content` and map the same variables in `theme.extend.colors`.
|
|
53
|
+
|
|
54
|
+
`animate-in` / `animate-out` (used by tooltips) come from `tailwindcss-animate`
|
|
55
|
+
(v3) or `tw-animate-css` (v4); without it those animations are simply absent.
|
|
56
|
+
|
|
57
|
+
## Use
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
import { Button, Card, PageContent } from '@caqophony/ui';
|
|
61
|
+
import { BarChart } from '@caqophony/ui/charts';
|
|
62
|
+
import { MarkdownRenderer } from '@caqophony/ui/markdown';
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Every entry point carries a `'use client'` directive.
|
|
66
|
+
|
|
67
|
+
## Licence
|
|
68
|
+
|
|
69
|
+
MIT, see `LICENSE`.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
export { Chart as ChartJS } from 'chart.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Chart Types - Shared type definitions for chart components
|
|
6
|
+
*
|
|
7
|
+
* Authored by Caqophony
|
|
8
|
+
*/
|
|
9
|
+
interface ChartDataPoint {
|
|
10
|
+
label: string;
|
|
11
|
+
value: number;
|
|
12
|
+
}
|
|
13
|
+
interface TimeSeriesDataPoint {
|
|
14
|
+
date: string | Date;
|
|
15
|
+
value: number;
|
|
16
|
+
secondaryValue?: number;
|
|
17
|
+
}
|
|
18
|
+
interface BaseChartProps {
|
|
19
|
+
/** Height of the chart container */
|
|
20
|
+
height?: number | string;
|
|
21
|
+
/** Whether to show the legend */
|
|
22
|
+
showLegend?: boolean;
|
|
23
|
+
/** Empty state message when no data */
|
|
24
|
+
emptyMessage?: string;
|
|
25
|
+
/** Additional CSS classes */
|
|
26
|
+
className?: string;
|
|
27
|
+
}
|
|
28
|
+
interface LineChartProps extends BaseChartProps {
|
|
29
|
+
data: TimeSeriesDataPoint[];
|
|
30
|
+
/** Label for the primary dataset */
|
|
31
|
+
label?: string;
|
|
32
|
+
/** Label for the secondary dataset (if secondaryValue provided) */
|
|
33
|
+
secondaryLabel?: string;
|
|
34
|
+
/** Primary line color */
|
|
35
|
+
color?: string;
|
|
36
|
+
/** Secondary line color */
|
|
37
|
+
secondaryColor?: string;
|
|
38
|
+
/** Fill area under line */
|
|
39
|
+
fill?: boolean;
|
|
40
|
+
/** Date format for labels */
|
|
41
|
+
dateFormat?: Intl.DateTimeFormatOptions;
|
|
42
|
+
}
|
|
43
|
+
interface BarChartProps extends BaseChartProps {
|
|
44
|
+
data: ChartDataPoint[];
|
|
45
|
+
/** Label for the dataset */
|
|
46
|
+
label?: string;
|
|
47
|
+
/** Bar color */
|
|
48
|
+
color?: string;
|
|
49
|
+
/** Horizontal bar chart */
|
|
50
|
+
horizontal?: boolean;
|
|
51
|
+
}
|
|
52
|
+
interface DoughnutChartProps extends BaseChartProps {
|
|
53
|
+
data: ChartDataPoint[];
|
|
54
|
+
/** Colors for each segment */
|
|
55
|
+
colors?: string[];
|
|
56
|
+
/** Show percentage labels */
|
|
57
|
+
showPercentage?: boolean;
|
|
58
|
+
}
|
|
59
|
+
type NumberFormatMode = 'compact' | 'full' | 'currency';
|
|
60
|
+
|
|
61
|
+
declare function LineChart({ data, label, secondaryLabel, color, secondaryColor, fill, showLegend, height, emptyMessage, dateFormat, className, }: LineChartProps): react_jsx_runtime.JSX.Element;
|
|
62
|
+
|
|
63
|
+
declare function BarChart({ data, label, color, horizontal, showLegend, height, emptyMessage, className, }: BarChartProps): react_jsx_runtime.JSX.Element;
|
|
64
|
+
|
|
65
|
+
declare function DoughnutChart({ data, colors, showPercentage, showLegend, height, emptyMessage, className, }: DoughnutChartProps): react_jsx_runtime.JSX.Element;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* ChartSkeleton - Loading state for chart components
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```tsx
|
|
72
|
+
* import { ChartSkeleton } from '@caqophony/ui/charts';
|
|
73
|
+
*
|
|
74
|
+
* {isLoading ? <ChartSkeleton /> : <LineChart data={data} />}
|
|
75
|
+
* ```
|
|
76
|
+
*
|
|
77
|
+
* Authored by Caqophony
|
|
78
|
+
*/
|
|
79
|
+
interface ChartSkeletonProps {
|
|
80
|
+
height?: number | string;
|
|
81
|
+
className?: string;
|
|
82
|
+
}
|
|
83
|
+
declare function ChartSkeleton({ height, className }: ChartSkeletonProps): react_jsx_runtime.JSX.Element;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Chart Utilities - Shared formatting and helper functions
|
|
87
|
+
*
|
|
88
|
+
* Authored by Caqophony
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Format a number for display in charts (compact: 1K, 1M, etc.)
|
|
93
|
+
*/
|
|
94
|
+
declare function formatNumber(value: number, mode?: NumberFormatMode): string;
|
|
95
|
+
/**
|
|
96
|
+
* Format a date for chart labels
|
|
97
|
+
*/
|
|
98
|
+
declare function formatDateLabel(date: string | Date, options?: Intl.DateTimeFormatOptions): string;
|
|
99
|
+
/**
|
|
100
|
+
* Generate colors for chart segments
|
|
101
|
+
*/
|
|
102
|
+
declare function generateColors(count: number): string[];
|
|
103
|
+
/**
|
|
104
|
+
* Default chart colors
|
|
105
|
+
*/
|
|
106
|
+
declare const CHART_COLORS: {
|
|
107
|
+
readonly primary: "rgb(59, 130, 246)";
|
|
108
|
+
readonly primaryFill: "rgba(59, 130, 246, 0.1)";
|
|
109
|
+
readonly secondary: "rgb(147, 51, 234)";
|
|
110
|
+
readonly secondaryFill: "rgba(147, 51, 234, 0.1)";
|
|
111
|
+
readonly success: "rgb(34, 197, 94)";
|
|
112
|
+
readonly warning: "rgb(234, 179, 8)";
|
|
113
|
+
readonly danger: "rgb(239, 68, 68)";
|
|
114
|
+
readonly gray: "rgb(107, 114, 128)";
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export { BarChart, type BarChartProps, type BaseChartProps, CHART_COLORS, type ChartDataPoint, ChartSkeleton, DoughnutChart, type DoughnutChartProps, LineChart, type LineChartProps, type NumberFormatMode, type TimeSeriesDataPoint, formatDateLabel, formatNumber, generateColors };
|
package/dist/charts.js
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// src/charts/line-chart.tsx
|
|
4
|
+
import { useMemo } from "react";
|
|
5
|
+
import { Line } from "react-chartjs-2";
|
|
6
|
+
|
|
7
|
+
// src/charts/utils.ts
|
|
8
|
+
function formatNumber(value, mode = "compact") {
|
|
9
|
+
if (mode === "currency") {
|
|
10
|
+
return new Intl.NumberFormat("en-US", {
|
|
11
|
+
style: "currency",
|
|
12
|
+
currency: "USD",
|
|
13
|
+
minimumFractionDigits: 0,
|
|
14
|
+
maximumFractionDigits: 0
|
|
15
|
+
}).format(value);
|
|
16
|
+
}
|
|
17
|
+
if (mode === "full") {
|
|
18
|
+
return value.toLocaleString();
|
|
19
|
+
}
|
|
20
|
+
if (value >= 1e6) {
|
|
21
|
+
return `${(value / 1e6).toFixed(1)}M`;
|
|
22
|
+
}
|
|
23
|
+
if (value >= 1e3) {
|
|
24
|
+
return `${Math.round(value / 1e3)}K`;
|
|
25
|
+
}
|
|
26
|
+
return value.toString();
|
|
27
|
+
}
|
|
28
|
+
function formatDateLabel(date, options = { month: "short", day: "numeric" }) {
|
|
29
|
+
const d = typeof date === "string" ? new Date(date) : date;
|
|
30
|
+
return d.toLocaleDateString("en-US", options);
|
|
31
|
+
}
|
|
32
|
+
function generateColors(count) {
|
|
33
|
+
return Array.from({ length: count }, (_, i) => {
|
|
34
|
+
const hue = i * 360 / count;
|
|
35
|
+
return `hsla(${hue}, 70%, 60%, 0.8)`;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
var CHART_COLORS = {
|
|
39
|
+
primary: "rgb(59, 130, 246)",
|
|
40
|
+
// blue-500
|
|
41
|
+
primaryFill: "rgba(59, 130, 246, 0.1)",
|
|
42
|
+
secondary: "rgb(147, 51, 234)",
|
|
43
|
+
// purple-600
|
|
44
|
+
secondaryFill: "rgba(147, 51, 234, 0.1)",
|
|
45
|
+
success: "rgb(34, 197, 94)",
|
|
46
|
+
// green-500
|
|
47
|
+
warning: "rgb(234, 179, 8)",
|
|
48
|
+
// yellow-500
|
|
49
|
+
danger: "rgb(239, 68, 68)",
|
|
50
|
+
// red-500
|
|
51
|
+
gray: "rgb(107, 114, 128)"
|
|
52
|
+
// gray-500
|
|
53
|
+
};
|
|
54
|
+
var BASE_CHART_OPTIONS = {
|
|
55
|
+
responsive: true,
|
|
56
|
+
maintainAspectRatio: false,
|
|
57
|
+
interaction: {
|
|
58
|
+
intersect: false,
|
|
59
|
+
mode: "index"
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// src/charts/chart-registry.ts
|
|
64
|
+
import {
|
|
65
|
+
Chart as ChartJS,
|
|
66
|
+
CategoryScale,
|
|
67
|
+
LinearScale,
|
|
68
|
+
PointElement,
|
|
69
|
+
LineElement,
|
|
70
|
+
BarElement,
|
|
71
|
+
ArcElement,
|
|
72
|
+
Title,
|
|
73
|
+
Tooltip,
|
|
74
|
+
Legend,
|
|
75
|
+
Filler
|
|
76
|
+
} from "chart.js";
|
|
77
|
+
ChartJS.register(
|
|
78
|
+
CategoryScale,
|
|
79
|
+
LinearScale,
|
|
80
|
+
PointElement,
|
|
81
|
+
LineElement,
|
|
82
|
+
BarElement,
|
|
83
|
+
ArcElement,
|
|
84
|
+
Title,
|
|
85
|
+
Tooltip,
|
|
86
|
+
Legend,
|
|
87
|
+
Filler
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// src/charts/line-chart.tsx
|
|
91
|
+
import { jsx } from "react/jsx-runtime";
|
|
92
|
+
function LineChart({
|
|
93
|
+
data,
|
|
94
|
+
label = "Value",
|
|
95
|
+
secondaryLabel,
|
|
96
|
+
color = CHART_COLORS.primary,
|
|
97
|
+
secondaryColor = CHART_COLORS.secondary,
|
|
98
|
+
fill = false,
|
|
99
|
+
showLegend = false,
|
|
100
|
+
height = 256,
|
|
101
|
+
emptyMessage = "No data available",
|
|
102
|
+
dateFormat,
|
|
103
|
+
className
|
|
104
|
+
}) {
|
|
105
|
+
const chartData = useMemo(() => {
|
|
106
|
+
const labels = data.map((d) => formatDateLabel(d.date, dateFormat));
|
|
107
|
+
const datasets = [
|
|
108
|
+
{
|
|
109
|
+
label,
|
|
110
|
+
data: data.map((d) => d.value),
|
|
111
|
+
borderColor: color,
|
|
112
|
+
backgroundColor: fill ? color.replace("rgb", "rgba").replace(")", ", 0.1)") : "transparent",
|
|
113
|
+
fill,
|
|
114
|
+
tension: 0.3,
|
|
115
|
+
pointRadius: 3,
|
|
116
|
+
pointHoverRadius: 5
|
|
117
|
+
}
|
|
118
|
+
];
|
|
119
|
+
if (data.some((d) => d.secondaryValue !== void 0) && secondaryLabel) {
|
|
120
|
+
datasets.push({
|
|
121
|
+
label: secondaryLabel,
|
|
122
|
+
data: data.map((d) => d.secondaryValue ?? 0),
|
|
123
|
+
borderColor: secondaryColor,
|
|
124
|
+
backgroundColor: "transparent",
|
|
125
|
+
fill: false,
|
|
126
|
+
tension: 0.3,
|
|
127
|
+
pointRadius: 2,
|
|
128
|
+
pointHoverRadius: 5
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return { labels, datasets };
|
|
132
|
+
}, [data, label, secondaryLabel, color, secondaryColor, fill, dateFormat]);
|
|
133
|
+
const options = useMemo(
|
|
134
|
+
() => ({
|
|
135
|
+
...BASE_CHART_OPTIONS,
|
|
136
|
+
plugins: {
|
|
137
|
+
legend: {
|
|
138
|
+
display: showLegend,
|
|
139
|
+
position: "top"
|
|
140
|
+
},
|
|
141
|
+
tooltip: {
|
|
142
|
+
callbacks: {
|
|
143
|
+
label: (context) => {
|
|
144
|
+
const val = context.parsed.y ?? 0;
|
|
145
|
+
return `${context.dataset.label}: ${formatNumber(val)}`;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
scales: {
|
|
151
|
+
x: {
|
|
152
|
+
grid: {
|
|
153
|
+
display: false
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
y: {
|
|
157
|
+
beginAtZero: true,
|
|
158
|
+
ticks: {
|
|
159
|
+
callback: (tickValue) => {
|
|
160
|
+
if (tickValue === null) return "";
|
|
161
|
+
const value = typeof tickValue === "number" ? tickValue : parseFloat(String(tickValue));
|
|
162
|
+
return formatNumber(value);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}),
|
|
168
|
+
[showLegend]
|
|
169
|
+
);
|
|
170
|
+
if (data.length === 0) {
|
|
171
|
+
return /* @__PURE__ */ jsx(
|
|
172
|
+
"div",
|
|
173
|
+
{
|
|
174
|
+
className: `flex items-center justify-center text-gray-500 ${className ?? ""}`,
|
|
175
|
+
style: { height },
|
|
176
|
+
children: emptyMessage
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
return /* @__PURE__ */ jsx("div", { className, style: { height }, children: /* @__PURE__ */ jsx(Line, { data: chartData, options }) });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/charts/bar-chart.tsx
|
|
184
|
+
import { useMemo as useMemo2 } from "react";
|
|
185
|
+
import { Bar } from "react-chartjs-2";
|
|
186
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
187
|
+
function BarChart({
|
|
188
|
+
data,
|
|
189
|
+
label = "Value",
|
|
190
|
+
color = CHART_COLORS.primary,
|
|
191
|
+
horizontal = false,
|
|
192
|
+
showLegend = false,
|
|
193
|
+
height = 256,
|
|
194
|
+
emptyMessage = "No data available",
|
|
195
|
+
className
|
|
196
|
+
}) {
|
|
197
|
+
const chartData = useMemo2(() => {
|
|
198
|
+
const labels = data.map((d) => d.label);
|
|
199
|
+
const colors = data.length > 1 ? generateColors(data.length) : [color];
|
|
200
|
+
return {
|
|
201
|
+
labels,
|
|
202
|
+
datasets: [
|
|
203
|
+
{
|
|
204
|
+
label,
|
|
205
|
+
data: data.map((d) => d.value),
|
|
206
|
+
backgroundColor: colors,
|
|
207
|
+
borderColor: colors.map((c) => c.replace("0.8", "1")),
|
|
208
|
+
borderWidth: 1
|
|
209
|
+
}
|
|
210
|
+
]
|
|
211
|
+
};
|
|
212
|
+
}, [data, label, color]);
|
|
213
|
+
const options = useMemo2(
|
|
214
|
+
() => ({
|
|
215
|
+
...BASE_CHART_OPTIONS,
|
|
216
|
+
indexAxis: horizontal ? "y" : "x",
|
|
217
|
+
plugins: {
|
|
218
|
+
legend: {
|
|
219
|
+
display: showLegend
|
|
220
|
+
},
|
|
221
|
+
tooltip: {
|
|
222
|
+
callbacks: {
|
|
223
|
+
label: (context) => {
|
|
224
|
+
const val = horizontal ? context.parsed.x ?? 0 : context.parsed.y ?? 0;
|
|
225
|
+
return `${context.dataset.label}: ${formatNumber(val)}`;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
scales: {
|
|
231
|
+
x: {
|
|
232
|
+
beginAtZero: true,
|
|
233
|
+
ticks: horizontal ? {
|
|
234
|
+
callback: (tickValue) => {
|
|
235
|
+
if (tickValue === null) return "";
|
|
236
|
+
const value = typeof tickValue === "number" ? tickValue : parseFloat(String(tickValue));
|
|
237
|
+
return formatNumber(value);
|
|
238
|
+
}
|
|
239
|
+
} : void 0
|
|
240
|
+
},
|
|
241
|
+
y: {
|
|
242
|
+
beginAtZero: true,
|
|
243
|
+
ticks: !horizontal ? {
|
|
244
|
+
callback: (tickValue) => {
|
|
245
|
+
if (tickValue === null) return "";
|
|
246
|
+
const value = typeof tickValue === "number" ? tickValue : parseFloat(String(tickValue));
|
|
247
|
+
return formatNumber(value);
|
|
248
|
+
}
|
|
249
|
+
} : void 0
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}),
|
|
253
|
+
[horizontal, showLegend]
|
|
254
|
+
);
|
|
255
|
+
if (data.length === 0) {
|
|
256
|
+
return /* @__PURE__ */ jsx2(
|
|
257
|
+
"div",
|
|
258
|
+
{
|
|
259
|
+
className: `flex items-center justify-center text-gray-500 ${className ?? ""}`,
|
|
260
|
+
style: { height },
|
|
261
|
+
children: emptyMessage
|
|
262
|
+
}
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
return /* @__PURE__ */ jsx2("div", { className, style: { height }, children: /* @__PURE__ */ jsx2(Bar, { data: chartData, options }) });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/charts/doughnut-chart.tsx
|
|
269
|
+
import { useMemo as useMemo3 } from "react";
|
|
270
|
+
import { Doughnut } from "react-chartjs-2";
|
|
271
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
272
|
+
function DoughnutChart({
|
|
273
|
+
data,
|
|
274
|
+
colors,
|
|
275
|
+
showPercentage = false,
|
|
276
|
+
showLegend = true,
|
|
277
|
+
height = 256,
|
|
278
|
+
emptyMessage = "No data available",
|
|
279
|
+
className
|
|
280
|
+
}) {
|
|
281
|
+
const total = useMemo3(
|
|
282
|
+
() => data.reduce((sum, d) => sum + d.value, 0),
|
|
283
|
+
[data]
|
|
284
|
+
);
|
|
285
|
+
const chartData = useMemo3(() => {
|
|
286
|
+
const labels = data.map((d) => d.label);
|
|
287
|
+
const chartColors = colors ?? generateColors(data.length);
|
|
288
|
+
return {
|
|
289
|
+
labels,
|
|
290
|
+
datasets: [
|
|
291
|
+
{
|
|
292
|
+
data: data.map((d) => d.value),
|
|
293
|
+
backgroundColor: chartColors,
|
|
294
|
+
borderColor: chartColors.map((c) => c.replace("0.8", "1")),
|
|
295
|
+
borderWidth: 1
|
|
296
|
+
}
|
|
297
|
+
]
|
|
298
|
+
};
|
|
299
|
+
}, [data, colors]);
|
|
300
|
+
const options = useMemo3(
|
|
301
|
+
() => ({
|
|
302
|
+
...BASE_CHART_OPTIONS,
|
|
303
|
+
plugins: {
|
|
304
|
+
legend: {
|
|
305
|
+
display: showLegend,
|
|
306
|
+
position: "right"
|
|
307
|
+
},
|
|
308
|
+
tooltip: {
|
|
309
|
+
callbacks: {
|
|
310
|
+
label: (context) => {
|
|
311
|
+
const val = context.parsed;
|
|
312
|
+
const percentage = total > 0 ? Math.round(val / total * 100) : 0;
|
|
313
|
+
if (showPercentage) {
|
|
314
|
+
return `${context.label}: ${formatNumber(val)} (${percentage}%)`;
|
|
315
|
+
}
|
|
316
|
+
return `${context.label}: ${formatNumber(val)}`;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}),
|
|
322
|
+
[showLegend, showPercentage, total]
|
|
323
|
+
);
|
|
324
|
+
if (data.length === 0) {
|
|
325
|
+
return /* @__PURE__ */ jsx3(
|
|
326
|
+
"div",
|
|
327
|
+
{
|
|
328
|
+
className: `flex items-center justify-center text-gray-500 ${className ?? ""}`,
|
|
329
|
+
style: { height },
|
|
330
|
+
children: emptyMessage
|
|
331
|
+
}
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
return /* @__PURE__ */ jsx3("div", { className, style: { height }, children: /* @__PURE__ */ jsx3(Doughnut, { data: chartData, options }) });
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// src/charts/chart-skeleton.tsx
|
|
338
|
+
import { jsx as jsx4, jsxs } from "react/jsx-runtime";
|
|
339
|
+
function ChartSkeleton({ height = 256, className }) {
|
|
340
|
+
return /* @__PURE__ */ jsx4(
|
|
341
|
+
"div",
|
|
342
|
+
{
|
|
343
|
+
className: `animate-pulse rounded-lg bg-gray-100 ${className ?? ""}`,
|
|
344
|
+
style: { height },
|
|
345
|
+
children: /* @__PURE__ */ jsxs("div", { className: "flex h-full items-end justify-around gap-2 p-4", children: [
|
|
346
|
+
/* @__PURE__ */ jsx4("div", { className: "h-1/3 w-8 rounded-t bg-gray-200" }),
|
|
347
|
+
/* @__PURE__ */ jsx4("div", { className: "h-1/2 w-8 rounded-t bg-gray-200" }),
|
|
348
|
+
/* @__PURE__ */ jsx4("div", { className: "h-2/3 w-8 rounded-t bg-gray-200" }),
|
|
349
|
+
/* @__PURE__ */ jsx4("div", { className: "h-1/2 w-8 rounded-t bg-gray-200" }),
|
|
350
|
+
/* @__PURE__ */ jsx4("div", { className: "h-3/4 w-8 rounded-t bg-gray-200" }),
|
|
351
|
+
/* @__PURE__ */ jsx4("div", { className: "h-1/3 w-8 rounded-t bg-gray-200" }),
|
|
352
|
+
/* @__PURE__ */ jsx4("div", { className: "h-1/2 w-8 rounded-t bg-gray-200" })
|
|
353
|
+
] })
|
|
354
|
+
}
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
export {
|
|
358
|
+
BarChart,
|
|
359
|
+
CHART_COLORS,
|
|
360
|
+
ChartJS,
|
|
361
|
+
ChartSkeleton,
|
|
362
|
+
DoughnutChart,
|
|
363
|
+
LineChart,
|
|
364
|
+
formatDateLabel,
|
|
365
|
+
formatNumber,
|
|
366
|
+
generateColors
|
|
367
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// src/markdown-renderer.tsx
|
|
4
|
+
import { memo } from "react";
|
|
5
|
+
import ReactMarkdown from "react-markdown";
|
|
6
|
+
import remarkGfm from "remark-gfm";
|
|
7
|
+
|
|
8
|
+
// src/utils.ts
|
|
9
|
+
import { clsx } from "clsx";
|
|
10
|
+
import { twMerge } from "tailwind-merge";
|
|
11
|
+
function cn(...inputs) {
|
|
12
|
+
return twMerge(clsx(inputs));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// src/markdown-renderer.tsx
|
|
16
|
+
import { Fragment, jsx } from "react/jsx-runtime";
|
|
17
|
+
var components = {
|
|
18
|
+
// Links open in new tab
|
|
19
|
+
a: ({ children, href, ...props }) => /* @__PURE__ */ jsx("a", { href, target: "_blank", rel: "noopener noreferrer", ...props, children }),
|
|
20
|
+
// Pre blocks pass through children (code block handles its own styling)
|
|
21
|
+
pre: ({ children }) => /* @__PURE__ */ jsx(Fragment, { children })
|
|
22
|
+
};
|
|
23
|
+
var remarkPlugins = [remarkGfm];
|
|
24
|
+
function getProseClasses(size, className) {
|
|
25
|
+
return cn(
|
|
26
|
+
// Base prose sizing
|
|
27
|
+
size === "sm" ? "prose prose-sm" : "prose",
|
|
28
|
+
// Let parent container control width
|
|
29
|
+
"max-w-none",
|
|
30
|
+
// Headings
|
|
31
|
+
"prose-headings:text-foreground dark:prose-headings:text-foreground prose-headings:font-semibold",
|
|
32
|
+
// Body text
|
|
33
|
+
"prose-p:text-foreground dark:prose-p:text-foreground",
|
|
34
|
+
// Strong/bold
|
|
35
|
+
"prose-strong:text-foreground dark:prose-strong:text-foreground",
|
|
36
|
+
// Emphasis/italic
|
|
37
|
+
"prose-em:text-foreground dark:prose-em:text-foreground",
|
|
38
|
+
// Links
|
|
39
|
+
"prose-a:text-primary-text dark:prose-a:text-primary-text prose-a:no-underline hover:prose-a:underline",
|
|
40
|
+
// Lists
|
|
41
|
+
"prose-li:text-foreground dark:prose-li:text-foreground",
|
|
42
|
+
// Inline code
|
|
43
|
+
"prose-code:text-foreground dark:prose-code:text-foreground prose-code:bg-muted dark:prose-code:bg-muted prose-code:rounded prose-code:px-1.5 prose-code:py-0.5 prose-code:text-[0.85em] prose-code:font-normal prose-code:before:content-none prose-code:after:content-none",
|
|
44
|
+
// Code blocks
|
|
45
|
+
"prose-pre:bg-muted dark:prose-pre:bg-muted prose-pre:text-foreground dark:prose-pre:text-foreground prose-pre:rounded-md",
|
|
46
|
+
// Blockquotes
|
|
47
|
+
"prose-blockquote:text-muted-foreground dark:prose-blockquote:text-muted-foreground prose-blockquote:border-border dark:prose-blockquote:border-border",
|
|
48
|
+
// Horizontal rules
|
|
49
|
+
"prose-hr:border-border dark:prose-hr:border-border",
|
|
50
|
+
// Tables
|
|
51
|
+
"prose-th:text-foreground dark:prose-th:text-foreground prose-th:border-border dark:prose-th:border-border",
|
|
52
|
+
"prose-td:text-foreground dark:prose-td:text-foreground prose-td:border-border dark:prose-td:border-border",
|
|
53
|
+
className
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
function NonMemoizedMarkdownRenderer({
|
|
57
|
+
children,
|
|
58
|
+
className,
|
|
59
|
+
size = "sm"
|
|
60
|
+
}) {
|
|
61
|
+
return /* @__PURE__ */ jsx("div", { className: getProseClasses(size, className), children: /* @__PURE__ */ jsx(ReactMarkdown, { remarkPlugins, components, children }) });
|
|
62
|
+
}
|
|
63
|
+
var MarkdownRenderer = memo(
|
|
64
|
+
NonMemoizedMarkdownRenderer,
|
|
65
|
+
(prev, next) => prev.children === next.children && prev.className === next.className && prev.size === next.size
|
|
66
|
+
);
|
|
67
|
+
MarkdownRenderer.displayName = "MarkdownRenderer";
|
|
68
|
+
|
|
69
|
+
export {
|
|
70
|
+
cn,
|
|
71
|
+
MarkdownRenderer
|
|
72
|
+
};
|