omnira-ui 0.6.6 → 0.6.7

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 (117) hide show
  1. package/README.md +16 -0
  2. package/app/globals.css +21 -1
  3. package/cli/omnira-init.mjs +45 -0
  4. package/components/ui/ActivityGauge/ActivityGauge.module.css +1 -2
  5. package/components/ui/AgentThinking/AgentThinking.module.css +13 -11
  6. package/components/ui/AgentThinking/AgentThinking.tsx +133 -58
  7. package/components/ui/AssetAllocation/AssetAllocation.module.css +119 -0
  8. package/components/ui/AssetAllocation/AssetAllocation.tsx +88 -0
  9. package/components/ui/AssetAllocation/index.ts +2 -0
  10. package/components/ui/Button/Button.module.css +0 -1
  11. package/components/ui/ButtonUtility/ButtonUtility.module.css +0 -1
  12. package/components/ui/Card/Card.module.css +1 -2
  13. package/components/ui/CardHeader/CardHeader.module.css +1 -2
  14. package/components/ui/CashflowAnalytics/CashflowAnalytics.module.css +198 -0
  15. package/components/ui/CashflowAnalytics/CashflowAnalytics.tsx +133 -0
  16. package/components/ui/CashflowAnalytics/index.ts +2 -0
  17. package/components/ui/CashflowRing/CashflowRing.module.css +109 -0
  18. package/components/ui/CashflowRing/CashflowRing.tsx +84 -0
  19. package/components/ui/CashflowRing/index.ts +2 -0
  20. package/components/ui/CashflowSplit/CashflowSplit.module.css +209 -0
  21. package/components/ui/CashflowSplit/CashflowSplit.tsx +116 -0
  22. package/components/ui/CashflowSplit/index.ts +2 -0
  23. package/components/ui/CashflowSummary/CashflowSummary.module.css +136 -0
  24. package/components/ui/CashflowSummary/CashflowSummary.tsx +87 -0
  25. package/components/ui/CashflowSummary/index.ts +2 -0
  26. package/components/ui/ConvertCurrency/ConvertCurrency.module.css +163 -0
  27. package/components/ui/ConvertCurrency/ConvertCurrency.tsx +82 -0
  28. package/components/ui/ConvertCurrency/index.ts +2 -0
  29. package/components/ui/CreditCard/CreditCard.module.css +1 -2
  30. package/components/ui/CreditCardUsed/CreditCardUsed.module.css +144 -0
  31. package/components/ui/CreditCardUsed/CreditCardUsed.tsx +93 -0
  32. package/components/ui/CreditCardUsed/index.ts +2 -0
  33. package/components/ui/CreditPaymentPlanner/CreditPaymentPlanner.module.css +170 -0
  34. package/components/ui/CreditPaymentPlanner/CreditPaymentPlanner.tsx +120 -0
  35. package/components/ui/CreditPaymentPlanner/index.ts +2 -0
  36. package/components/ui/CreditScore/CreditScore.module.css +101 -0
  37. package/components/ui/CreditScore/CreditScore.tsx +92 -0
  38. package/components/ui/CreditScore/index.ts +2 -0
  39. package/components/ui/DailyRevenue/DailyRevenue.module.css +119 -0
  40. package/components/ui/DailyRevenue/DailyRevenue.tsx +70 -0
  41. package/components/ui/DailyRevenue/index.ts +2 -0
  42. package/components/ui/EmergencyFunds/EmergencyFunds.module.css +123 -0
  43. package/components/ui/EmergencyFunds/EmergencyFunds.tsx +79 -0
  44. package/components/ui/EmergencyFunds/index.ts +2 -0
  45. package/components/ui/EmptyState/EmptyState.module.css +1 -2
  46. package/components/ui/ExpensesChart/ExpensesChart.module.css +143 -0
  47. package/components/ui/ExpensesChart/ExpensesChart.tsx +82 -0
  48. package/components/ui/ExpensesChart/index.ts +2 -0
  49. package/components/ui/FinancialGrowth/FinancialGrowth.module.css +152 -0
  50. package/components/ui/FinancialGrowth/FinancialGrowth.tsx +81 -0
  51. package/components/ui/FinancialGrowth/index.ts +2 -0
  52. package/components/ui/GoalProgress/GoalProgress.module.css +144 -0
  53. package/components/ui/GoalProgress/GoalProgress.tsx +78 -0
  54. package/components/ui/GoalProgress/index.ts +2 -0
  55. package/components/ui/IncomeBreakdown/IncomeBreakdown.module.css +101 -0
  56. package/components/ui/IncomeBreakdown/IncomeBreakdown.tsx +62 -0
  57. package/components/ui/IncomeBreakdown/index.ts +2 -0
  58. package/components/ui/InvestmentChart/InvestmentChart.module.css +105 -0
  59. package/components/ui/InvestmentChart/InvestmentChart.tsx +71 -0
  60. package/components/ui/InvestmentChart/index.ts +2 -0
  61. package/components/ui/LanguageSelector/LanguageSelector.module.css +238 -0
  62. package/components/ui/LanguageSelector/LanguageSelector.tsx +261 -0
  63. package/components/ui/LanguageSelector/index.ts +7 -0
  64. package/components/ui/LanguageSelector/languageAbbrev.ts +18 -0
  65. package/components/ui/LanguageSelector/types.ts +9 -0
  66. package/components/ui/MarketingHeader/MarketingHeader.module.css +637 -0
  67. package/components/ui/MarketingHeader/MarketingHeader.tsx +531 -0
  68. package/components/ui/MarketingHeader/index.ts +8 -0
  69. package/components/ui/Metric/Metric.module.css +3 -6
  70. package/components/ui/Modal/Modal.module.css +5 -3
  71. package/components/ui/MonthlySubscription/MonthlySubscription.module.css +145 -0
  72. package/components/ui/MonthlySubscription/MonthlySubscription.tsx +92 -0
  73. package/components/ui/MonthlySubscription/index.ts +2 -0
  74. package/components/ui/MyCard/MyCard.module.css +152 -0
  75. package/components/ui/MyCard/MyCard.tsx +77 -0
  76. package/components/ui/MyCard/index.ts +2 -0
  77. package/components/ui/PageHeader/PageHeader.module.css +1 -2
  78. package/components/ui/Rating/Rating.module.css +0 -1
  79. package/components/ui/SavedMoney/SavedMoney.module.css +151 -0
  80. package/components/ui/SavedMoney/SavedMoney.tsx +92 -0
  81. package/components/ui/SavedMoney/index.ts +2 -0
  82. package/components/ui/SavingAccount/SavingAccount.module.css +227 -0
  83. package/components/ui/SavingAccount/SavingAccount.tsx +121 -0
  84. package/components/ui/SavingAccount/index.ts +2 -0
  85. package/components/ui/SavingsBuckets/SavingsBuckets.module.css +117 -0
  86. package/components/ui/SavingsBuckets/SavingsBuckets.tsx +74 -0
  87. package/components/ui/SavingsBuckets/index.ts +2 -0
  88. package/components/ui/SavingsGoals/SavingsGoals.module.css +176 -0
  89. package/components/ui/SavingsGoals/SavingsGoals.tsx +88 -0
  90. package/components/ui/SavingsGoals/index.ts +2 -0
  91. package/components/ui/SavingsMonthly/SavingsMonthly.module.css +227 -0
  92. package/components/ui/SavingsMonthly/SavingsMonthly.tsx +171 -0
  93. package/components/ui/SavingsMonthly/index.ts +2 -0
  94. package/components/ui/SendMoney/SendMoney.module.css +224 -0
  95. package/components/ui/SendMoney/SendMoney.tsx +98 -0
  96. package/components/ui/SendMoney/index.ts +2 -0
  97. package/components/ui/SendMoneyCompact/SendMoneyCompact.module.css +147 -0
  98. package/components/ui/SendMoneyCompact/SendMoneyCompact.tsx +64 -0
  99. package/components/ui/SendMoneyCompact/index.ts +2 -0
  100. package/components/ui/SocialButton/SocialButton.module.css +0 -1
  101. package/components/ui/SpendingLimit/SpendingLimit.module.css +124 -0
  102. package/components/ui/SpendingLimit/SpendingLimit.tsx +73 -0
  103. package/components/ui/SpendingLimit/index.ts +2 -0
  104. package/components/ui/SpendsBreakdown/SpendsBreakdown.module.css +180 -0
  105. package/components/ui/SpendsBreakdown/SpendsBreakdown.tsx +107 -0
  106. package/components/ui/SpendsBreakdown/index.ts +2 -0
  107. package/components/ui/StockPosition/StockPosition.module.css +150 -0
  108. package/components/ui/StockPosition/StockPosition.tsx +84 -0
  109. package/components/ui/StockPosition/index.ts +2 -0
  110. package/components/ui/Table/Table.module.css +0 -1
  111. package/components/ui/TotalBalance/TotalBalance.module.css +112 -0
  112. package/components/ui/TotalBalance/TotalBalance.tsx +105 -0
  113. package/components/ui/TotalBalance/index.ts +2 -0
  114. package/components/ui/WeeklyExpenditure/WeeklyExpenditure.module.css +101 -0
  115. package/components/ui/WeeklyExpenditure/WeeklyExpenditure.tsx +63 -0
  116. package/components/ui/WeeklyExpenditure/index.ts +2 -0
  117. package/package.json +2 -1
