Skip to content

Commit 9edeafd

Browse files
committed
LOADS of new components. v1.2 update
1 parent f1d7b67 commit 9edeafd

51 files changed

Lines changed: 2900 additions & 2 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎components/Steps.js‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@ export default function Steps({ items = [] }) {
22
return (
33
<ol className="my-6 space-y-3">
44
{items.map((item, index) => (
5-
<li key={`${item}-${index}`} className="flex gap-3 rounded-lg border border-slate-200 bg-white px-3 py-2 shadow-sm dark:border-slate-800 dark:bg-slate-900">
5+
<li key={`step-${index}`} className="flex gap-3 rounded-lg border border-slate-200 bg-white px-3 py-2 shadow-sm dark:border-slate-800 dark:bg-slate-900">
66
<span className="mt-0.5 inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-blue-600 text-xs font-bold text-white">
77
{index + 1}
88
</span>
9-
<span className="text-sm text-slate-700 dark:text-slate-200">{item}</span>
9+
{typeof item === 'string' ? (
10+
<span className="text-sm text-slate-700 dark:text-slate-200">{item}</span>
11+
) : (
12+
<div className="min-w-0 flex-1">{item}</div>
13+
)}
1014
</li>
1115
))}
1216
</ol>

‎components/charts/AreaChart.js‎

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
function normalizeData(data = []) {
2+
const input = Array.isArray(data) && data.length > 0
3+
? data
4+
: [
5+
{ label: 'Jan', value: 12 },
6+
{ label: 'Feb', value: 16 },
7+
{ label: 'Mar', value: 14 },
8+
{ label: 'Apr', value: 22 },
9+
{ label: 'May', value: 19 },
10+
];
11+
12+
return input
13+
.map((entry, index) => ({
14+
label: entry?.label || `Item ${index + 1}`,
15+
value: Number(entry?.value || 0),
16+
}))
17+
.filter((entry) => Number.isFinite(entry.value));
18+
}
19+
20+
export default function AreaChart({
21+
data = [],
22+
width = 640,
23+
height = 220,
24+
className = '',
25+
showDots = true,
26+
}) {
27+
const points = normalizeData(data);
28+
29+
if (!points.length) {
30+
return (
31+
<div className={`my-4 rounded-lg border border-slate-200 bg-white p-4 text-sm text-slate-500 dark:border-slate-800 dark:bg-slate-950/40 dark:text-slate-400 ${className}`}>
32+
No chart data provided.
33+
</div>
34+
);
35+
}
36+
37+
const paddingX = 26;
38+
const paddingY = 16;
39+
const usableWidth = width - paddingX * 2;
40+
const usableHeight = height - paddingY * 2;
41+
const values = points.map((entry) => entry.value);
42+
const minValue = Math.min(...values);
43+
const maxValue = Math.max(...values);
44+
const range = maxValue - minValue || 1;
45+
46+
const coordinates = points.map((entry, index) => {
47+
const x = paddingX + (index / Math.max(points.length - 1, 1)) * usableWidth;
48+
const y = paddingY + (1 - (entry.value - minValue) / range) * usableHeight;
49+
return { ...entry, x, y };
50+
});
51+
52+
const linePath = coordinates
53+
.map((point, index) => `${index === 0 ? 'M' : 'L'} ${point.x} ${point.y}`)
54+
.join(' ');
55+
56+
const areaPath = `${linePath} L ${paddingX + usableWidth} ${height - paddingY} L ${paddingX} ${height - paddingY} Z`;
57+
58+
return (
59+
<div className={`my-4 rounded-lg border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-950/40 ${className}`}>
60+
<svg viewBox={`0 0 ${width} ${height}`} className="h-56 w-full" role="img" aria-label="Area chart">
61+
<defs>
62+
<linearGradient id="miniwiki-area-gradient" x1="0" y1="0" x2="0" y2="1">
63+
<stop offset="0%" stopColor="currentColor" stopOpacity="0.35" />
64+
<stop offset="100%" stopColor="currentColor" stopOpacity="0.03" />
65+
</linearGradient>
66+
</defs>
67+
68+
<path d={areaPath} fill="url(#miniwiki-area-gradient)" className="text-blue-500 dark:text-blue-300" />
69+
<path d={linePath} fill="none" stroke="currentColor" strokeWidth="3" className="text-blue-600 dark:text-blue-300" />
70+
71+
{showDots
72+
? coordinates.map((point) => (
73+
<circle
74+
key={`${point.label}-${point.value}`}
75+
cx={point.x}
76+
cy={point.y}
77+
r="3.5"
78+
className="fill-blue-600 dark:fill-blue-300"
79+
/>
80+
))
81+
: null}
82+
</svg>
83+
84+
<div className="mt-2 grid grid-flow-col auto-cols-fr gap-2 text-center text-[11px] text-slate-500 dark:text-slate-400">
85+
{points.map((point) => (
86+
<span key={`label-${point.label}`}>{point.label}</span>
87+
))}
88+
</div>
89+
</div>
90+
);
91+
}

‎components/charts/BarChart.js‎

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
function getMaxValue(data = []) {
2+
const values = data
3+
.map((entry) => Number(entry?.value || 0))
4+
.filter((value) => Number.isFinite(value) && value >= 0);
5+
6+
if (!values.length) {
7+
return 1;
8+
}
9+
10+
return Math.max(...values, 1);
11+
}
12+
13+
const FALLBACK_DATA = [
14+
{ label: 'Mon', value: 12 },
15+
{ label: 'Tue', value: 18 },
16+
{ label: 'Wed', value: 15 },
17+
{ label: 'Thu', value: 22 },
18+
{ label: 'Fri', value: 17 },
19+
];
20+
21+
function normalizeData(data) {
22+
if (Array.isArray(data) && data.length > 0) {
23+
return data.map((entry, index) => ({
24+
label: entry?.label || `Item ${index + 1}`,
25+
value: Number(entry?.value || 0),
26+
}));
27+
}
28+
29+
return FALLBACK_DATA;
30+
}
31+
32+
export default function BarChart({
33+
data = [],
34+
height = 180,
35+
showValues = true,
36+
className = '',
37+
}) {
38+
const normalizedData = normalizeData(data);
39+
const maxValue = getMaxValue(normalizedData);
40+
const chartHeight = Math.max(140, Number(height) || 180);
41+
42+
return (
43+
<div className={`my-4 rounded-lg border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-950/40 ${className}`}>
44+
<div className="grid grid-flow-col auto-cols-fr items-end gap-3" style={{ minHeight: `${chartHeight}px` }}>
45+
{normalizedData.map((entry, index) => {
46+
const numericValue = Number(entry?.value || 0);
47+
const safeValue = Number.isFinite(numericValue) && numericValue > 0 ? numericValue : 0;
48+
const ratio = Math.max(0, Math.min(1, safeValue / maxValue));
49+
const fillHeight = Math.max(8, Math.round(ratio * (chartHeight - 68)));
50+
51+
return (
52+
<div key={`${entry?.label || 'item'}-${index}`} className="flex min-w-0 flex-col items-center gap-2">
53+
<div className="flex w-full items-end justify-center" style={{ height: `${Math.max(84, chartHeight - 44)}px` }}>
54+
<div
55+
className="w-full max-w-14 rounded-t-md bg-blue-500/85 transition-all duration-500 ease-out dark:bg-blue-400/80"
56+
style={{ height: `${fillHeight}px` }}
57+
title={`${entry?.label || 'Item'}: ${safeValue}`}
58+
/>
59+
</div>
60+
61+
<div className="w-full text-center">
62+
<p className="truncate text-[11px] font-medium text-slate-600 dark:text-slate-300">
63+
{entry?.label || `Item ${index + 1}`}
64+
</p>
65+
{showValues ? (
66+
<p className="text-[11px] text-slate-500 dark:text-slate-400">{safeValue}</p>
67+
) : null}
68+
</div>
69+
</div>
70+
);
71+
})}
72+
</div>
73+
</div>
74+
);
75+
}

‎components/charts/DonutChart.js‎

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
function toSegments(data = []) {
2+
const normalized = data
3+
.map((entry, index) => ({
4+
label: entry?.label || `Segment ${index + 1}`,
5+
value: Math.max(0, Number(entry?.value || 0)),
6+
colorClass:
7+
typeof entry?.colorClass === 'string' && entry.colorClass.trim()
8+
? entry.colorClass.trim()
9+
: null,
10+
}))
11+
.filter((entry) => Number.isFinite(entry.value));
12+
13+
const total = normalized.reduce((sum, entry) => sum + entry.value, 0);
14+
15+
return {
16+
segments: normalized,
17+
total: total > 0 ? total : 1,
18+
hasValues: total > 0,
19+
};
20+
}
21+
22+
const FALLBACK_DATA = [
23+
{ label: 'Docs', value: 58 },
24+
{ label: 'API', value: 24 },
25+
{ label: 'Ops', value: 18 },
26+
];
27+
28+
function normalizeInputData(data) {
29+
if (Array.isArray(data) && data.length > 0) {
30+
return data;
31+
}
32+
33+
return FALLBACK_DATA;
34+
}
35+
36+
const DEFAULT_COLORS = [
37+
'text-blue-500 dark:text-blue-300',
38+
'text-emerald-500 dark:text-emerald-300',
39+
'text-amber-500 dark:text-amber-300',
40+
'text-purple-500 dark:text-purple-300',
41+
'text-rose-500 dark:text-rose-300',
42+
'text-cyan-500 dark:text-cyan-300',
43+
];
44+
45+
export default function DonutChart({
46+
data = [],
47+
size = 180,
48+
strokeWidth = 22,
49+
showLegend = true,
50+
className = '',
51+
}) {
52+
const normalizedData = normalizeInputData(data);
53+
const { segments, total, hasValues } = toSegments(normalizedData);
54+
const radius = (size - strokeWidth) / 2;
55+
const circumference = 2 * Math.PI * radius;
56+
57+
let offset = 0;
58+
59+
return (
60+
<div className={`my-4 rounded-lg border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-950/40 ${className}`}>
61+
<div className="flex flex-col items-center gap-4 sm:flex-row sm:items-start">
62+
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} role="img" aria-label="Donut chart">
63+
<circle
64+
cx={size / 2}
65+
cy={size / 2}
66+
r={radius}
67+
fill="none"
68+
stroke="currentColor"
69+
className="text-slate-200 dark:text-slate-800"
70+
strokeWidth={strokeWidth}
71+
/>
72+
73+
{hasValues
74+
? segments.map((segment, index) => {
75+
const segmentLength = (segment.value / total) * circumference;
76+
const strokeDasharray = `${segmentLength} ${circumference - segmentLength}`;
77+
const strokeDashoffset = -offset;
78+
offset += segmentLength;
79+
80+
return (
81+
<circle
82+
key={`${segment.label}-${index}`}
83+
cx={size / 2}
84+
cy={size / 2}
85+
r={radius}
86+
fill="none"
87+
stroke="currentColor"
88+
className={segment.colorClass || DEFAULT_COLORS[index % DEFAULT_COLORS.length]}
89+
strokeWidth={strokeWidth}
90+
strokeDasharray={strokeDasharray}
91+
strokeDashoffset={strokeDashoffset}
92+
transform={`rotate(-90 ${size / 2} ${size / 2})`}
93+
strokeLinecap="butt"
94+
/>
95+
);
96+
})
97+
: null}
98+
</svg>
99+
100+
{showLegend ? (
101+
<div className="min-w-44 space-y-2 text-sm">
102+
{segments.map((segment, index) => {
103+
const colorClass = segment.colorClass || DEFAULT_COLORS[index % DEFAULT_COLORS.length];
104+
const pct = hasValues ? Math.round((segment.value / total) * 100) : 0;
105+
106+
return (
107+
<div key={`${segment.label}-legend-${index}`} className="flex items-center justify-between gap-3">
108+
<span className="inline-flex min-w-0 items-center gap-2">
109+
<span className={`h-2.5 w-2.5 rounded-full bg-current ${colorClass}`} />
110+
<span className="truncate text-slate-700 dark:text-slate-200">{segment.label}</span>
111+
</span>
112+
<span className="text-slate-500 dark:text-slate-400">{pct}%</span>
113+
</div>
114+
);
115+
})}
116+
</div>
117+
) : null}
118+
</div>
119+
</div>
120+
);
121+
}

0 commit comments

Comments
 (0)