omnira-ui 0.3.1 → 0.5.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.
Files changed (48) hide show
  1. package/cli/omnira-init.mjs +213 -33
  2. package/components/ui/ActivityGauge/ActivityGauge.module.css +43 -0
  3. package/components/ui/ActivityGauge/MultiActivityGauge.tsx +141 -0
  4. package/components/ui/ActivityGauge/index.ts +2 -0
  5. package/components/ui/AgentThinking/AgentThinking.module.css +428 -0
  6. package/components/ui/AgentThinking/AgentThinking.tsx +346 -0
  7. package/components/ui/AgentThinking/config.ts +324 -0
  8. package/components/ui/AgentThinking/index.ts +11 -0
  9. package/components/ui/AgentThinking/types.ts +38 -0
  10. package/components/ui/Alert/Alert.module.css +224 -0
  11. package/components/ui/Alert/Alert.tsx +138 -0
  12. package/components/ui/Alert/index.ts +2 -0
  13. package/components/ui/Breadcrumbs/Breadcrumbs.module.css +134 -0
  14. package/components/ui/Breadcrumbs/Breadcrumbs.tsx +139 -0
  15. package/components/ui/Breadcrumbs/index.ts +2 -0
  16. package/components/ui/Calendar/Calendar.module.css +235 -6
  17. package/components/ui/Calendar/Calendar.tsx +212 -11
  18. package/components/ui/Calendar/config.ts +54 -4
  19. package/components/ui/CodeSnippet/CodeSnippet.module.css +158 -0
  20. package/components/ui/CodeSnippet/CodeSnippet.tsx +106 -0
  21. package/components/ui/CodeSnippet/index.ts +2 -0
  22. package/components/ui/ContentDivider/ContentDivider.module.css +214 -0
  23. package/components/ui/ContentDivider/ContentDivider.tsx +197 -0
  24. package/components/ui/ContentDivider/index.ts +10 -0
  25. package/components/ui/DatePicker/DatePicker.module.css +319 -0
  26. package/components/ui/DatePicker/DatePicker.tsx +614 -0
  27. package/components/ui/DatePicker/index.ts +14 -0
  28. package/components/ui/FileUpload/FileUpload.module.css +322 -0
  29. package/components/ui/FileUpload/FileUpload.tsx +413 -0
  30. package/components/ui/FileUpload/index.ts +8 -0
  31. package/components/ui/LoadingIndicator/LoadingIndicator.module.css +381 -0
  32. package/components/ui/LoadingIndicator/LoadingIndicator.tsx +226 -0
  33. package/components/ui/LoadingIndicator/index.ts +8 -0
  34. package/components/ui/Metric/Metric.module.css +271 -19
  35. package/components/ui/Metric/Metric.tsx +189 -44
  36. package/components/ui/Modal/Modal.module.css +402 -0
  37. package/components/ui/Modal/Modal.tsx +123 -0
  38. package/components/ui/Modal/index.ts +2 -0
  39. package/components/ui/Notification/Notification.module.css +305 -0
  40. package/components/ui/Notification/Notification.tsx +282 -0
  41. package/components/ui/Notification/index.ts +13 -0
  42. package/components/ui/SlideOut/SlideOut.module.css +1420 -0
  43. package/components/ui/SlideOut/SlideOut.tsx +81 -0
  44. package/components/ui/SlideOut/index.ts +2 -0
  45. package/components/ui/Tabs/Tabs.module.css +257 -0
  46. package/components/ui/Tabs/Tabs.tsx +149 -0
  47. package/components/ui/Tabs/index.ts +2 -0
  48. package/package.json +3 -2
@@ -25,6 +25,28 @@ const __filename = fileURLToPath(import.meta.url);
25
25
  const __dirname = path.dirname(__filename);
26
26
  const PKG_ROOT = path.resolve(__dirname, "..");
27
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
+
28
50
  // ── ANSI helpers ─────────────────────────────────────────────────────
29
51
 
30
52
  const RESET = "\x1b[0m";
