omnira-ui 0.3.0 → 0.4.1

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.
@@ -1,20 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Omnira UI — Project Scaffolding CLI
4
+ * Omnira UI — CLI
5
5
  *
6
- * Usage: npx omnira-ui init
6
+ * Commands:
7
+ * npx omnira-ui init Full project scaffolding
8
+ * npx omnira-ui add <Component> Copy a single component into your project
9
+ * npx omnira-ui add List all available components
7
10
  *
8
- * Scaffolds the full Omnira UI design system into your project:
9
- * - All base components components/ui/
10
- * - Utility helpers → lib/
11
- * - Theme provider → lib/theme-context.tsx
12
- * - Design tokens CSS → app/globals.css
13
- * - Accent overrides → omnira-overrides.css (if non-default)
14
- * - Config file → omnira.config.ts
15
- *
16
- * Advanced components (Sidebar, Feature Cards, etc.) can be copied
17
- * from the documentation site: https://ui.omnira.space
11
+ * The `add` command copies only the requested component folder into
12
+ * components/ui/ and ensures required lib utilities (cn.ts, etc.)
13
+ * and globals.css are present. No full scaffolding needed.
18
14
  */
19
15
 
20
16
  import * as readline from "node:readline";
@@ -29,6 +25,28 @@ const __filename = fileURLToPath(import.meta.url);
29
25
  const __dirname = path.dirname(__filename);
30
26
  const PKG_ROOT = path.resolve(__dirname, "..");
31
27
 
28
+ // ── Detect src/ directory structure ─────────────────────────────────
29
+
30
+ function detectSrcDir(cwd) {
31
+ const srcDir = path.join(cwd, "src");
32
+ if (fs.existsSync(srcDir)) {
33
+ // If src/ contains app/, components/, or lib/, it's a src-based project
34
+ if (
35
+ fs.existsSync(path.join(srcDir, "app")) ||
36
+ fs.existsSync(path.join(srcDir, "components")) ||
37
+ fs.existsSync(path.join(srcDir, "lib"))
38
+ ) {
39
+ return "src";
40
+ }
41
+ }
42
+ return "";
43
+ }
44
+
45
+ function getProjectBase(cwd) {
46
+ const srcPrefix = detectSrcDir(cwd);
47
+ return srcPrefix ? path.join(cwd, srcPrefix) : cwd;
48
+ }
49
+
32
50
  // ── ANSI helpers ─────────────────────────────────────────────────────
33
51
 
34
52
  const RESET = "\x1b[0m";