package/README.md CHANGED
@@ -122,6 +122,22 @@ Browse the full library and copy advanced components (Sidebar, Feature Cards, et
122
122
  - **User Account Menu** — Popup with profile, settings, account switching, sign out
123
123
  - **Search Bar** — Integrated ⌘K search component
124
124
 
125
+ ### Industry Widgets
126
+
127
+ Drop-in dashboard widgets that follow the system design tokens — accent colors track the configurator and never go off-system.
128
+
129
+ - **Fintech / Large** (10) — `CashflowAnalytics`, `SendMoney`, `CashflowSplit`, `SavingAccount`, `FinancialGrowth`, `SpendsBreakdown`, `SavingsMonthly`, `CreditPaymentPlanner`, `SavingsGoals`, `ExpensesChart`
130
+ - **Fintech / Medium** (20) — `EmergencyFunds`, `IncomeBreakdown`, `WeeklyExpenditure`, `CreditCardUsed`, `AssetAllocation`, `MonthlySubscription`, `SavingsBuckets`, `MyCard`, `ConvertCurrency`, `SendMoneyCompact`, `TotalBalance`, `StockPosition`, `SpendingLimit`, `GoalProgress`, `SavedMoney`, `CashflowSummary`, `CashflowRing`, `CreditScore`, `InvestmentChart`, `DailyRevenue`
131
+
132
+ Install all of one size in a single command:
133
+
134
+ ```bash
135
+ npx omnira-ui add fintech-widgets # 10 large widgets
136
+ npx omnira-ui add fintech-widgets-medium # 20 medium widgets
137
+ ```
138
+
139
+ Or grab any single widget by its kebab-case slug — e.g. `npx omnira-ui add cashflow-analytics`. Browse them at [omnira.one/docs/widgets/fintech](https://omnira.one/docs/widgets/fintech).
140
+
125
141
  ---
126
142
 
127
143
  ## Design System
package/app/globals.css CHANGED
@@ -13,29 +13,49 @@
13
13
 
14
14
  /* --- CSS Custom Properties: Dark Mode (Default) --- */
15
15
  [data-theme="dark"] {
16
+ /* Primary Accent (Mint/Teal for Pulse Dashboard) */
17
+ --color-primary: #3FC39E;
18
+ --color-primary-hover: #36B08E;
19
+ --color-primary-gradient: #2D9A7A;
20
+ --color-primary-text: #121212;
21
+
22
+ /* Legacy Lime Accent (Kept for backward compatibility with old components) */
16
23
  --color-lime: #D2FE17;
17
24
  --color-lime-hover: #c0e616;
18
25
  --color-lime-gradient: #ABC928;
19
26
  --color-lime-text: #121212;
20
27
 
28
+ /* Backgrounds */
21
29
  --color-bg-primary: #202020;
22
30
  --color-bg-secondary: #1a1a1a;
23
31
  --color-bg-card: rgba(248, 248, 248, 0.03);
24
32
  --color-bg-elevated: rgba(248, 248, 248, 0.06);
25
33
  --color-bg-input: rgba(248, 248, 248, 0.04);
26
34
  --color-bg-hover: rgba(248, 248, 248, 0.05);
35
+ --color-bg-sunken: rgba(248, 248, 248, 0.02);
27
36
  --color-bg-overlay: rgba(10, 10, 10, 0.97);
28
37
  --color-bg-sidebar: #1a1a1a;
29
38
 
39
+ /* Primary Accent Backgrounds */
40
+ --color-bg-primary-subtle: rgba(63, 195, 158, 0.06);
41
+ --color-bg-primary-medium: rgba(63, 195, 158, 0.08);
42
+ --color-bg-primary-strong: rgba(63, 195, 158, 0.12);
43
+
30
44
  --color-text-primary: rgba(248, 248, 248, 0.95);
31
45
  --color-text-secondary: rgba(248, 248, 248, 0.70);
32
46
  --color-text-tertiary: rgba(248, 248, 248, 0.50);
33
47
 
48
+ /* Borders */
34
49
  --color-border-subtle: rgba(255, 255, 255, 0.05);
35
50
  --color-border-standard: rgba(255, 255, 255, 0.06);
36
51
  --color-border-medium: rgba(255, 255, 255, 0.08);
37
52
  --color-border-strong: rgba(255, 255, 255, 0.15);
38
53
 
54
+ /* Primary Accent Borders */
55
+ --color-border-primary-subtle: rgba(63, 195, 158, 0.10);
56
+ --color-border-primary-medium: rgba(63, 195, 158, 0.15);
57
+ --color-border-primary-strong: rgba(63, 195, 158, 0.30);
58
+
39
59
  --color-border-lime-subtle: rgba(210, 254, 23, 0.1);
40
60
  --color-border-lime-medium: rgba(210, 254, 23, 0.15);
41
61
  --color-border-lime-strong: rgba(210, 254, 23, 0.3);
@@ -62,7 +82,7 @@
62
82
  --shadow-card-light: inset 1px 2px 12px rgba(248, 248, 248, 0.03), 0px 8px 28px rgba(0, 0, 0, 0.12);
63
83
  --shadow-card-accent: inset 2px 4px 16px rgba(210, 254, 23, 0.04), 0px 16px 48px rgba(0, 0, 0, 0.2);
64
84
  --shadow-card-hover: inset 2px 4px 16px rgba(248, 248, 248, 0.08), 0px 12px 40px rgba(0, 0, 0, 0.28);
65
- --shadow-btn-primary: 0 8px 24px rgba(210, 254, 23, 0.3);
85
+ --shadow-btn-primary: 0 8px 24px rgba(63, 195, 158, 0.3);
66
86
  --shadow-glow-lime: 0 0 8px rgba(210, 254, 23, 0.6), 0 0 16px rgba(210, 254, 23, 0.3);
67
87
 
68
88
  --gradient-text: linear-gradient(93deg, rgba(248, 248, 248, 0.9), rgba(248, 248, 248, 0.5));
@@ -406,6 +406,7 @@ const PAGE_BUNDLES = {
406
406
  // Navigation
407
407
  "sidebar-navigation": ["SidebarNavigation", "Button", "Avatar", "Badge", "Dropdown", "Toggle", "Tooltip"],
408
408
  "header-navigation": ["Button", "Avatar", "Badge", "Dropdown"],
409
+ "language-selector": ["LanguageSelector"],
409
410
  // Modals
410
411
  "modal": ["Modal", "Button", "Badge", "Input", "Toggle", "Checkbox"],
411
412
  "modals": ["Modal", "Button", "Badge", "Input", "Toggle", "Checkbox"],
@@ -492,6 +493,50 @@ const PAGE_BUNDLES = {
492
493
  "not-found-page": ["NotFoundPage", "Button"],
493
494
  "404": ["NotFoundPage", "Button"],
494
495
  "email-template": ["EmailTemplate"],
496
+ // ── Marketing component bundles ──
497
+ "marketing-header": ["MarketingHeader", "Button"],
498
+ // ── Widget bundles — industry-specific UI blocks ──
499
+ "fintech-widgets": [
500
+ "CashflowAnalytics", "SendMoney", "CashflowSplit", "SavingAccount", "FinancialGrowth",
501
+ "SpendsBreakdown", "SavingsMonthly", "CreditPaymentPlanner", "SavingsGoals", "ExpensesChart",
502
+ ],
503
+ "cashflow-analytics": ["CashflowAnalytics"],
504
+ "send-money": ["SendMoney"],
505
+ "cashflow-split": ["CashflowSplit"],
506
+ "saving-account": ["SavingAccount"],
507
+ "financial-growth": ["FinancialGrowth"],
508
+ "spends-breakdown": ["SpendsBreakdown"],
509
+ "savings-monthly": ["SavingsMonthly"],
510
+ "credit-payment-planner": ["CreditPaymentPlanner"],
511
+ "savings-goals": ["SavingsGoals"],
512
+ "expenses-chart": ["ExpensesChart"],
513
+ // ── Medium fintech widgets ──
514
+ "fintech-widgets-medium": [
515
+ "EmergencyFunds", "IncomeBreakdown", "WeeklyExpenditure", "CreditCardUsed", "AssetAllocation",
516
+ "MonthlySubscription", "SavingsBuckets", "MyCard", "ConvertCurrency", "SendMoneyCompact",
517
+ "TotalBalance", "StockPosition", "SpendingLimit", "GoalProgress", "SavedMoney",
518
+ "CashflowSummary", "CashflowRing", "CreditScore", "InvestmentChart", "DailyRevenue",
519
+ ],
520
+ "emergency-funds": ["EmergencyFunds"],
521
+ "income-breakdown": ["IncomeBreakdown"],
522
+ "weekly-expenditure": ["WeeklyExpenditure"],
523
+ "credit-card-used": ["CreditCardUsed"],
524
+ "asset-allocation": ["AssetAllocation"],
525
+ "monthly-subscription": ["MonthlySubscription"],
526
+ "savings-buckets": ["SavingsBuckets"],
527
+ "my-card": ["MyCard"],
528
+ "convert-currency": ["ConvertCurrency"],
529
+ "send-money-compact": ["SendMoneyCompact"],
530
+ "total-balance": ["TotalBalance"],
531
+ "stock-position": ["StockPosition"],
532
+ "spending-limit": ["SpendingLimit"],
533
+ "goal-progress": ["GoalProgress"],
534
+ "saved-money": ["SavedMoney"],
535
+ "cashflow-summary": ["CashflowSummary"],
536
+ "cashflow-ring": ["CashflowRing"],
537
+ "credit-score": ["CreditScore"],
538
+ "investment-chart": ["InvestmentChart"],
539
+ "daily-revenue": ["DailyRevenue"],
495
540
  };
496
541
 
497
542
  // ── Add command — copy a single component ───────────────────────────
@@ -11,8 +11,7 @@
11
11
  border-radius: var(--radius-lg);
12
12
  background: var(--color-bg-card);
13
13
  border: 1px solid var(--color-border-standard);
14
- box-shadow: var(--shadow-card);
15
- backdrop-filter: var(--blur-standard);
14
+ box-shadow: var(--shadow-card-light);
16
15
  }
17
16
 
18
17
  /* ── SVG Gauge ── */
@@ -35,7 +35,7 @@
35
35
  border-radius: var(--radius-lg);
36
36
  background: var(--color-bg-card);
37
37
  border: 1px solid var(--color-border-standard);
38
- overflow: hidden;
38
+ overflow: visible;
39
39
  }
40
40
 
41
41
  /* ── Header bar ── */
@@ -221,7 +221,8 @@
221
221
  z-index: 1;
222
222
  flex: 1;
223
223
  overflow: hidden;
224
- height: 28px;
224
+ /* 8 × 4pt grid; 28px clipped .stateBadge borders */
225
+ height: 32px;
225
226
  justify-content: center;
226
227
  }
227
228
 
@@ -306,17 +307,15 @@
306
307
  position: relative;
307
308
  }