@@ -266,13 +288,21 @@ async function main() {
266
288
 
267
289
  const cwd = process.cwd();
268
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
+
269
299
  // ── 1. Copy all base components ──
270
300
  const componentsSrc = path.join(PKG_ROOT, "components", "ui");
271
- const componentsDest = path.join(cwd, "components", "ui");
301
+ const componentsDest = path.join(base, "components", "ui");
272
302
 
273
303
  if (fs.existsSync(componentsSrc)) {
274
304
  const count = copyDirRecursive(componentsSrc, componentsDest);
275
- 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}`);
276
306
  } else {
277
307
  log(` ${RED}✗${RESET} Components source not found at ${componentsSrc}`);
278
308
  log(` ${DIM}This may happen when running locally. Components are bundled in the npm package.${RESET}`);
@@ -280,7 +310,7 @@ async function main() {
280
310
 
281
311
  // ── 2. Copy lib utilities ──
282
312
  const libFiles = ["cn.ts", "copy-to-clipboard.ts", "theme-context.tsx"];
283
- const libDest = path.join(cwd, "lib");
313
+ const libDest = path.join(base, "lib");
284
314
  fs.mkdirSync(libDest, { recursive: true });
285
315
 
286
316
  for (const file of libFiles) {
@@ -293,7 +323,7 @@ async function main() {
293
323
 
294
324
  // ── 3. Copy globals.css (design system tokens) ──
295
325
  const globalsSrc = path.join(PKG_ROOT, "app", "globals.css");
296
- const appDir = path.join(cwd, "app");
326
+ const appDir = path.join(base, "app");
297
327
  fs.mkdirSync(appDir, { recursive: true });
298
328
 
299
329
  if (copyFile(globalsSrc, path.join(appDir, "globals.css"))) {
@@ -311,7 +341,7 @@ async function main() {
311
341
  log(` ${GREEN}✓${RESET} Created ${BOLD}omnira-overrides.css${RESET}`);
312
342
 
313
343
  // ── 6. Generate providers.tsx ──
314
- const providersPath = path.join(appDir, "providers.tsx");
344
+ const providersPath = path.join(base, "app", "providers.tsx");
315
345
  if (!fs.existsSync(providersPath)) {
316
346
  fs.writeFileSync(providersPath, generateProviders(themeMode), "utf-8");
317
347
  log(` ${GREEN}✓${RESET} Created ${BOLD}app/providers.tsx${RESET} ${DIM}(ThemeProvider wrapper)${RESET}`);
@@ -358,6 +388,88 @@ async function main() {
358
388
  blank();
359
389
  }
360
390
 
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
+
361
473
  // ── Add command — copy a single component ───────────────────────────
362
474
 
363
475
  function getAvailableComponents() {
@@ -371,10 +483,11 @@ function getAvailableComponents() {
371
483
 
372
484
  function listComponents() {
373
485
  const components = getAvailableComponents();
486
+ const bundles = Object.keys(PAGE_BUNDLES).sort();
374
487
 
375
488
  blank();
376
489
  log(` ${BOLD}${GREEN}✦${RESET} ${BOLD}${WHITE}Omnira UI — Available Components${RESET}`);
377
- log(` ${DIM}Copy individual components into your project${RESET}`);
490
+ log(` ${DIM}Copy individual components or full page bundles into your project${RESET}`);
378
491
  blank();
379
492
 
380
493
  if (components.length === 0) {
@@ -383,6 +496,9 @@ function listComponents() {
383
496
  return;
384
497
  }
385
498
 
499
+ log(` ${BOLD}${WHITE}Individual Components:${RESET}`);
500
+ blank();
501
+
386
502
  const cols = 3;
387
503
  const rows = Math.ceil(components.length / cols);
388
504
  const colWidth = 22;
@@ -399,15 +515,38 @@ function listComponents() {
399
515
  }
400
516
 
401
517
  blank();
402
- log(` ${DIM}Usage:${RESET} ${CYAN}npx omnira-ui add <Component>${RESET}`);
403
- log(` ${DIM}Example:${RESET} ${CYAN}npx omnira-ui add Table${RESET}`);
404
- log(` ${DIM}Multiple:${RESET} ${CYAN}npx omnira-ui add Table Button Badge${RESET}`);
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}`);
405
543
  blank();
406
544
  }
407
545
 
