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.
- package/README.md +16 -0
- package/app/globals.css +21 -1
- package/cli/omnira-init.mjs +45 -0
- package/components/ui/ActivityGauge/ActivityGauge.module.css +1 -2
- package/components/ui/AgentThinking/AgentThinking.module.css +13 -11
- package/components/ui/AgentThinking/AgentThinking.tsx +133 -58
- package/components/ui/AssetAllocation/AssetAllocation.module.css +119 -0
- package/components/ui/AssetAllocation/AssetAllocation.tsx +88 -0
- package/components/ui/AssetAllocation/index.ts +2 -0
- package/components/ui/Button/Button.module.css +0 -1
- package/components/ui/ButtonUtility/ButtonUtility.module.css +0 -1
- package/components/ui/Card/Card.module.css +1 -2
- package/components/ui/CardHeader/CardHeader.module.css +1 -2
- package/components/ui/CashflowAnalytics/CashflowAnalytics.module.css +198 -0
- package/components/ui/CashflowAnalytics/CashflowAnalytics.tsx +133 -0
- package/components/ui/CashflowAnalytics/index.ts +2 -0
- package/components/ui/CashflowRing/CashflowRing.module.css +109 -0
- package/components/ui/CashflowRing/CashflowRing.tsx +84 -0
- package/components/ui/CashflowRing/index.ts +2 -0
- package/components/ui/CashflowSplit/CashflowSplit.module.css +209 -0
- package/components/ui/CashflowSplit/CashflowSplit.tsx +116 -0
- package/components/ui/CashflowSplit/index.ts +2 -0
- package/components/ui/CashflowSummary/CashflowSummary.module.css +136 -0
- package/components/ui/CashflowSummary/CashflowSummary.tsx +87 -0
- package/components/ui/CashflowSummary/index.ts +2 -0
- package/components/ui/ConvertCurrency/ConvertCurrency.module.css +163 -0
- package/components/ui/ConvertCurrency/ConvertCurrency.tsx +82 -0
- package/components/ui/ConvertCurrency/index.ts +2 -0
- package/components/ui/CreditCard/CreditCard.module.css +1 -2
- package/components/ui/CreditCardUsed/CreditCardUsed.module.css +144 -0
- package/components/ui/CreditCardUsed/CreditCardUsed.tsx +93 -0
- package/components/ui/CreditCardUsed/index.ts +2 -0
- package/components/ui/CreditPaymentPlanner/CreditPaymentPlanner.module.css +170 -0
- package/components/ui/CreditPaymentPlanner/CreditPaymentPlanner.tsx +120 -0
- package/components/ui/CreditPaymentPlanner/index.ts +2 -0
- package/components/ui/CreditScore/CreditScore.module.css +101 -0
- package/components/ui/CreditScore/CreditScore.tsx +92 -0
- package/components/ui/CreditScore/index.ts +2 -0
- package/components/ui/DailyRevenue/DailyRevenue.module.css +119 -0
- package/components/ui/DailyRevenue/DailyRevenue.tsx +70 -0
- package/components/ui/DailyRevenue/index.ts +2 -0
- package/components/ui/EmergencyFunds/EmergencyFunds.module.css +123 -0
- package/components/ui/EmergencyFunds/EmergencyFunds.tsx +79 -0
- package/components/ui/EmergencyFunds/index.ts +2 -0
- package/components/ui/EmptyState/EmptyState.module.css +1 -2
- package/components/ui/ExpensesChart/ExpensesChart.module.css +143 -0
- package/components/ui/ExpensesChart/ExpensesChart.tsx +82 -0
- package/components/ui/ExpensesChart/index.ts +2 -0
- package/components/ui/FinancialGrowth/FinancialGrowth.module.css +152 -0
- package/components/ui/FinancialGrowth/FinancialGrowth.tsx +81 -0
- package/components/ui/FinancialGrowth/index.ts +2 -0
- package/components/ui/GoalProgress/GoalProgress.module.css +144 -0
- package/components/ui/GoalProgress/GoalProgress.tsx +78 -0
- package/components/ui/GoalProgress/index.ts +2 -0
- package/components/ui/IncomeBreakdown/IncomeBreakdown.module.css +101 -0
- package/components/ui/IncomeBreakdown/IncomeBreakdown.tsx +62 -0
- package/components/ui/IncomeBreakdown/index.ts +2 -0
- package/components/ui/InvestmentChart/InvestmentChart.module.css +105 -0
- package/components/ui/InvestmentChart/InvestmentChart.tsx +71 -0
- package/components/ui/InvestmentChart/index.ts +2 -0
- package/components/ui/LanguageSelector/LanguageSelector.module.css +238 -0
- package/components/ui/LanguageSelector/LanguageSelector.tsx +261 -0
- package/components/ui/LanguageSelector/index.ts +7 -0
- package/components/ui/LanguageSelector/languageAbbrev.ts +18 -0
- package/components/ui/LanguageSelector/types.ts +9 -0
- package/components/ui/MarketingHeader/MarketingHeader.module.css +637 -0
- package/components/ui/MarketingHeader/MarketingHeader.tsx +531 -0
- package/components/ui/MarketingHeader/index.ts +8 -0
- package/components/ui/Metric/Metric.module.css +3 -6
- package/components/ui/Modal/Modal.module.css +5 -3
- package/components/ui/MonthlySubscription/MonthlySubscription.module.css +145 -0
- package/components/ui/MonthlySubscription/MonthlySubscription.tsx +92 -0
- package/components/ui/MonthlySubscription/index.ts +2 -0
- package/components/ui/MyCard/MyCard.module.css +152 -0
- package/components/ui/MyCard/MyCard.tsx +77 -0
- package/components/ui/MyCard/index.ts +2 -0
- package/components/ui/PageHeader/PageHeader.module.css +1 -2
- package/components/ui/Rating/Rating.module.css +0 -1
- package/components/ui/SavedMoney/SavedMoney.module.css +151 -0
- package/components/ui/SavedMoney/SavedMoney.tsx +92 -0
- package/components/ui/SavedMoney/index.ts +2 -0
- package/components/ui/SavingAccount/SavingAccount.module.css +227 -0
- package/components/ui/SavingAccount/SavingAccount.tsx +121 -0
- package/components/ui/SavingAccount/index.ts +2 -0
- package/components/ui/SavingsBuckets/SavingsBuckets.module.css +117 -0
- package/components/ui/SavingsBuckets/SavingsBuckets.tsx +74 -0
- package/components/ui/SavingsBuckets/index.ts +2 -0
- package/components/ui/SavingsGoals/SavingsGoals.module.css +176 -0
- package/components/ui/SavingsGoals/SavingsGoals.tsx +88 -0
- package/components/ui/SavingsGoals/index.ts +2 -0
- package/components/ui/SavingsMonthly/SavingsMonthly.module.css +227 -0
- package/components/ui/SavingsMonthly/SavingsMonthly.tsx +171 -0
- package/components/ui/SavingsMonthly/index.ts +2 -0
- package/components/ui/SendMoney/SendMoney.module.css +224 -0
- package/components/ui/SendMoney/SendMoney.tsx +98 -0
- package/components/ui/SendMoney/index.ts +2 -0
- package/components/ui/SendMoneyCompact/SendMoneyCompact.module.css +147 -0
- package/components/ui/SendMoneyCompact/SendMoneyCompact.tsx +64 -0
- package/components/ui/SendMoneyCompact/index.ts +2 -0
- package/components/ui/SocialButton/SocialButton.module.css +0 -1
- package/components/ui/SpendingLimit/SpendingLimit.module.css +124 -0
- package/components/ui/SpendingLimit/SpendingLimit.tsx +73 -0
- package/components/ui/SpendingLimit/index.ts +2 -0
- package/components/ui/SpendsBreakdown/SpendsBreakdown.module.css +180 -0
- package/components/ui/SpendsBreakdown/SpendsBreakdown.tsx +107 -0
- package/components/ui/SpendsBreakdown/index.ts +2 -0
- package/components/ui/StockPosition/StockPosition.module.css +150 -0
- package/components/ui/StockPosition/StockPosition.tsx +84 -0
- package/components/ui/StockPosition/index.ts +2 -0
- package/components/ui/Table/Table.module.css +0 -1
- package/components/ui/TotalBalance/TotalBalance.module.css +112 -0
- package/components/ui/TotalBalance/TotalBalance.tsx +105 -0
- package/components/ui/TotalBalance/index.ts +2 -0
- package/components/ui/WeeklyExpenditure/WeeklyExpenditure.module.css +101 -0
- package/components/ui/WeeklyExpenditure/WeeklyExpenditure.tsx +63 -0
- package/components/ui/WeeklyExpenditure/index.ts +2 -0
- 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(
|
|
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));
|
package/cli/omnira-init.mjs
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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-
|
|
314
|
+
background: var(--color-bg-overlay);
|
|
317
315
|
border: 1px solid var(--color-border-standard);
|
|
318
|
-
box-shadow:
|
|
319
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
312
|
-
|
|
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
|
+
}
|