308
309
 
310
+ /* Near-opaque surface so the activity log stays readable over arbitrary UI
311
+ (card tokens are glass / low-alpha by design — see --color-bg-card). */
309
312
  .popover {
310
- position: absolute;
311
- top: calc(100% + 8px);
312
- left: 50%;
313
- transform: translateX(-50%);
314
- width: 280px;
315
313
  border-radius: var(--radius-lg);
316
- background: var(--color-bg-card);
314
+ background: var(--color-bg-overlay);
317
315
  border: 1px solid var(--color-border-standard);
318
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
319
- z-index: 100;
316
+ box-shadow:
317
+ 0 4px 6px rgba(0, 0, 0, 0.08),
318
+ 0 12px 40px rgba(0, 0, 0, 0.28);
320
319
  overflow: hidden;
321
320
  }
322
321
 
@@ -418,7 +417,10 @@
418
417
  background: transparent !important;
419
418
  border: none !important;
420
419
  border-radius: var(--radius-lg) !important;
421
- overflow: hidden !important;
420
+ overflow: visible !important;
421
+ /* Docs: portaled popover is position:fixed — extra margin separates stacked demos
422
+ so the next section sits lower and overlaps less in the viewport. */
423
+ margin-bottom: 5rem !important;
422
424
  }
423
425
 
