@nukleas/cyberdesign 0.1.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 +38 -0
- package/build.ts +286 -0
- package/dist/cyberdesign-vars-only.css +116 -0
- package/dist/cyberdesign.css +8377 -0
- package/dist/fonts/JetBrainsMono-Variable.woff2 +0 -0
- package/dist/fonts/OFL.txt +93 -0
- package/dist/react/Accordion.d.ts +34 -0
- package/dist/react/Accordion.js +13 -0
- package/dist/react/Alert.d.ts +19 -0
- package/dist/react/Alert.js +6 -0
- package/dist/react/AugButton.d.ts +12 -0
- package/dist/react/AugButton.js +6 -0
- package/dist/react/AugPanel.d.ts +10 -0
- package/dist/react/AugPanel.js +6 -0
- package/dist/react/Avatar.d.ts +28 -0
- package/dist/react/Avatar.js +11 -0
- package/dist/react/Badge.d.ts +10 -0
- package/dist/react/Badge.js +6 -0
- package/dist/react/Breadcrumb.d.ts +19 -0
- package/dist/react/Breadcrumb.js +10 -0
- package/dist/react/Button.d.ts +12 -0
- package/dist/react/Button.js +6 -0
- package/dist/react/Checkbox.d.ts +9 -0
- package/dist/react/Checkbox.js +9 -0
- package/dist/react/Dropdown.d.ts +35 -0
- package/dist/react/Dropdown.js +20 -0
- package/dist/react/Field.d.ts +13 -0
- package/dist/react/Field.js +6 -0
- package/dist/react/Gauge.d.ts +19 -0
- package/dist/react/Gauge.js +6 -0
- package/dist/react/GlowCard.d.ts +12 -0
- package/dist/react/GlowCard.js +4 -0
- package/dist/react/HudBar.d.ts +34 -0
- package/dist/react/HudBar.js +10 -0
- package/dist/react/HudFrame.d.ts +19 -0
- package/dist/react/HudFrame.js +6 -0
- package/dist/react/Input.d.ts +11 -0
- package/dist/react/Input.js +6 -0
- package/dist/react/Interference.d.ts +18 -0
- package/dist/react/Interference.js +6 -0
- package/dist/react/Kbd.d.ts +18 -0
- package/dist/react/Kbd.js +11 -0
- package/dist/react/Label.d.ts +8 -0
- package/dist/react/Label.js +6 -0
- package/dist/react/Modal.d.ts +19 -0
- package/dist/react/Modal.js +9 -0
- package/dist/react/Pagination.d.ts +23 -0
- package/dist/react/Pagination.js +28 -0
- package/dist/react/PanelHeader.d.ts +9 -0
- package/dist/react/PanelHeader.js +6 -0
- package/dist/react/Progress.d.ts +13 -0
- package/dist/react/Progress.js +6 -0
- package/dist/react/Radio.d.ts +9 -0
- package/dist/react/Radio.js +9 -0
- package/dist/react/Scanner.d.ts +11 -0
- package/dist/react/Scanner.js +6 -0
- package/dist/react/Select.d.ts +13 -0
- package/dist/react/Select.js +6 -0
- package/dist/react/Separator.d.ts +11 -0
- package/dist/react/Separator.js +7 -0
- package/dist/react/Skeleton.d.ts +11 -0
- package/dist/react/Skeleton.js +13 -0
- package/dist/react/Slider.d.ts +9 -0
- package/dist/react/Slider.js +9 -0
- package/dist/react/Spinner.d.ts +11 -0
- package/dist/react/Spinner.js +6 -0
- package/dist/react/Stat.d.ts +20 -0
- package/dist/react/Stat.js +11 -0
- package/dist/react/Switch.d.ts +9 -0
- package/dist/react/Switch.js +6 -0
- package/dist/react/Table.d.ts +55 -0
- package/dist/react/Table.js +36 -0
- package/dist/react/Tabs.d.ts +28 -0
- package/dist/react/Tabs.js +16 -0
- package/dist/react/Terminal.d.ts +33 -0
- package/dist/react/Terminal.js +6 -0
- package/dist/react/Textarea.d.ts +7 -0
- package/dist/react/Textarea.js +6 -0
- package/dist/react/Ticker.d.ts +13 -0
- package/dist/react/Ticker.js +7 -0
- package/dist/react/Toast.d.ts +15 -0
- package/dist/react/Toast.js +6 -0
- package/dist/react/Tooltip.d.ts +11 -0
- package/dist/react/Tooltip.js +6 -0
- package/dist/react/index.d.ts +39 -0
- package/dist/react/index.js +46 -0
- package/dist/tailwind-theme.css +171 -0
- package/dist/tokens/base.d.ts +33 -0
- package/dist/tokens/base.js +41 -0
- package/dist/tokens/index.d.ts +7 -0
- package/dist/tokens/index.js +7 -0
- package/dist/tokens/semantic.d.ts +1 -0
- package/dist/tokens/semantic.js +54 -0
- package/dist/tokens/themes/cyan.d.ts +1 -0
- package/dist/tokens/themes/cyan.js +15 -0
- package/dist/tokens/themes/green.d.ts +1 -0
- package/dist/tokens/themes/green.js +15 -0
- package/dist/tokens/themes/orange.d.ts +1 -0
- package/dist/tokens/themes/orange.js +15 -0
- package/dist/tokens/themes/red.d.ts +1 -0
- package/dist/tokens/themes/red.js +15 -0
- package/dist/tokens/themes/steel.d.ts +1 -0
- package/dist/tokens/themes/steel.js +15 -0
- package/package.json +73 -0
- package/src/css/a11y.css +67 -0
- package/src/css/animations.css +111 -0
- package/src/css/base.css +34 -0
- package/src/css/components/accordion.css +148 -0
- package/src/css/components/alert.css +116 -0
- package/src/css/components/app-patterns.css +894 -0
- package/src/css/components/augmented.css +474 -0
- package/src/css/components/avatar.css +105 -0
- package/src/css/components/badge.css +46 -0
- package/src/css/components/breadcrumb.css +75 -0
- package/src/css/components/button.css +130 -0
- package/src/css/components/calendar.css +135 -0
- package/src/css/components/card.css +68 -0
- package/src/css/components/carousel.css +69 -0
- package/src/css/components/charts.css +464 -0
- package/src/css/components/checkbox.css +136 -0
- package/src/css/components/command.css +162 -0
- package/src/css/components/dropdown.css +187 -0
- package/src/css/components/form.css +280 -0
- package/src/css/components/gauge.css +156 -0
- package/src/css/components/glow-card.css +32 -0
- package/src/css/components/hud.css +165 -0
- package/src/css/components/image.css +164 -0
- package/src/css/components/input-otp.css +53 -0
- package/src/css/components/input.css +182 -0
- package/src/css/components/interference.css +217 -0
- package/src/css/components/kbd.css +54 -0
- package/src/css/components/layout-extras.css +256 -0
- package/src/css/components/modal.css +146 -0
- package/src/css/components/navigation-extras.css +280 -0
- package/src/css/components/pagination.css +122 -0
- package/src/css/components/panel-header.css +55 -0
- package/src/css/components/popover.css +112 -0
- package/src/css/components/scanner.css +125 -0
- package/src/css/components/select.css +99 -0
- package/src/css/components/sheet.css +149 -0
- package/src/css/components/skeleton.css +77 -0
- package/src/css/components/slider.css +116 -0
- package/src/css/components/spinner.css +101 -0
- package/src/css/components/spreadsheet.css +314 -0
- package/src/css/components/svg.css +104 -0
- package/src/css/components/switch.css +114 -0
- package/src/css/components/table.css +115 -0
- package/src/css/components/tabs.css +159 -0
- package/src/css/components/terminal.css +259 -0
- package/src/css/components/ticker.css +100 -0
- package/src/css/components/toast.css +137 -0
- package/src/css/components/toaster.css +48 -0
- package/src/css/components/toggle.css +99 -0
- package/src/css/components/typography.css +122 -0
- package/src/css/components/vectorbg.css +57 -0
- package/src/css/utilities.css +165 -0
- package/src/fonts/JetBrainsMono-Variable.woff2 +0 -0
- package/src/fonts/OFL.txt +93 -0
- package/src/js/charts.js +533 -0
- package/src/react/Accordion.tsx +85 -0
- package/src/react/Alert.tsx +58 -0
- package/src/react/AugButton.tsx +40 -0
- package/src/react/AugPanel.tsx +36 -0
- package/src/react/Avatar.tsx +69 -0
- package/src/react/Badge.tsx +28 -0
- package/src/react/Breadcrumb.tsx +63 -0
- package/src/react/Button.tsx +39 -0
- package/src/react/Checkbox.tsx +31 -0
- package/src/react/Dropdown.tsx +104 -0
- package/src/react/Field.tsx +36 -0
- package/src/react/Gauge.tsx +57 -0
- package/src/react/GlowCard.tsx +25 -0
- package/src/react/HudBar.tsx +86 -0
- package/src/react/HudFrame.tsx +52 -0
- package/src/react/Input.tsx +38 -0
- package/src/react/Interference.tsx +53 -0
- package/src/react/Kbd.tsx +62 -0
- package/src/react/Label.tsx +31 -0
- package/src/react/Modal.tsx +63 -0
- package/src/react/Pagination.tsx +113 -0
- package/src/react/PanelHeader.tsx +28 -0
- package/src/react/Progress.tsx +47 -0
- package/src/react/Radio.tsx +31 -0
- package/src/react/Scanner.tsx +37 -0
- package/src/react/Select.tsx +43 -0
- package/src/react/Separator.tsx +40 -0
- package/src/react/Skeleton.tsx +50 -0
- package/src/react/Slider.tsx +28 -0
- package/src/react/Spinner.tsx +39 -0
- package/src/react/Stat.tsx +52 -0
- package/src/react/Switch.tsx +25 -0
- package/src/react/Table.tsx +144 -0
- package/src/react/Tabs.tsx +97 -0
- package/src/react/Terminal.tsx +95 -0
- package/src/react/Textarea.tsx +20 -0
- package/src/react/Ticker.tsx +46 -0
- package/src/react/Toast.tsx +43 -0
- package/src/react/Tooltip.tsx +32 -0
- package/src/react/index.ts +52 -0
- package/src/tokens/base.ts +48 -0
- package/src/tokens/index.ts +7 -0
- package/src/tokens/semantic.ts +63 -0
- package/src/tokens/themes/cyan.ts +16 -0
- package/src/tokens/themes/green.ts +16 -0
- package/src/tokens/themes/orange.ts +16 -0
- package/src/tokens/themes/red.ts +16 -0
- package/src/tokens/themes/steel.ts +16 -0
package/src/js/charts.js
ADDED
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cyberdesign/charts.js
|
|
3
|
+
* Lightweight canvas + SVG chart helpers. No dependencies.
|
|
4
|
+
* All colors read from --cd-* CSS variables so they respond to .theme-orange / .theme-cyan.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ── Utility ──────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
function css(el, prop) {
|
|
10
|
+
return getComputedStyle(el).getPropertyValue(prop).trim();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function colors(canvas) {
|
|
14
|
+
const el = canvas.closest('[class*="theme-"]') || document.documentElement;
|
|
15
|
+
return {
|
|
16
|
+
bg: css(el, '--cd-bg-panel') || '#0b0f18',
|
|
17
|
+
bgDark: css(el, '--cd-bg') || '#05060a',
|
|
18
|
+
border: css(el, '--cd-border') || '#26324c',
|
|
19
|
+
borderDim:css(el, '--cd-border-dim') || '#1e1e24',
|
|
20
|
+
text: css(el, '--cd-text') || '#e8e4e0',
|
|
21
|
+
muted: css(el, '--cd-text-muted') || '#8a8078',
|
|
22
|
+
accent: css(el, '--cd-accent') || '#ff8800',
|
|
23
|
+
secondary:css(el, '--cd-accent-secondary') || '#ff2bd6',
|
|
24
|
+
green: css(el, '--cd-neon-green') || '#52ff9f',
|
|
25
|
+
red: css(el, '--cd-neon-red') || '#ff456c',
|
|
26
|
+
yellow: css(el, '--cd-neon-yellow') || '#f7ff5a',
|
|
27
|
+
blue: css(el, '--cd-neon-blue') || '#6aa8ff',
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function dpr() { return window.devicePixelRatio || 1; }
|
|
32
|
+
|
|
33
|
+
function setupCanvas(canvas) {
|
|
34
|
+
const parent = canvas.parentElement;
|
|
35
|
+
const rect = parent.getBoundingClientRect();
|
|
36
|
+
const ratio = dpr();
|
|
37
|
+
const w = rect.width || 300;
|
|
38
|
+
const h = rect.height || 200;
|
|
39
|
+
canvas.width = w * ratio;
|
|
40
|
+
canvas.height = h * ratio;
|
|
41
|
+
canvas.style.width = w + 'px';
|
|
42
|
+
canvas.style.height = h + 'px';
|
|
43
|
+
const ctx = canvas.getContext('2d');
|
|
44
|
+
ctx.scale(ratio, ratio);
|
|
45
|
+
return { ctx, w, h };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Line / Area Chart ────────────────────────────────────
|
|
49
|
+
/**
|
|
50
|
+
* CdLineChart(canvas, opts) → { update(opts), destroy() }
|
|
51
|
+
*
|
|
52
|
+
* opts: {
|
|
53
|
+
* series: [{ data: number[], color?: string, fill?: boolean, label?: string }]
|
|
54
|
+
* min?: number, // y-axis min (auto if null)
|
|
55
|
+
* max?: number, // y-axis max (auto if null)
|
|
56
|
+
* gridLines?: number,
|
|
57
|
+
* xLabels?: string[],
|
|
58
|
+
* yFormat?: (v: number) => string,
|
|
59
|
+
* pad?: { top, right, bottom, left }
|
|
60
|
+
* }
|
|
61
|
+
*/
|
|
62
|
+
export function CdLineChart(canvas, opts = {}) {
|
|
63
|
+
let o = {
|
|
64
|
+
series: [],
|
|
65
|
+
min: null,
|
|
66
|
+
max: null,
|
|
67
|
+
gridLines: 4,
|
|
68
|
+
xLabels: [],
|
|
69
|
+
yFormat: v => Math.round(v),
|
|
70
|
+
pad: { top: 16, right: 12, bottom: 28, left: 36 },
|
|
71
|
+
...opts,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
let { ctx, w, h } = setupCanvas(canvas);
|
|
75
|
+
const font = `700 10px ${css(document.documentElement, '--cd-font-mono') || 'monospace'}`;
|
|
76
|
+
|
|
77
|
+
function draw() {
|
|
78
|
+
const c = colors(canvas);
|
|
79
|
+
const { top, right, bottom, left } = o.pad;
|
|
80
|
+
const pw = w - left - right;
|
|
81
|
+
const ph = h - top - bottom;
|
|
82
|
+
|
|
83
|
+
ctx.clearRect(0, 0, w, h);
|
|
84
|
+
ctx.fillStyle = c.bg;
|
|
85
|
+
ctx.fillRect(0, 0, w, h);
|
|
86
|
+
|
|
87
|
+
if (!o.series.length || !o.series[0].data.length) return;
|
|
88
|
+
|
|
89
|
+
const allVals = o.series.flatMap(s => s.data).filter(Number.isFinite);
|
|
90
|
+
let lo = o.min ?? Math.min(...allVals);
|
|
91
|
+
let hi = o.max ?? Math.max(...allVals);
|
|
92
|
+
if (lo === hi) { lo -= 1; hi += 1; }
|
|
93
|
+
const range = hi - lo;
|
|
94
|
+
const maxLen = Math.max(...o.series.map(s => s.data.length));
|
|
95
|
+
|
|
96
|
+
function xp(i) { return left + (i / Math.max(maxLen - 1, 1)) * pw; }
|
|
97
|
+
function yp(v) { return top + ph - ((v - lo) / range) * ph; }
|
|
98
|
+
|
|
99
|
+
// Grid
|
|
100
|
+
ctx.setLineDash([2, 5]);
|
|
101
|
+
ctx.lineWidth = 1;
|
|
102
|
+
ctx.font = font;
|
|
103
|
+
ctx.textAlign = 'right';
|
|
104
|
+
for (let g = 0; g <= o.gridLines; g++) {
|
|
105
|
+
const y = top + (g / o.gridLines) * ph;
|
|
106
|
+
const v = hi - (g / o.gridLines) * range;
|
|
107
|
+
ctx.strokeStyle = c.borderDim;
|
|
108
|
+
ctx.beginPath();
|
|
109
|
+
ctx.moveTo(left, y);
|
|
110
|
+
ctx.lineTo(left + pw, y);
|
|
111
|
+
ctx.stroke();
|
|
112
|
+
ctx.fillStyle = c.muted;
|
|
113
|
+
ctx.fillText(o.yFormat(v), left - 5, y + 3);
|
|
114
|
+
}
|
|
115
|
+
ctx.setLineDash([]);
|
|
116
|
+
|
|
117
|
+
// X labels
|
|
118
|
+
if (o.xLabels.length) {
|
|
119
|
+
ctx.textAlign = 'center';
|
|
120
|
+
ctx.fillStyle = c.muted;
|
|
121
|
+
const every = Math.ceil(o.xLabels.length / 8);
|
|
122
|
+
for (let i = 0; i < o.xLabels.length; i += every) {
|
|
123
|
+
ctx.fillText(o.xLabels[i], xp(i), top + ph + 16);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Plot border
|
|
128
|
+
ctx.strokeStyle = c.border;
|
|
129
|
+
ctx.lineWidth = 1;
|
|
130
|
+
ctx.strokeRect(left, top, pw, ph);
|
|
131
|
+
|
|
132
|
+
// Clip to plot area
|
|
133
|
+
ctx.save();
|
|
134
|
+
ctx.beginPath();
|
|
135
|
+
ctx.rect(left, top, pw, ph);
|
|
136
|
+
ctx.clip();
|
|
137
|
+
|
|
138
|
+
for (const s of o.series) {
|
|
139
|
+
if (s.data.length < 2) continue;
|
|
140
|
+
const col = s.color || c.accent;
|
|
141
|
+
|
|
142
|
+
// Build smooth bezier path
|
|
143
|
+
function buildPath() {
|
|
144
|
+
ctx.beginPath();
|
|
145
|
+
ctx.moveTo(xp(0), yp(s.data[0]));
|
|
146
|
+
for (let i = 1; i < s.data.length; i++) {
|
|
147
|
+
const cpx = (xp(i - 1) + xp(i)) / 2;
|
|
148
|
+
ctx.bezierCurveTo(cpx, yp(s.data[i - 1]), cpx, yp(s.data[i]), xp(i), yp(s.data[i]));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Fill
|
|
153
|
+
if (s.fill) {
|
|
154
|
+
buildPath();
|
|
155
|
+
ctx.lineTo(xp(s.data.length - 1), top + ph);
|
|
156
|
+
ctx.lineTo(xp(0), top + ph);
|
|
157
|
+
ctx.closePath();
|
|
158
|
+
const grad = ctx.createLinearGradient(0, top, 0, top + ph);
|
|
159
|
+
grad.addColorStop(0, col + '40');
|
|
160
|
+
grad.addColorStop(1, col + '00');
|
|
161
|
+
ctx.fillStyle = grad;
|
|
162
|
+
ctx.fill();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Glow pass
|
|
166
|
+
buildPath();
|
|
167
|
+
ctx.strokeStyle = col + '28';
|
|
168
|
+
ctx.lineWidth = 5;
|
|
169
|
+
ctx.stroke();
|
|
170
|
+
|
|
171
|
+
// Main line
|
|
172
|
+
buildPath();
|
|
173
|
+
ctx.strokeStyle = col;
|
|
174
|
+
ctx.lineWidth = 1.5;
|
|
175
|
+
ctx.stroke();
|
|
176
|
+
|
|
177
|
+
// End-point dot
|
|
178
|
+
const ex = xp(s.data.length - 1);
|
|
179
|
+
const ey = yp(s.data[s.data.length - 1]);
|
|
180
|
+
ctx.beginPath();
|
|
181
|
+
ctx.arc(ex, ey, 3, 0, Math.PI * 2);
|
|
182
|
+
ctx.fillStyle = col;
|
|
183
|
+
ctx.fill();
|
|
184
|
+
// dot glow
|
|
185
|
+
ctx.beginPath();
|
|
186
|
+
ctx.arc(ex, ey, 6, 0, Math.PI * 2);
|
|
187
|
+
ctx.fillStyle = col + '30';
|
|
188
|
+
ctx.fill();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
ctx.restore();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const ro = new ResizeObserver(() => {
|
|
195
|
+
({ ctx, w, h } = setupCanvas(canvas));
|
|
196
|
+
draw();
|
|
197
|
+
});
|
|
198
|
+
ro.observe(canvas.parentElement);
|
|
199
|
+
draw();
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
update(newOpts) { o = { ...o, ...newOpts }; draw(); },
|
|
203
|
+
redraw() { draw(); },
|
|
204
|
+
destroy() { ro.disconnect(); },
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ── Waveform / Oscilloscope ──────────────────────────────
|
|
209
|
+
/**
|
|
210
|
+
* CdWaveform(canvas) → { push(Float32Array | Uint8Array), demo(), destroy() }
|
|
211
|
+
*
|
|
212
|
+
* push() feeds a time-domain sample buffer (Web Audio AnalyserNode).
|
|
213
|
+
* demo() starts an animated sine wave for testing without audio.
|
|
214
|
+
*/
|
|
215
|
+
export function CdWaveform(canvas) {
|
|
216
|
+
let { ctx, w, h } = setupCanvas(canvas);
|
|
217
|
+
let buf = null;
|
|
218
|
+
let rafId = null;
|
|
219
|
+
let demoPhase = 0;
|
|
220
|
+
let demoActive = false;
|
|
221
|
+
|
|
222
|
+
function drawBuf(data, isFloat) {
|
|
223
|
+
const c = colors(canvas);
|
|
224
|
+
const col = c.yellow;
|
|
225
|
+
|
|
226
|
+
ctx.clearRect(0, 0, w, h);
|
|
227
|
+
ctx.fillStyle = c.bgDark;
|
|
228
|
+
ctx.fillRect(0, 0, w, h);
|
|
229
|
+
|
|
230
|
+
// Centre line
|
|
231
|
+
ctx.strokeStyle = c.borderDim;
|
|
232
|
+
ctx.lineWidth = 1;
|
|
233
|
+
ctx.setLineDash([2, 6]);
|
|
234
|
+
ctx.beginPath();
|
|
235
|
+
ctx.moveTo(0, h / 2);
|
|
236
|
+
ctx.lineTo(w, h / 2);
|
|
237
|
+
ctx.stroke();
|
|
238
|
+
ctx.setLineDash([]);
|
|
239
|
+
|
|
240
|
+
// Waveform
|
|
241
|
+
const step = Math.max(1, Math.floor(data.length / w));
|
|
242
|
+
ctx.beginPath();
|
|
243
|
+
for (let i = 0; i < w; i++) {
|
|
244
|
+
const idx = i * step;
|
|
245
|
+
const raw = isFloat ? data[idx] : (data[idx] / 128) - 1;
|
|
246
|
+
const y = (h / 2) + raw * (h / 2) * 0.85;
|
|
247
|
+
i === 0 ? ctx.moveTo(i, y) : ctx.lineTo(i, y);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Glow stroke
|
|
251
|
+
ctx.strokeStyle = col + '35';
|
|
252
|
+
ctx.lineWidth = 4;
|
|
253
|
+
ctx.stroke();
|
|
254
|
+
// Main stroke
|
|
255
|
+
ctx.strokeStyle = col;
|
|
256
|
+
ctx.lineWidth = 1.5;
|
|
257
|
+
ctx.stroke();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function demoTick(ts) {
|
|
261
|
+
if (!demoActive) return;
|
|
262
|
+
demoPhase = (ts / 1200) * Math.PI * 2;
|
|
263
|
+
const samples = new Float32Array(512);
|
|
264
|
+
for (let i = 0; i < 512; i++) {
|
|
265
|
+
const t = i / 512;
|
|
266
|
+
samples[i] =
|
|
267
|
+
Math.sin(demoPhase + t * Math.PI * 4) * 0.55 +
|
|
268
|
+
Math.sin(demoPhase * 2.3 + t * Math.PI * 12) * 0.2 +
|
|
269
|
+
Math.sin(demoPhase * 0.7 + t * Math.PI * 1.5) * 0.25;
|
|
270
|
+
}
|
|
271
|
+
drawBuf(samples, true);
|
|
272
|
+
rafId = requestAnimationFrame(demoTick);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const ro = new ResizeObserver(() => {
|
|
276
|
+
({ ctx, w, h } = setupCanvas(canvas));
|
|
277
|
+
if (buf) drawBuf(buf.data, buf.isFloat);
|
|
278
|
+
});
|
|
279
|
+
ro.observe(canvas.parentElement);
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
push(data) {
|
|
283
|
+
demoActive = false;
|
|
284
|
+
cancelAnimationFrame(rafId);
|
|
285
|
+
const isFloat = data instanceof Float32Array;
|
|
286
|
+
buf = { data, isFloat };
|
|
287
|
+
drawBuf(data, isFloat);
|
|
288
|
+
},
|
|
289
|
+
demo() {
|
|
290
|
+
demoActive = true;
|
|
291
|
+
rafId = requestAnimationFrame(demoTick);
|
|
292
|
+
},
|
|
293
|
+
destroy() {
|
|
294
|
+
demoActive = false;
|
|
295
|
+
cancelAnimationFrame(rafId);
|
|
296
|
+
ro.disconnect();
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ── Radar / Spider Chart (SVG) ───────────────────────────
|
|
302
|
+
/**
|
|
303
|
+
* CdRadar(svgEl, opts) → { update(opts), destroy() }
|
|
304
|
+
*
|
|
305
|
+
* opts: {
|
|
306
|
+
* axes: string[],
|
|
307
|
+
* series: [{ values: number[], color?: string, label?: string }],
|
|
308
|
+
* max?: number, // defaults to 1.0
|
|
309
|
+
* rings?: number, // concentric grid rings (default 4)
|
|
310
|
+
* }
|
|
311
|
+
*/
|
|
312
|
+
export function CdRadar(svg, opts = {}) {
|
|
313
|
+
let o = { axes: [], series: [], max: 1, rings: 4, ...opts };
|
|
314
|
+
|
|
315
|
+
function draw() {
|
|
316
|
+
const c = colors(svg);
|
|
317
|
+
const W = svg.clientWidth || 240;
|
|
318
|
+
const H = svg.clientHeight || 240;
|
|
319
|
+
const cx = W / 2;
|
|
320
|
+
const cy = H / 2;
|
|
321
|
+
const r = Math.min(cx, cy) * 0.72;
|
|
322
|
+
const n = o.axes.length;
|
|
323
|
+
if (n < 3) return;
|
|
324
|
+
|
|
325
|
+
function angle(i) { return (i / n) * Math.PI * 2 - Math.PI / 2; }
|
|
326
|
+
function pt(i, frac) {
|
|
327
|
+
const a = angle(i);
|
|
328
|
+
return [cx + Math.cos(a) * r * frac, cy + Math.sin(a) * r * frac];
|
|
329
|
+
}
|
|
330
|
+
function polygon(fracs) {
|
|
331
|
+
return fracs.map((f, i) => pt(i, f).join(',')).join(' ');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
svg.setAttribute('viewBox', `0 0 ${W} ${H}`);
|
|
335
|
+
svg.innerHTML = '';
|
|
336
|
+
|
|
337
|
+
const ns = 'http://www.w3.org/2000/svg';
|
|
338
|
+
function el(tag, attrs, parent = svg) {
|
|
339
|
+
const e = document.createElementNS(ns, tag);
|
|
340
|
+
for (const [k, v] of Object.entries(attrs)) e.setAttribute(k, v);
|
|
341
|
+
parent.appendChild(e);
|
|
342
|
+
return e;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Grid rings
|
|
346
|
+
for (let ring = 1; ring <= o.rings; ring++) {
|
|
347
|
+
const frac = ring / o.rings;
|
|
348
|
+
el('polygon', {
|
|
349
|
+
points: polygon(Array(n).fill(frac)),
|
|
350
|
+
fill: 'none',
|
|
351
|
+
stroke: c.borderDim,
|
|
352
|
+
'stroke-width': '1',
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Spokes
|
|
357
|
+
for (let i = 0; i < n; i++) {
|
|
358
|
+
const [x, y] = pt(i, 1);
|
|
359
|
+
el('line', { x1: cx, y1: cy, x2: x, y2: y, stroke: c.borderDim, 'stroke-width': '1' });
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Axis labels
|
|
363
|
+
for (let i = 0; i < n; i++) {
|
|
364
|
+
const [x, y] = pt(i, 1.14);
|
|
365
|
+
const t = el('text', {
|
|
366
|
+
x, y,
|
|
367
|
+
'text-anchor': 'middle',
|
|
368
|
+
'dominant-baseline': 'middle',
|
|
369
|
+
fill: c.muted,
|
|
370
|
+
'font-size': '10',
|
|
371
|
+
'font-family': 'monospace',
|
|
372
|
+
'font-weight': '700',
|
|
373
|
+
'letter-spacing': '0.1em',
|
|
374
|
+
'text-transform': 'uppercase',
|
|
375
|
+
});
|
|
376
|
+
t.textContent = o.axes[i].toUpperCase();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Series
|
|
380
|
+
for (const s of o.series) {
|
|
381
|
+
const col = s.color || c.accent;
|
|
382
|
+
const fracs = s.values.map(v => Math.min(v / o.max, 1));
|
|
383
|
+
|
|
384
|
+
// Fill
|
|
385
|
+
el('polygon', {
|
|
386
|
+
points: polygon(fracs),
|
|
387
|
+
fill: col + '22',
|
|
388
|
+
stroke: 'none',
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// Stroke
|
|
392
|
+
el('polygon', {
|
|
393
|
+
points: polygon(fracs),
|
|
394
|
+
fill: 'none',
|
|
395
|
+
stroke: col,
|
|
396
|
+
'stroke-width': '1.5',
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// Glow stroke
|
|
400
|
+
el('polygon', {
|
|
401
|
+
points: polygon(fracs),
|
|
402
|
+
fill: 'none',
|
|
403
|
+
stroke: col + '30',
|
|
404
|
+
'stroke-width': '5',
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// Data point dots
|
|
408
|
+
for (let i = 0; i < n; i++) {
|
|
409
|
+
const [x, y] = pt(i, fracs[i]);
|
|
410
|
+
el('circle', { cx: x, cy: y, r: '3', fill: col });
|
|
411
|
+
el('circle', { cx: x, cy: y, r: '6', fill: col + '30' });
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const ro = new ResizeObserver(draw);
|
|
417
|
+
ro.observe(svg);
|
|
418
|
+
draw();
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
update(newOpts) { o = { ...o, ...newOpts }; draw(); },
|
|
422
|
+
destroy() { ro.disconnect(); },
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ── Bar Chart (canvas) ───────────────────────────────────
|
|
427
|
+
/**
|
|
428
|
+
* CdBarChart(canvas, opts) → { update(opts), destroy() }
|
|
429
|
+
*
|
|
430
|
+
* opts: {
|
|
431
|
+
* bars: [{ label: string, value: number, color?: string }],
|
|
432
|
+
* max?: number, // y-axis max (auto if null)
|
|
433
|
+
* pad?: { top, right, bottom, left }
|
|
434
|
+
* barGap?: number, // 0–1, fraction of bar width that is gap
|
|
435
|
+
* }
|
|
436
|
+
*/
|
|
437
|
+
export function CdBarChart(canvas, opts = {}) {
|
|
438
|
+
let o = {
|
|
439
|
+
bars: [],
|
|
440
|
+
max: null,
|
|
441
|
+
pad: { top: 16, right: 12, bottom: 28, left: 36 },
|
|
442
|
+
barGap: 0.25,
|
|
443
|
+
yFormat: v => Math.round(v),
|
|
444
|
+
gridLines: 4,
|
|
445
|
+
...opts,
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
let { ctx, w, h } = setupCanvas(canvas);
|
|
449
|
+
const font = `700 10px ${css(document.documentElement, '--cd-font-mono') || 'monospace'}`;
|
|
450
|
+
|
|
451
|
+
function draw() {
|
|
452
|
+
const c = colors(canvas);
|
|
453
|
+
const { top, right, bottom, left } = o.pad;
|
|
454
|
+
const pw = w - left - right;
|
|
455
|
+
const ph = h - top - bottom;
|
|
456
|
+
|
|
457
|
+
ctx.clearRect(0, 0, w, h);
|
|
458
|
+
ctx.fillStyle = c.bg;
|
|
459
|
+
ctx.fillRect(0, 0, w, h);
|
|
460
|
+
|
|
461
|
+
if (!o.bars.length) return;
|
|
462
|
+
|
|
463
|
+
const hi = o.max ?? Math.max(...o.bars.map(b => b.value));
|
|
464
|
+
const lo = 0;
|
|
465
|
+
const range = hi - lo || 1;
|
|
466
|
+
|
|
467
|
+
function yp(v) { return top + ph - ((v - lo) / range) * ph; }
|
|
468
|
+
|
|
469
|
+
// Grid
|
|
470
|
+
ctx.font = font;
|
|
471
|
+
ctx.textAlign = 'right';
|
|
472
|
+
for (let g = 0; g <= o.gridLines; g++) {
|
|
473
|
+
const y = top + (g / o.gridLines) * ph;
|
|
474
|
+
const v = hi - (g / o.gridLines) * range;
|
|
475
|
+
ctx.strokeStyle = c.borderDim;
|
|
476
|
+
ctx.lineWidth = 1;
|
|
477
|
+
ctx.setLineDash([2, 5]);
|
|
478
|
+
ctx.beginPath();
|
|
479
|
+
ctx.moveTo(left, y);
|
|
480
|
+
ctx.lineTo(left + pw, y);
|
|
481
|
+
ctx.stroke();
|
|
482
|
+
ctx.setLineDash([]);
|
|
483
|
+
ctx.fillStyle = c.muted;
|
|
484
|
+
ctx.fillText(o.yFormat(v), left - 5, y + 3);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
ctx.strokeStyle = c.border;
|
|
488
|
+
ctx.lineWidth = 1;
|
|
489
|
+
ctx.strokeRect(left, top, pw, ph);
|
|
490
|
+
|
|
491
|
+
const n = o.bars.length;
|
|
492
|
+
const slotW = pw / n;
|
|
493
|
+
const gap = slotW * o.barGap;
|
|
494
|
+
const barW = slotW - gap;
|
|
495
|
+
|
|
496
|
+
for (let i = 0; i < n; i++) {
|
|
497
|
+
const bar = o.bars[i];
|
|
498
|
+
const col = bar.color || c.accent;
|
|
499
|
+
const bx = left + i * slotW + gap / 2;
|
|
500
|
+
const by = yp(bar.value);
|
|
501
|
+
const bh = top + ph - by;
|
|
502
|
+
|
|
503
|
+
// Bar fill
|
|
504
|
+
const grad = ctx.createLinearGradient(0, by, 0, by + bh);
|
|
505
|
+
grad.addColorStop(0, col + 'cc');
|
|
506
|
+
grad.addColorStop(1, col + '44');
|
|
507
|
+
ctx.fillStyle = grad;
|
|
508
|
+
ctx.fillRect(bx, by, barW, bh);
|
|
509
|
+
|
|
510
|
+
// Top glow line
|
|
511
|
+
ctx.fillStyle = col;
|
|
512
|
+
ctx.fillRect(bx, by, barW, 2);
|
|
513
|
+
|
|
514
|
+
// Label
|
|
515
|
+
ctx.textAlign = 'center';
|
|
516
|
+
ctx.fillStyle = c.muted;
|
|
517
|
+
ctx.fillText(bar.label.toUpperCase().slice(0, 8), bx + barW / 2, top + ph + 16);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const ro = new ResizeObserver(() => {
|
|
522
|
+
({ ctx, w, h } = setupCanvas(canvas));
|
|
523
|
+
draw();
|
|
524
|
+
});
|
|
525
|
+
ro.observe(canvas.parentElement);
|
|
526
|
+
draw();
|
|
527
|
+
|
|
528
|
+
return {
|
|
529
|
+
update(newOpts) { o = { ...o, ...newOpts }; draw(); },
|
|
530
|
+
redraw() { draw(); },
|
|
531
|
+
destroy() { ro.disconnect(); },
|
|
532
|
+
};
|
|
533
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { HTMLAttributes, DetailsHTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
const cx = (...parts: Array<string | false | null | undefined>) =>
|
|
4
|
+
parts.filter(Boolean).join(' ');
|
|
5
|
+
|
|
6
|
+
export interface AccordionItemData {
|
|
7
|
+
/** Trigger heading text. */
|
|
8
|
+
title: ReactNode;
|
|
9
|
+
/** Body content revealed when open. */
|
|
10
|
+
content: ReactNode;
|
|
11
|
+
/** Optional icon glyph before the title. */
|
|
12
|
+
icon?: ReactNode;
|
|
13
|
+
/** Render this item open initially. */
|
|
14
|
+
defaultOpen?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AccordionItemProps
|
|
18
|
+
extends Omit<DetailsHTMLAttributes<HTMLDetailsElement>, 'title'> {
|
|
19
|
+
/** Trigger heading text. */
|
|
20
|
+
title: ReactNode;
|
|
21
|
+
/** Optional icon glyph before the title. */
|
|
22
|
+
icon?: ReactNode;
|
|
23
|
+
/** Body content. */
|
|
24
|
+
children?: ReactNode;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Single `<details>` accordion row — for manual composition under `<Accordion>`. */
|
|
28
|
+
function AccordionItem({ title, icon, children, className = '', ...rest }: AccordionItemProps) {
|
|
29
|
+
return (
|
|
30
|
+
<details className={cx('cd-accordion__item', className)} {...rest}>
|
|
31
|
+
<summary className="cd-accordion__trigger">
|
|
32
|
+
{icon != null && <span className="cd-accordion__icon">{icon}</span>}
|
|
33
|
+
{title}
|
|
34
|
+
</summary>
|
|
35
|
+
<div className="cd-accordion__body">
|
|
36
|
+
<div className="cd-accordion__content">
|
|
37
|
+
<div className="cd-accordion__content-inner">{children}</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</details>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface AccordionProps extends HTMLAttributes<HTMLDivElement> {
|
|
45
|
+
/** Data-driven rows. Omit to compose `<Accordion.Item>` children manually. */
|
|
46
|
+
items?: AccordionItemData[];
|
|
47
|
+
/** `sep` adds gaps between items; `flush` drops side borders. */
|
|
48
|
+
variant?: 'default' | 'sep' | 'flush';
|
|
49
|
+
children?: ReactNode;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Disclosure accordion — native `<details>` rows, data-driven or composed via `Accordion.Item`. */
|
|
53
|
+
function AccordionRoot({
|
|
54
|
+
items,
|
|
55
|
+
variant = 'default',
|
|
56
|
+
className = '',
|
|
57
|
+
children,
|
|
58
|
+
...rest
|
|
59
|
+
}: AccordionProps) {
|
|
60
|
+
return (
|
|
61
|
+
<div
|
|
62
|
+
className={cx(
|
|
63
|
+
'cd-accordion',
|
|
64
|
+
variant !== 'default' && `cd-accordion--${variant}`,
|
|
65
|
+
className,
|
|
66
|
+
)}
|
|
67
|
+
{...rest}
|
|
68
|
+
>
|
|
69
|
+
{items
|
|
70
|
+
? items.map((item, i) => (
|
|
71
|
+
<AccordionItem
|
|
72
|
+
key={i}
|
|
73
|
+
title={item.title}
|
|
74
|
+
icon={item.icon}
|
|
75
|
+
open={item.defaultOpen}
|
|
76
|
+
>
|
|
77
|
+
{item.content}
|
|
78
|
+
</AccordionItem>
|
|
79
|
+
))
|
|
80
|
+
: children}
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const Accordion = Object.assign(AccordionRoot, { Item: AccordionItem });
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
const cx = (...parts: Array<string | false | null | undefined>) =>
|
|
4
|
+
parts.filter(Boolean).join(' ');
|
|
5
|
+
|
|
6
|
+
export interface AlertProps extends Omit<HTMLAttributes<HTMLDivElement>, 'title'> {
|
|
7
|
+
/** Semantic color. `accent` follows the active theme accent. */
|
|
8
|
+
variant?: 'info' | 'success' | 'warning' | 'error' | 'accent';
|
|
9
|
+
/** Compact padding/type. */
|
|
10
|
+
size?: 'sm';
|
|
11
|
+
/** Drop the left accent bar for a flatter inline banner. */
|
|
12
|
+
inline?: boolean;
|
|
13
|
+
/** Bold uppercase heading line. */
|
|
14
|
+
title?: ReactNode;
|
|
15
|
+
/** Leading glyph (e.g. ℹ ✓ ⚠ ✕ ⬡). */
|
|
16
|
+
icon?: ReactNode;
|
|
17
|
+
/** When provided, renders a dismiss ✕ that calls this on click. */
|
|
18
|
+
onDismiss?: () => void;
|
|
19
|
+
/** Body text. */
|
|
20
|
+
children?: ReactNode;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Banner / alert with semantic color, optional icon, title and dismiss. */
|
|
24
|
+
export function Alert({
|
|
25
|
+
variant = 'info',
|
|
26
|
+
size,
|
|
27
|
+
inline = false,
|
|
28
|
+
title,
|
|
29
|
+
icon,
|
|
30
|
+
onDismiss,
|
|
31
|
+
className = '',
|
|
32
|
+
children,
|
|
33
|
+
...rest
|
|
34
|
+
}: AlertProps) {
|
|
35
|
+
return (
|
|
36
|
+
<div
|
|
37
|
+
className={cx(
|
|
38
|
+
'cd-alert',
|
|
39
|
+
`cd-alert--${variant}`,
|
|
40
|
+
size === 'sm' && 'cd-alert--sm',
|
|
41
|
+
inline && 'cd-alert--inline',
|
|
42
|
+
className,
|
|
43
|
+
)}
|
|
44
|
+
{...rest}
|
|
45
|
+
>
|
|
46
|
+
{icon != null && <span className="cd-alert__icon">{icon}</span>}
|
|
47
|
+
<div className="cd-alert__content">
|
|
48
|
+
{title != null && <div className="cd-alert__title">{title}</div>}
|
|
49
|
+
{children != null && <div className="cd-alert__body">{children}</div>}
|
|
50
|
+
</div>
|
|
51
|
+
{onDismiss && (
|
|
52
|
+
<button className="cd-alert__dismiss" onClick={onDismiss} aria-label="Dismiss">
|
|
53
|
+
✕
|
|
54
|
+
</button>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
const cx = (...parts: Array<string | false | null | undefined>) =>
|
|
4
|
+
parts.filter(Boolean).join(' ');
|
|
5
|
+
|
|
6
|
+
export interface AugButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
7
|
+
/** `primary` fills with the accent-subtle background and a drop-shadow glow. */
|
|
8
|
+
variant?: 'default' | 'primary';
|
|
9
|
+
/** `sm` and `lg` adjust height, padding and clip size; `md` is the default. */
|
|
10
|
+
size?: 'sm' | 'md' | 'lg';
|
|
11
|
+
/** Reverse the diagonal clip to cut the TR+BL corners instead of TL+BR. */
|
|
12
|
+
reverse?: boolean;
|
|
13
|
+
children?: ReactNode;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Augmented clip-path button — neon-bordered diagonal-corner button matching cd-aug-btn. */
|
|
17
|
+
export function AugButton({
|
|
18
|
+
variant = 'default',
|
|
19
|
+
size = 'md',
|
|
20
|
+
reverse = false,
|
|
21
|
+
className = '',
|
|
22
|
+
children,
|
|
23
|
+
...rest
|
|
24
|
+
}: AugButtonProps) {
|
|
25
|
+
return (
|
|
26
|
+
<button
|
|
27
|
+
className={cx(
|
|
28
|
+
'cd-aug-btn',
|
|
29
|
+
variant === 'primary' && 'cd-aug-btn--primary',
|
|
30
|
+
size === 'sm' && 'cd-aug-btn--sm',
|
|
31
|
+
size === 'lg' && 'cd-aug-btn--lg',
|
|
32
|
+
reverse && 'cd-aug-btn--rev',
|
|
33
|
+
className,
|
|
34
|
+
)}
|
|
35
|
+
{...rest}
|
|
36
|
+
>
|
|
37
|
+
{children}
|
|
38
|
+
</button>
|
|
39
|
+
);
|
|
40
|
+
}
|