@@ -270,13 +288,21 @@ async function main() {
270
288
 
271
289
  const cwd = process.cwd();
272
290
 
291
+ // ── Detect project structure ──
292
+ const base = getProjectBase(cwd);
293
+ const srcPrefix = detectSrcDir(cwd);
294
+ if (srcPrefix) {
295
+ log(` ${DIM}Detected ${BOLD}${srcPrefix}/${RESET}${DIM} directory structure${RESET}`);
296
+ blank();
297
+ }
298
+
273
299
  // ── 1. Copy all base components ──
274
300
  const componentsSrc = path.join(PKG_ROOT, "components", "ui");
275
- const componentsDest = path.join(cwd, "components", "ui");
301
+ const componentsDest = path.join(base, "components", "ui");
276
302
 
277
303
  if (fs.existsSync(componentsSrc)) {
278
304
  const count = copyDirRecursive(componentsSrc, componentsDest);
279
- log(` ${GREEN}✓${RESET} Copied ${BOLD}${count} files${RESET} → ${DIM}components/ui/${RESET}`);
305
+ log(` ${GREEN}✓${RESET} Copied ${BOLD}${count} files${RESET} → ${DIM}${srcPrefix ? srcPrefix + "/" : ""}components/ui/${RESET}`);
280
306
  } else {
281
307
  log(` ${RED}✗${RESET} Components source not found at ${componentsSrc}`);
282
308
  log(` ${DIM}This may happen when running locally. Components are bundled in the npm package.${RESET}`);
@@ -284,7 +310,7 @@ async function main() {
284
310
 
285
311
  // ── 2. Copy lib utilities ──
286
312
  const libFiles = ["cn.ts", "copy-to-clipboard.ts", "theme-context.tsx"];
287
- const libDest = path.join(cwd, "lib");
313
+ const libDest = path.join(base, "lib");
288
314
  fs.mkdirSync(libDest, { recursive: true });
289
315
 
290
316
  for (const file of libFiles) {
@@ -297,7 +323,7 @@ async function main() {
297
323
 
298
324
  // ── 3. Copy globals.css (design system tokens) ──
299
325
  const globalsSrc = path.join(PKG_ROOT, "app", "globals.css");
300
- const appDir = path.join(cwd, "app");
326
+ const appDir = path.join(base, "app");
301
327
  fs.mkdirSync(appDir, { recursive: true });
302
328
 
303
329
  if (copyFile(globalsSrc, path.join(appDir, "globals.css"))) {
@@ -315,7 +341,7 @@ async function main() {
315
341
  log(` ${GREEN}✓${RESET} Created ${BOLD}omnira-overrides.css${RESET}`);
316
342
 
317
343
  // ── 6. Generate providers.tsx ──
318
- const providersPath = path.join(appDir, "providers.tsx");
344
+ const providersPath = path.join(base, "app", "providers.tsx");
319
345
  if (!fs.existsSync(providersPath)) {
320
346
  fs.writeFileSync(providersPath, generateProviders(themeMode), "utf-8");
321
347
  log(` ${GREEN}✓${RESET} Created ${BOLD}app/providers.tsx${RESET} ${DIM}(ThemeProvider wrapper)${RESET}`);
@@ -362,7 +388,342 @@ async function main() {
362
388
  blank();
363
389
  }
364
390
 
365
- main().catch((err) => {
366
- console.error(err);
367
- process.exit(1);
368
- });
391
+ // ── Page bundles — map page slugs to all required components ────────
392
+
393
+ const PAGE_BUNDLES = {
394
+ // Card Headers
395
+ "card-header": ["CardHeader", "Button", "Badge", "Avatar", "Dropdown"],
396
+ "card-headers": ["CardHeader", "Button", "Badge", "Avatar", "Dropdown"],
397
+ // Page Headers
398
+ "page-header": ["PageHeader", "Button", "Badge"],
399
+ "page-headers": ["PageHeader", "Button", "Badge"],
400
+ // Section Headers
401
+ "section-header": ["Button", "Badge"],
402
+ "section-headers": ["Button", "Badge"],
403
+ // Section Footers
404
+ "section-footer": ["Button"],
405
+ "section-footers": ["Button"],
406
+ // Navigation
407
+ "sidebar-navigation": ["SidebarNavigation", "Button", "Avatar", "Badge", "Dropdown", "Toggle", "Tooltip"],
408
+ "header-navigation": ["Button", "Avatar", "Badge", "Dropdown"],
409
+ // Modals
410
+ "modal": ["Modal", "Button", "Badge", "Input", "Toggle", "Checkbox"],
411
+ "modals": ["Modal", "Button", "Badge", "Input", "Toggle", "Checkbox"],
412
+ // Command Menus
413
+ "command-menu": ["Button", "Input", "Badge"],
414
+ "command-menus": ["Button", "Input", "Badge"],
415
+ // Charts
416
+ "line-bar-chart": ["Card"],
417
+ "line-bar-charts": ["Card"],
418
+ "activity-gauge": ["ActivityGauge"],
419
+ "activity-gauges": ["ActivityGauge"],
420
+ "pie-chart": ["Card"],
421
+ "pie-charts": ["Card"],
422
+ "radar-chart": ["Card"],
423
+ "radar-charts": ["Card"],
424
+ // Metrics
425
+ "metrics": ["Metric", "Button"],
426
+ // Slide Out
427
+ "slide-out": ["SlideOut", "Button", "Input", "Badge"],
428
+ // Inline CTA
429
+ "inline-cta": ["Button", "Badge", "Input"],
430
+ // Pagination
431
+ "pagination": ["Button", "ButtonUtility"],
432
+ // Carousel
433
+ "carousel": ["Button", "ButtonUtility"],
434
+ // Progress Steps
435
+ "progress-steps": ["ProgressBar", "Badge"],
436
+ // Activity Feed
437
+ "activity-feed": ["Avatar", "Badge", "Button"],
438
+ // Messaging
439
+ "messaging": ["Avatar", "Button", "Input"],
440
+ // Tabs
441
+ "tabs": ["Button", "Badge"],
442
+ // Table
443
+ "table": ["Table", "Avatar", "Badge", "Button", "ButtonUtility", "Dropdown", "ProgressBar"],
444
+ // Breadcrumbs
445
+ "breadcrumbs": ["Button"],
446
+ // Alerts
447
+ "alert": ["Button", "Badge"],
448
+ "alerts": ["Button", "Badge"],
449
+ // Notifications
450
+ "notification": ["Button", "Avatar", "Badge"],
451
+ "notifications": ["Button", "Avatar", "Badge"],
452
+ // Date Picker
453
+ "date-picker": ["Button", "Input"],
454
+ // Calendar
455
+ "calendar": ["Calendar", "Button"],
456
+ // File Upload
457
+ "file-upload": ["Button", "ProgressBar"],
458
+ // Content Divider
459
+ "content-divider": ["Button"],
460
+ // Loading Indicator
461
+ "loading-indicator": ["ProgressBar"],
462
+ // Empty States
463
+ "empty-state": ["EmptyState", "Button"],
464
+ "empty-states": ["EmptyState", "Button"],
465
+ // Code Snippet
466
+ "code-snippet": ["Button"],
467
+ // Card
468
+ "card": ["Card", "Button", "Badge"],
469
+ // Matrix
470
+ "matrix": ["Card"],
471
+ };
472
+
473
+ // ── Add command — copy a single component ───────────────────────────
474
+
475
+ function getAvailableComponents() {
476
+ const componentsDir = path.join(PKG_ROOT, "components", "ui");
477
+ if (!fs.existsSync(componentsDir)) return [];
478
+ return fs.readdirSync(componentsDir, { withFileTypes: true })
479
+ .filter((d) => d.isDirectory())
480
+ .map((d) => d.name)
481
+ .sort();
482
+ }
483
+
484
+ function listComponents() {
485
+ const components = getAvailableComponents();
486
+ const bundles = Object.keys(PAGE_BUNDLES).sort();
487
+
488
+ blank();
489
+ log(` ${BOLD}${GREEN}✦${RESET} ${BOLD}${WHITE}Omnira UI — Available Components${RESET}`);
490
+ log(` ${DIM}Copy individual components or full page bundles into your project${RESET}`);
491
+ blank();
492
+
493
+ if (components.length === 0) {
494
+ log(` ${RED}✗${RESET} No components found. This may happen when running locally.`);
495
+ blank();
496
+ return;
497
+ }
498
+
499
+ log(` ${BOLD}${WHITE}Individual Components:${RESET}`);
500
+ blank();
501
+
502
+ const cols = 3;
503
+ const rows = Math.ceil(components.length / cols);
504
+ const colWidth = 22;
505
+
506
+ for (let r = 0; r < rows; r++) {
507
+ let line = " ";
508
+ for (let c = 0; c < cols; c++) {
509
+ const idx = r + c * rows;
510
+ if (idx < components.length) {
511
+ line += components[idx].padEnd(colWidth);
512
+ }
513
+ }
514
+ log(line);
515
+ }
516
+
517
+ blank();
518
+ log(` ${BOLD}${WHITE}Page Bundles:${RESET} ${DIM}(installs all components needed for a page)${RESET}`);
519
+ blank();
520
+
521
+ const bCols = 3;
522
+ const bRows = Math.ceil(bundles.length / bCols);
523
+
524
+ for (let r = 0; r < bRows; r++) {
525
+ let line = " ";
526
+ for (let c = 0; c < bCols; c++) {
527
+ const idx = r + c * bRows;
528
+ if (idx < bundles.length) {
529
+ const slug = bundles[idx];
530
+ const count = PAGE_BUNDLES[slug].length;
531
+ line += `${slug} ${DIM}(${count})${RESET}`.padEnd(colWidth + 10);
532
+ }
533
+ }
534
+ log(line);
535
+ }
536
+
537
+ blank();
538
+ log(` ${BOLD}${WHITE}Usage:${RESET}`);
539
+ blank();
540
+ log(` ${CYAN}npx omnira-ui add Table${RESET} ${DIM}Single component${RESET}`);
541
+ log(` ${CYAN}npx omnira-ui add card-headers${RESET} ${DIM}All components for Card Headers page${RESET}`);
542
+ log(` ${CYAN}npx omnira-ui add Button Badge${RESET} ${DIM}Multiple components${RESET}`);
543
+ blank();
544
+ }
545
+
546
+ function ensureLibDeps(cwd) {
547
+ const base = getProjectBase(cwd);
548
+ const libFiles = ["cn.ts", "copy-to-clipboard.ts", "theme-context.tsx"];
549
+ const libDest = path.join(base, "lib");
550
+ let copied = 0;
551
+
552
+ for (const file of libFiles) {
553
+ const dest = path.join(libDest, file);
554
+ if (!fs.existsSync(dest)) {
555
+ const src = path.join(PKG_ROOT, "lib", file);
556
+ if (copyFile(src, dest)) {
557
+ log(` ${GREEN}✓${RESET} Copied ${BOLD}lib/${file}${RESET} ${DIM}(dependency)${RESET}`);
558
+ copied++;
559
+ }
560
+ }
561
+ }
562
+
563
+ // Ensure globals.css exists
564
+ const globalsDest = path.join(base, "app", "globals.css");
565
+ if (!fs.existsSync(globalsDest)) {
566
+ const globalsSrc = path.join(PKG_ROOT, "app", "globals.css");
567
+ if (copyFile(globalsSrc, globalsDest)) {
568
+ log(` ${GREEN}✓${RESET} Copied ${BOLD}app/globals.css${RESET} ${DIM}(design tokens)${RESET}`);
569
+ copied++;
570
+ }
571
+ }
572
+
573
+ return copied;
574
+ }
575
+
576
+ function resolveNames(inputNames) {
577
+ // Expand page bundle aliases into individual component names
578
+ const resolved = new Set();
579
+ const unknown = [];
580
+
581
+ const available = getAvailableComponents();
582
+ const availableLower = available.map((c) => c.toLowerCase());
583
+
584
+ for (const name of inputNames) {
585
+ const lower = name.toLowerCase();
586
+
587
+ // Check page bundles first
588
+ if (PAGE_BUNDLES[lower]) {
589
+ for (const comp of PAGE_BUNDLES[lower]) {
590
+ resolved.add(comp);
591
+ }
592
+ continue;
593
+ }
594
+
595
+ // Check individual component (case-insensitive)
596
+ const idx = availableLower.indexOf(lower);
597
+ if (idx !== -1) {
598
+ resolved.add(available[idx]);
599
+ continue;
600
+ }
601
+
602
+ unknown.push(name);
603
+ }
604
+
605
+ return { resolved: [...resolved], unknown };
606
+ }
607
+
608
+ function addComponents(componentNames) {
609
+ const cwd = process.cwd();
610
+ const base = getProjectBase(cwd);
611
+ const srcPrefix = detectSrcDir(cwd);
612
+ const available = getAvailableComponents();
613
+
614
+ // Resolve page bundles and individual names
615
+ const { resolved, unknown } = resolveNames(componentNames);
616
+
617
+ // Check if any input was a page bundle for nicer messaging
618
+ const isBundle = componentNames.some((n) => PAGE_BUNDLES[n.toLowerCase()]);
619
+
620
+ blank();
621
+ log(` ${BOLD}${GREEN}✦${RESET} ${BOLD}${WHITE}Omnira UI — Add Components${RESET}`);
622
+ if (isBundle) {
623
+ log(` ${DIM}Installing page bundle: ${componentNames.filter((n) => PAGE_BUNDLES[n.toLowerCase()]).join(", ")}${RESET}`);
624
+ }
625
+ if (srcPrefix) {
626
+ log(` ${DIM}Detected ${BOLD}${srcPrefix}/${RESET}${DIM} directory structure${RESET}`);
627
+ }
628
+ blank();
629
+
630
+ let totalFiles = 0;
631
+ const added = [];
632
+ const destPrefix = srcPrefix ? srcPrefix + "/" : "";
633
+
634
+ for (const actualName of resolved) {
635
+ const src = path.join(PKG_ROOT, "components", "ui", actualName);
636
+ const dest = path.join(base, "components", "ui", actualName);
637
+
638
+ if (!fs.existsSync(src)) continue;
639
+
640
+ const count = copyDirRecursive(src, dest);
641
+ log(` ${GREEN}✓${RESET} Copied ${BOLD}${actualName}${RESET} ${DIM}(${count} files)${RESET} → ${DIM}${destPrefix}components/ui/${actualName}/${RESET}`);
642
+ totalFiles += count;
643
+ added.push(actualName);
644
+ }
645
+
646
+ for (const name of unknown) {
647
+ log(` ${RED}✗${RESET} "${name}" is not a component or page bundle.`);
648
+ }
649
+
650
+ // Ensure lib dependencies are present
651
+ if (added.length > 0) {
652
+ blank();
653
+ log(` ${DIM}Checking dependencies...${RESET}`);
654
+ ensureLibDeps(cwd);
655
+ }
656
+
657
+ blank();
658
+ log(` ${DIM}─────────────────────────────────────${RESET}`);
659
+ blank();
660
+
661
+ if (added.length > 0) {
662
+ log(` ${GREEN}✓${RESET} ${BOLD}${WHITE}Added ${added.length} component${added.length > 1 ? "s" : ""} (${totalFiles} files)${RESET}`);
663
+ blank();
664
+ log(` ${BOLD}${WHITE}Usage:${RESET}`);
665
+ blank();
666
+ for (const name of added) {
667
+ log(` ${MAGENTA}import${RESET} { ${name} } ${MAGENTA}from${RESET} ${WHITE}"@/components/ui/${name}"${RESET};`);
668
+ }
669
+ blank();
670
+
671
+ const globalsPath = path.join(base, "app", "globals.css");
672
+ if (!fs.existsSync(globalsPath)) {
673
+ log(` ${YELLOW}!${RESET} Don't forget to import ${BOLD}globals.css${RESET} in your root layout.`);
674
+ blank();
675
+ }
676
+ }
677
+
678
+ if (unknown.length > 0) {
679
+ log(` ${YELLOW}!${RESET} Run ${CYAN}npx omnira-ui add${RESET} to see all available components.`);
680
+ blank();
681
+ }
682
+ }
683
+
684
+ // ── Help ─────────────────────────────────────────────────────────────
685
+
686
+ function showHelp() {
687
+ blank();
688
+ log(` ${BOLD}${GREEN}✦${RESET} ${BOLD}${WHITE}Omnira UI — CLI${RESET}`);
689
+ log(` ${DIM}The premium glassmorphism design system${RESET}`);
690
+ blank();
691
+ log(` ${BOLD}${WHITE}Commands:${RESET}`);
692
+ blank();
693
+ log(` ${CYAN}npx omnira-ui init${RESET} Scaffold the full design system`);
694
+ log(` ${CYAN}npx omnira-ui add <name>${RESET} Add a component or page bundle`);
695
+ log(` ${CYAN}npx omnira-ui add${RESET} List all components & page bundles`);
696
+ log(` ${CYAN}npx omnira-ui help${RESET} Show this help message`);
697
+ blank();
698
+ log(` ${BOLD}${WHITE}Examples:${RESET}`);
699
+ blank();
700
+ log(` ${CYAN}npx omnira-ui add Table${RESET} ${DIM}Single component${RESET}`);
701
+ log(` ${CYAN}npx omnira-ui add card-headers${RESET} ${DIM}All components for Card Headers page${RESET}`);
702
+ log(` ${CYAN}npx omnira-ui add Button Badge${RESET} ${DIM}Multiple components${RESET}`);
703
+ log(` ${CYAN}npx omnira-ui init${RESET} ${DIM}Full project scaffolding${RESET}`);
704
+ blank();
705
+ }
706
+
707
+ // ── Command router ──────────────────────────────────────────────────
708
+
709
+ const args = process.argv.slice(2);
710
+ const command = args[0]?.toLowerCase();
711
+
712
+ if (!command || command === "init") {
713
+ main().catch((err) => {
714
+ console.error(err);
715
+ process.exit(1);
716
+ });
717
+ } else if (command === "add") {
718
+ const componentNames = args.slice(1);
719
+ if (componentNames.length === 0) {
720
+ listComponents();
721
+ } else {
722
+ addComponents(componentNames);
723
+ }
724
+ } else if (command === "help" || command === "--help" || command === "-h") {
725
+ showHelp();
726
+ } else {
727
+ log(`\n ${RED}✗${RESET} Unknown command: "${command}"\n`);
728
+ showHelp();
729
+ }
@@ -100,6 +100,43 @@
100
100
  margin-top: -8px;
101
101
  }
102
102
 
103
+ /* ── Multi-ring center label ── */
104
+
105
+ .multiCenterLabel {
106
+ font-size: 12px;
107
+ color: var(--color-text-tertiary);
108
+ line-height: 1;
109
+ margin-bottom: 2px;
110
+ }
111
+
112
+ /* ── Legend ── */
113
+
114
+ .legend {
115
+ display: flex;
116
+ align-items: center;
117
+ justify-content: center;
118
+ gap: 16px;
119
+ flex-wrap: wrap;
120
+ }
121
+
122
+ .legendItem {
123
+ display: flex;
124
+ align-items: center;
125
+ gap: 6px;
126
+ }
127
+
128
+ .legendDot {
129
+ width: 8px;
130
+ height: 8px;
131
+ border-radius: var(--radius-full);
132
+ flex-shrink: 0;
133
+ }
134
+
135
+ .legendText {
136
+ font-size: 13px;
137
+ color: var(--color-text-secondary);
138
+ }
139
+
103
140
  /* ── Grid layout for demos ── */
104
141
 
105
142
  .gaugeGrid {
@@ -107,3 +144,9 @@
107
144
  grid-template-columns: repeat(4, 1fr);
108
145
  gap: 16px;
109
146
  }
147
+
148
+ .multiGaugeGrid {
149
+ display: grid;
150
+ grid-template-columns: repeat(3, 1fr);
151
+ gap: 16px;
152
+ }
@@ -0,0 +1,141 @@
1
+ import { cn } from "@/lib/cn";
2
+ import styles from "./ActivityGauge.module.css";
3
+
4
+ /* ── Types ── */
5
+
6
+ export type MultiGaugeColor = "lime" | "info" | "warning" | "error" | "success";
7
+
8
+ export interface RingData {
9
+ value: number;
10
+ max?: number;
11
+ label: string;
12
+ }
13
+
14
+ export interface MultiActivityGaugeProps {
15
+ rings: RingData[];
16
+ centerLabel?: string;
17
+ centerValue?: string | number;
18
+ color?: MultiGaugeColor;
19
+ size?: number;
20
+ strokeWidth?: number;
21
+ gap?: number;
22
+ showLegend?: boolean;
23
+ className?: string;
24
+ }
25
+
26
+ /* ── Color shades (dark → medium → light for each base color) ── */
27
+
28
+ const COLOR_SHADES: Record<MultiGaugeColor, string[]> = {
29
+ lime: [
30
+ "var(--color-lime)",
31
+ "rgba(163, 230, 53, 0.6)",
32
+ "rgba(163, 230, 53, 0.3)",
33
+ ],
34
+ info: [
35
+ "var(--color-info)",
36
+ "rgba(96, 165, 250, 0.6)",
37
+ "rgba(96, 165, 250, 0.3)",
38
+ ],
39
+ warning: [
40
+ "var(--color-warning)",
41
+ "rgba(251, 191, 36, 0.6)",
42
+ "rgba(251, 191, 36, 0.3)",
43
+ ],
44
+ error: [
45
+ "var(--color-error)",
46
+ "rgba(248, 113, 113, 0.6)",
47
+ "rgba(248, 113, 113, 0.3)",
48
+ ],
49
+ success: [
50
+ "var(--color-success)",
51
+ "rgba(74, 222, 128, 0.6)",
52
+ "rgba(74, 222, 128, 0.3)",
53
+ ],
54
+ };
55
+
56
+ /* ── Component ── */
57
+
58
+ export function MultiActivityGauge({
59
+ rings,
60
+ centerLabel,
61
+ centerValue,
62
+ color = "lime",
63
+ size = 180,
64
+ strokeWidth = 10,
65
+ gap = 4,
66
+ showLegend = true,
67
+ className,
68
+ }: MultiActivityGaugeProps) {
69
+ const shades = COLOR_SHADES[color];
70
+ const ringCount = Math.min(rings.length, 3);
71
+
72
+ return (
73
+ <div className={cn(styles.gaugeCard, className)}>
74
+ <div className={styles.gaugeWrapper} style={{ width: size, height: size }}>
75
+ <svg
76
+ className={styles.gaugeSvg}
77
+ width={size}
78
+ height={size}
79
+ viewBox={`0 0 ${size} ${size}`}
80
+ >
81
+ {rings.slice(0, 3).map((ring, i) => {
82
+ const ringRadius =
83
+ (size - strokeWidth) / 2 - i * (strokeWidth + gap);
84
+ const circumference = 2 * Math.PI * ringRadius;
85
+ const percentage = Math.min(ring.value / (ring.max ?? 100), 1);
86
+ const offset = circumference * (1 - percentage);
87
+
88
+ return (
89
+ <g key={i}>
90
+ {/* Track */}
91
+ <circle
92
+ className={styles.gaugeTrack}
93
+ cx={size / 2}
94
+ cy={size / 2}
95
+ r={ringRadius}
96
+ strokeWidth={strokeWidth}
97
+ />
98
+ {/* Fill */}
99
+ <circle
100
+ className={styles.gaugeFill}
101
+ cx={size / 2}
102
+ cy={size / 2}
103
+ r={ringRadius}
104
+ strokeWidth={strokeWidth}
105
+ strokeDasharray={circumference}
106
+ strokeDashoffset={offset}
107
+ style={{ stroke: shades[i] }}
108
+ />
109
+ </g>
110
+ );
111
+ })}
112
+ </svg>
113
+
114
+ {/* Center text */}
115
+ <div className={styles.gaugeCenter}>
116
+ {centerLabel && (
117
+ <span className={styles.multiCenterLabel}>{centerLabel}</span>
118
+ )}
119
+ {centerValue !== undefined && (
120
+ <span className={styles.gaugeValue}>{centerValue}</span>
121
+ )}
122
+ </div>
123
+ </div>
124
+
125
+ {/* Legend */}
126
+ {showLegend && (
127
+ <div className={styles.legend}>
128
+ {rings.slice(0, 3).map((ring, i) => (
129
+ <div key={i} className={styles.legendItem}>
130
+ <span
131
+ className={styles.legendDot}
132
+ style={{ background: shades[i] }}
133
+ />
134
+ <span className={styles.legendText}>{ring.label}</span>
135
+ </div>
136
+ ))}
137
+ </div>
138
+ )}
139
+ </div>
140
+ );
141
+ }
@@ -1,2 +1,4 @@
1
1
  export { ActivityGauge } from "./ActivityGauge";
2
2
  export type { ActivityGaugeProps, GaugeColor } from "./ActivityGauge";
3
+ export { MultiActivityGauge } from "./MultiActivityGauge";
4
+ export type { MultiActivityGaugeProps, RingData, MultiGaugeColor } from "./MultiActivityGauge";