408
546
  function ensureLibDeps(cwd) {
547
+ const base = getProjectBase(cwd);
409
548
  const libFiles = ["cn.ts", "copy-to-clipboard.ts", "theme-context.tsx"];
410
- const libDest = path.join(cwd, "lib");
549
+ const libDest = path.join(base, "lib");
411
550
  let copied = 0;
412
551
 
413
552
  for (const file of libFiles) {
@@ -422,7 +561,7 @@ function ensureLibDeps(cwd) {
422
561
  }
423
562
 
424
563
  // Ensure globals.css exists
425
- const globalsDest = path.join(cwd, "app", "globals.css");
564
+ const globalsDest = path.join(base, "app", "globals.css");
426
565
  if (!fs.existsSync(globalsDest)) {
427
566
  const globalsSrc = path.join(PKG_ROOT, "app", "globals.css");
428
567
  if (copyFile(globalsSrc, globalsDest)) {
@@ -434,39 +573,78 @@ function ensureLibDeps(cwd) {
434
573
  return copied;
435
574
  }
436
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
+
437
608
  function addComponents(componentNames) {
438
609
  const cwd = process.cwd();
610
+ const base = getProjectBase(cwd);
611
+ const srcPrefix = detectSrcDir(cwd);
439
612
  const available = getAvailableComponents();
440
- const availableLower = available.map((c) => c.toLowerCase());
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()]);
441
619
 
442
620
  blank();
443
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
+ }
444
628
  blank();
445
629
 
446
630
  let totalFiles = 0;
447
631
  const added = [];
448
- const notFound = [];
449
-
450
- for (const name of componentNames) {
451
- // Case-insensitive match
452
- const idx = availableLower.indexOf(name.toLowerCase());
453
- if (idx === -1) {
454
- notFound.push(name);
455
- continue;
456
- }
632
+ const destPrefix = srcPrefix ? srcPrefix + "/" : "";
457
633
 
458
- const actualName = available[idx];
634
+ for (const actualName of resolved) {
459
635
  const src = path.join(PKG_ROOT, "components", "ui", actualName);
460
- const dest = path.join(cwd, "components", "ui", actualName);
636
+ const dest = path.join(base, "components", "ui", actualName);
637
+
638
+ if (!fs.existsSync(src)) continue;
461
639
 
462
640
  const count = copyDirRecursive(src, dest);
463
- log(` ${GREEN}✓${RESET} Copied ${BOLD}${actualName}${RESET} ${DIM}(${count} files)${RESET} → ${DIM}components/ui/${actualName}/${RESET}`);
641
+ log(` ${GREEN}✓${RESET} Copied ${BOLD}${actualName}${RESET} ${DIM}(${count} files)${RESET} → ${DIM}${destPrefix}components/ui/${actualName}/${RESET}`);
464
642
  totalFiles += count;
465
643
  added.push(actualName);
466
644
  }
467
645
 
468
- for (const name of notFound) {
469
- log(` ${RED}✗${RESET} Component "${name}" not found.`);
646
+ for (const name of unknown) {
647
+ log(` ${RED}✗${RESET} "${name}" is not a component or page bundle.`);
470
648
  }
471
649
 
472
650
  // Ensure lib dependencies are present
@@ -490,13 +668,14 @@ function addComponents(componentNames) {
490
668
  }
491
669
  blank();
492
670
 
493
- if (!fs.existsSync(path.join(cwd, "app", "globals.css"))) {
671
+ const globalsPath = path.join(base, "app", "globals.css");
672
+ if (!fs.existsSync(globalsPath)) {
494
673
  log(` ${YELLOW}!${RESET} Don't forget to import ${BOLD}globals.css${RESET} in your root layout.`);
495
674
  blank();
496
675
  }
497
676
  }
498
677
 
499
- if (notFound.length > 0) {
678
+ if (unknown.length > 0) {
500
679
  log(` ${YELLOW}!${RESET} Run ${CYAN}npx omnira-ui add${RESET} to see all available components.`);
501
680
  blank();
502
681
  }
@@ -512,15 +691,16 @@ function showHelp() {
512
691
  log(` ${BOLD}${WHITE}Commands:${RESET}`);
513
692
  blank();
514
693
  log(` ${CYAN}npx omnira-ui init${RESET} Scaffold the full design system`);
515
- log(` ${CYAN}npx omnira-ui add <Component>${RESET} Add a single component`);
516
- log(` ${CYAN}npx omnira-ui add${RESET} List all available components`);
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`);
517
696
  log(` ${CYAN}npx omnira-ui help${RESET} Show this help message`);
518
697
  blank();
519
698
  log(` ${BOLD}${WHITE}Examples:${RESET}`);
520
699
  blank();
521
- log(` ${CYAN}npx omnira-ui add Table${RESET}`);
522
- log(` ${CYAN}npx omnira-ui add Button Badge Input${RESET}`);
523
- log(` ${CYAN}npx omnira-ui init${RESET}`);
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}`);
524
704
  blank();
525
705
  }
526
706
 
@@ -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";