424
426
  /* ── Responsive ── */
@@ -1,6 +1,8 @@
1
1
  "use client";
2
2
 
3
- import { useState, useEffect, useRef, useCallback } from "react";
3
+ import { useState, useEffect, useRef, useCallback, forwardRef } from "react";
4
+ import { createPortal } from "react-dom";
5
+ import { autoUpdate, flip, offset, shift, useFloating } from "@floating-ui/react-dom";
4
6
  import { cn } from "@/lib/cn";
5
7
  import { motion, AnimatePresence } from "framer-motion";
6
8
  import type { AgentConfig, AgentActivity, AgentState } from "./types";
@@ -101,11 +103,13 @@ function AgentRing({ state, color }: { state: AgentState; color: string }) {
101
103
  interface AgentChipProps {
102
104
  agent: AgentConfig;
103
105
  activity: AgentActivity;
104
- isPopoverOpen: boolean;
105
106
  onTogglePopover: () => void;
106
107
  }
107
108
 
108
- function AgentChip({ agent, activity, isPopoverOpen, onTogglePopover }: AgentChipProps) {
109
+ const AgentChip = forwardRef<HTMLDivElement, AgentChipProps>(function AgentChip(
110
+ { agent, activity, onTogglePopover },
111
+ ref,
112
+ ) {
109
113
  const isAnimating = activity.state === "thinking" || activity.state === "active";
110
114
  const statusText = useTextCycler(
111
115
  activity.currentAction?.text ?? "",
@@ -121,7 +125,7 @@ function AgentChip({ agent, activity, isPopoverOpen, onTogglePopover }: AgentChi
121
125
  } as React.CSSProperties;
122
126
 
123
127
  return (
124
- <div className={s.popoverAnchor}>
128
+ <div ref={ref} className={s.popoverAnchor}>
125
129
  <motion.div
126
130
  className={cn(s.chip, isAnimating && s.chipActive)}
127
131
  style={chipStyle}
@@ -149,7 +153,7 @@ function AgentChip({ agent, activity, isPopoverOpen, onTogglePopover }: AgentChi
149
153
  {agent.name.charAt(0)}
150
154
  </div>
151
155
  {activity.state === "completed" && (
152
- <div className={s.completedCheck}>
156
+ <div className={s.completedCheck} style={{ background: agent.color }}>
153
157
  <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
154
158
  <path d="M3 7l3 3 5-5" stroke="#fff" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
155
159
  </svg>
@@ -176,58 +180,106 @@ function AgentChip({ agent, activity, isPopoverOpen, onTogglePopover }: AgentChi
176
180
  )}
177
181
  </div>
178
182
  </motion.div>
179
-
180
- {/* Popover */}
181
- <AnimatePresence>
182
- {isPopoverOpen && (
183
- <motion.div
184
- className={s.popover}
185
- initial={{ opacity: 0, y: -6, scale: 0.96 }}
186
- animate={{ opacity: 1, y: 0, scale: 1 }}
187
- exit={{ opacity: 0, y: -4, scale: 0.98 }}
188
- transition={{ duration: 0.25, ease: [0.16, 1, 0.3, 1] }}
189
- >
190
- <div className={s.popoverHeader}>
191
- <span className={s.popoverName} style={{ color: agent.color }}>{agent.name}</span>
192
- <span className={s.popoverRole}>{agent.role}</span>
193
- </div>
194
- <div className={s.popoverBody}>
195
- {activity.activityLog.map((entry, i) => (
196
- <motion.div
197
- key={i}
198
- className={s.logEntry}
199
- initial={{ opacity: 0, x: -8 }}
200
- animate={{ opacity: 1, x: 0 }}
201
- transition={{ delay: i * 0.06, duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
202
- >
203
- <div className={s.logIcon}>
204
- <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
205
- <circle cx="6" cy="6" r="2" fill="currentColor" />
206
- </svg>
207
- </div>
208
- <div className={s.logContent}>
209
- <span className={s.logText}>{entry.text}</span>
210
- <div className={s.logMeta}>
211
- <span className={s.logTime}>{entry.time}</span>
212
- <span className={s.logStatus}>
213
- <span
214
- className={s.logStatusDot}
215
- style={{ background: entry.status === "active" ? agent.color : "var(--color-success)" }}
216
- />
217
- {entry.status === "active" ? "active" : "done"}
218
- </span>
219
- </div>
220
- </div>
221
- </motion.div>
222
- ))}
223
- </div>
224
- </motion.div>
225
- )}
226
- </AnimatePresence>
227
183
  </div>
228
184
  );
185
+ });
186
+
187
+ const POPOVER_WIDTH = 280;
188
+
189
+ interface AgentPopoverContentProps {
190
+ agent: AgentConfig;
191
+ activity: AgentActivity;
192
+ anchorEl: HTMLElement | null;
229
193
  }
230
194
 
195
+ const AgentPopoverContent = forwardRef<HTMLDivElement, AgentPopoverContentProps>(
196
+ function AgentPopoverContent({ agent, activity, anchorEl }, ref) {
197
+ const { refs, floatingStyles } = useFloating({
198
+ open: Boolean(anchorEl),
199
+ placement: "bottom-start",
200
+ strategy: "fixed",
201
+ /* top/left positioning — avoids fighting Framer Motion transforms on the inner surface */
202
+ transform: false,
203
+ middleware: [
204
+ offset(8),
205
+ flip({ fallbackPlacements: ["top-start"] }),
206
+ shift({ padding: 8 }),
207
+ ],
208
+ whileElementsMounted: autoUpdate,
209
+ elements: {
210
+ reference: anchorEl,
211
+ },
212
+ });
213
+
214
+ const setFloatingRef = useCallback(
215
+ (node: HTMLDivElement | null) => {
216
+ refs.setFloating(node);
217
+ if (typeof ref === "function") ref(node);
218
+ else if (ref) ref.current = node;
219
+ },
220
+ [refs.setFloating, ref],
221
+ );
222
+
223
+ if (!anchorEl) return null;
224
+
225
+ return (
226
+ <div
227
+ ref={setFloatingRef}
228
+ style={{
229
+ ...floatingStyles,
230
+ width: POPOVER_WIDTH,
231
+ /* Root stacking: floatingStyles has no z-index; without this, chips (transform/z-index)
232
+ in the row can paint above the portaled popover. */
233
+ zIndex: 11_000,
234
+ }}
235
+ >
236
+ <motion.div
237
+ className={s.popover}
238
+ initial={{ opacity: 0, y: -6, scale: 0.96 }}
239
+ animate={{ opacity: 1, y: 0, scale: 1 }}
240
+ exit={{ opacity: 0, y: -4, scale: 0.98 }}
241
+ transition={{ duration: 0.25, ease: [0.16, 1, 0.3, 1] }}
242
+ >
243
+ <div className={s.popoverHeader}>
244
+ <span className={s.popoverName} style={{ color: agent.color }}>{agent.name}</span>
245
+ <span className={s.popoverRole}>{agent.role}</span>
246
+ </div>
247
+ <div className={s.popoverBody}>
248
+ {activity.activityLog.map((entry, i) => (
249
+ <motion.div
250
+ key={i}
251
+ className={s.logEntry}
252
+ initial={{ opacity: 0, x: -8 }}
253
+ animate={{ opacity: 1, x: 0 }}
254
+ transition={{ delay: i * 0.06, duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
255
+ >
256
+ <div className={s.logIcon}>
257
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
258
+ <circle cx="6" cy="6" r="2" fill="currentColor" />
259
+ </svg>
260
+ </div>
261
+ <div className={s.logContent}>
262
+ <span className={s.logText}>{entry.text}</span>
263
+ <div className={s.logMeta}>
264
+ <span className={s.logTime}>{entry.time}</span>
265
+ <span className={s.logStatus}>
266
+ <span
267
+ className={s.logStatusDot}
268
+ style={{ background: entry.status === "active" ? agent.color : "var(--color-success)" }}
269
+ />
270
+ {entry.status === "active" ? "active" : "done"}
271
+ </span>
272
+ </div>
273
+ </div>
274
+ </motion.div>
275
+ ))}
276
+ </div>
277
+ </motion.div>
278
+ </div>
279
+ );
280
+ },
281
+ );
282
+
231
283
  /* ══════════════════════════════════════════════
232
284
  Main AgentThinking component
233
285
  ══════════════════════════════════════════════ */
@@ -242,6 +294,8 @@ export function AgentThinking({ agents, title = "Agent Activity", className }: A
242
294
  const [activities, setActivities] = useState<Record<string, AgentActivity>>({});
243
295
  const [openPopover, setOpenPopover] = useState<string | null>(null);
244
296
  const containerRef = useRef<HTMLDivElement>(null);
297
+ const popoverPortalRef = useRef<HTMLDivElement>(null);
298
+ const chipAnchorRefs = useRef<Record<string, HTMLDivElement | null>>({});
245
299
 
246
300
  /* ── Initialize activities with rotating demo states ── */
247
301
  useEffect(() => {
@@ -305,12 +359,13 @@ export function AgentThinking({ agents, title = "Agent Activity", className }: A
305
359
  return () => clearInterval(interval);
306
360
  }, [agents]);
307
361
 
308
- /* ── Close popover on outside click ── */
362
+ /* ── Close popover on outside click (chip container + portaled popover) ── */
309
363
  useEffect(() => {
310
364
  function handleClick(e: MouseEvent) {
311
- if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
312
- setOpenPopover(null);
313
- }
365
+ const t = e.target as Node;
366
+ if (containerRef.current?.contains(t)) return;
367
+ if (popoverPortalRef.current?.contains(t)) return;
368
+ setOpenPopover(null);
314
369
  }
315
370
  document.addEventListener("mousedown", handleClick);
316
371
  return () => document.removeEventListener("mousedown", handleClick);
@@ -324,6 +379,10 @@ export function AgentThinking({ agents, title = "Agent Activity", className }: A
324
379
  a => a.state === "active" || a.state === "thinking"
325
380
  ).length;
326
381
 
382
+ const openAgent = openPopover ? agents.find(a => a.id === openPopover) : undefined;
383
+ const openActivity = openPopover ? activities[openPopover] : undefined;
384
+ const anchorEl = openPopover ? chipAnchorRefs.current[openPopover] ?? null : null;
385
+
327
386
  return (
328
387
  <div className={cn(s.container, className)} ref={containerRef}>
329
388
  <div className={s.headerBar}>
@@ -340,13 +399,29 @@ export function AgentThinking({ agents, title = "Agent Activity", className }: A
340
399
  {agents.map(agent => (
341
400
  <AgentChip
342
401
  key={agent.id}
402
+ ref={(el) => {
403
+ chipAnchorRefs.current[agent.id] = el;
404
+ }}
343
405
  agent={agent}
344
406
  activity={activities[agent.id] ?? { state: "idle", activityLog: [] }}
345
- isPopoverOpen={openPopover === agent.id}
346
407
  onTogglePopover={() => togglePopover(agent.id)}
347
408
  />
348
409
  ))}
349
410
  </div>
411
+ {typeof document !== "undefined" && createPortal(
412
+ <AnimatePresence>
413
+ {openPopover && openAgent && openActivity && (
414
+ <AgentPopoverContent
415
+ key={openPopover}
416
+ ref={popoverPortalRef}
417
+ agent={openAgent}
418
+ activity={openActivity}
419
+ anchorEl={anchorEl}
420
+ />
421
+ )}
422
+ </AnimatePresence>,
423
+ document.body,
424
+ )}
350
425
  </div>
351
426
  );
352
427
  }
@@ -0,0 +1,119 @@
1
+ /* ============================================
2
+ AssetAllocation — Widget / Medium / 20
3
+ Pie chart with central amount + legend. Surface uses system tokens; per-
4
+ slice colors are passed by the component (default: 3 shades of accent lime).
5
+ ============================================ */
6
+
7
+ .widget {
8
+ position: relative;
9
+ display: flex;
10
+ flex-direction: column;
11
+ gap: 10px;
12
+ padding: 16px;
13
+ width: 100%;
14
+ max-width: 348px;
15
+ aspect-ratio: 348 / 164;
16
+ border-radius: var(--radius-2xl);
17
+ border: 1px solid var(--color-border-standard);
18
+ overflow: hidden;
19
+ isolation: isolate;
20
+ box-shadow: var(--shadow-card-light);
21
+ background: var(--color-bg-card);
22
+ }
23
+
24
+ .title {
25
+ font-family: var(--font-body);
26
+ font-size: 12px;
27
+ font-weight: 500;
28
+ line-height: 16px;
29
+ letter-spacing: -0.01em;
30
+ color: var(--color-text-primary);
31
+ }
32
+
33
+ .body {
34
+ flex: 1;
35
+ display: flex;
36
+ align-items: center;
37
+ gap: 16px;
38
+ width: 100%;
39
+ min-height: 0;
40
+ }
41
+
42
+ /* ── Pie chart ── */
43
+ .pieWrap {
44
+ position: relative;
45
+ aspect-ratio: 1;
46
+ height: 100%;
47
+ flex-shrink: 0;
48
+ }
49
+
50
+ .pie {
51
+ width: 100%;
52
+ height: 100%;
53
+ overflow: visible;
54
+ }
55
+
56
+ .pieHole {
57
+ fill: var(--color-bg-card);
58
+ }
59
+
60
+ .pieAmount {
61
+ position: absolute;
62
+ inset: 0;
63
+ display: flex;
64
+ align-items: center;
65
+ justify-content: center;
66
+ font-family: var(--font-display);
67
+ font-size: 13px;
68
+ font-weight: 600;
69
+ line-height: 1;
70
+ letter-spacing: -0.02em;
71
+ color: var(--color-text-primary);
72
+ pointer-events: none;
73
+ }
74
+
75
+ /* ── Legend ── */
76
+ .legend {
77
+ list-style: none;
78
+ margin: 0;
79
+ padding: 0;
80
+ flex: 1;
81
+ display: flex;
82
+ flex-direction: column;
83
+ gap: 10px;
84
+ min-width: 0;
85
+ }
86
+
87
+ .legendRow {
88
+ display: flex;
89
+ align-items: baseline;
90
+ justify-content: space-between;
91
+ gap: 8px;
92
+ width: 100%;
93
+ }
94
+
95
+ .legendLabel {
96
+ font-family: var(--font-body);
97
+ font-size: 10px;
98
+ line-height: 12px;
99
+ color: var(--color-text-primary);
100
+ flex: 1;
101
+ min-width: 0;
102
+ overflow: hidden;
103
+ text-overflow: ellipsis;
104
+ white-space: nowrap;
105
+ }
106
+
107
+ .legendPercent {
108
+ font-family: var(--font-body);
109
+ font-size: 10px;
110
+ line-height: 12px;
111
+ font-weight: 500;
112
+ color: var(--color-text-primary);
113
+ }
114
+
115
+ @media (max-width: 480px) {
116
+ .widget {
117
+ max-width: 100%;
118
+ }
119
+ }
@@ -0,0 +1,88 @@
1
+ "use client";
2
+
3
+ import { cn } from "@/lib/cn";
4
+ import styles from "./AssetAllocation.module.css";
5
+
6
+ export interface AssetAllocationSegment {
7
+ label: string;
8
+ /** Share percent of the pie (0–100). Segment percents should sum to 100. */
9
+ percent: number;
10
+ /** CSS color used for the pie slice and the legend bullet. */
11
+ color: string;
12
+ }
13
+
14
+ export interface AssetAllocationProps {
15
+ title?: string;
16
+ /** Total amount centered in the pie, e.g. "$13,420". */
17
+ amount?: string;
18
+ segments?: AssetAllocationSegment[];
19
+ className?: string;
20
+ }
21
+
22
+ const DEFAULT_SEGMENTS: AssetAllocationSegment[] = [
23
+ { label: "Crypto", percent: 7.5, color: "var(--color-lime-gradient)" },
24
+ { label: "Mutual Funds", percent: 25.5, color: "var(--color-lime-hover)" },
25
+ { label: "Cash", percent: 67, color: "var(--color-lime)" },
26
+ ];
27
+
28
+ /** Generate an SVG path for a pie slice from `startPct` → `endPct` (0–100). */
29
+ function pieSlicePath(cx: number, cy: number, r: number, startPct: number, endPct: number): string {
30
+ if (endPct - startPct >= 100) {
31
+ // Full circle — draw two half-arcs to avoid the degenerate same-point issue.
32
+ return `M ${cx + r} ${cy} A ${r} ${r} 0 1 1 ${cx - r} ${cy} A ${r} ${r} 0 1 1 ${cx + r} ${cy} Z`;
33
+ }
34
+ const startAngle = (startPct / 100) * 360 - 90;
35
+ const endAngle = (endPct / 100) * 360 - 90;
36
+ const startRad = (startAngle * Math.PI) / 180;
37
+ const endRad = (endAngle * Math.PI) / 180;
38
+ const x1 = cx + r * Math.cos(startRad);
39
+ const y1 = cy + r * Math.sin(startRad);
40
+ const x2 = cx + r * Math.cos(endRad);
41
+ const y2 = cy + r * Math.sin(endRad);
42
+ const largeArc = endPct - startPct > 50 ? 1 : 0;
43
+ return `M ${cx} ${cy} L ${x1} ${y1} A ${r} ${r} 0 ${largeArc} 1 ${x2} ${y2} Z`;
44
+ }
45
+
46
+ export function AssetAllocation({
47
+ title = "Asset Class Allocation",
48
+ amount = "$13,420",
49
+ segments = DEFAULT_SEGMENTS,
50
+ className,
51
+ }: AssetAllocationProps) {
52
+ const total = Math.max(1, segments.reduce((sum, s) => sum + s.percent, 0));
53
+ let runningPct = 0;
54
+ const slices = segments.map((s) => {
55
+ const startPct = runningPct;
56
+ const endPct = runningPct + (s.percent / total) * 100;
57
+ runningPct = endPct;
58
+ return { ...s, path: pieSlicePath(50, 50, 44, startPct, endPct) };
59
+ });
60
+
61
+ return (
62
+ <div className={cn(styles.widget, className)}>
63
+ <span className={styles.title}>{title}</span>
64
+
65
+ <div className={styles.body}>
66
+ <div className={styles.pieWrap}>
67
+ <svg viewBox="0 0 100 100" className={styles.pie} aria-hidden="true">
68
+ {slices.map((s, i) => (
69
+ <path key={i} d={s.path} fill={s.color} />
70
+ ))}
71
+ {/* Larger hole = thinner visible ring + more room for the central amount. */}
72
+ <circle cx="50" cy="50" r="34" className={styles.pieHole} />
73
+ </svg>
74
+ <span className={styles.pieAmount}>{amount}</span>
75
+ </div>
76
+
77
+ <ul className={styles.legend}>
78
+ {segments.map((s, i) => (
79
+ <li key={i} className={styles.legendRow}>
80
+ <span className={styles.legendLabel}>{s.label}</span>
81
+ <span className={styles.legendPercent}>{s.percent}%</span>
82
+ </li>
83
+ ))}
84
+ </ul>
85
+ </div>
86
+ </div>
87
+ );
88
+ }