indas-ui 0.1.3 → 0.1.4

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 (59) hide show
  1. package/dist/cjs/components/chat/ai-messages.js +3 -2
  2. package/dist/cjs/components/chat/chat.js +2 -1
  3. package/dist/cjs/components/dashboard/charts/Area.js +6 -4
  4. package/dist/cjs/components/dashboard/charts/Bar.js +6 -4
  5. package/dist/cjs/components/dashboard/charts/FlowMap.js +2 -1
  6. package/dist/cjs/components/dashboard/charts/Funnel.js +2 -1
  7. package/dist/cjs/components/dashboard/charts/GeoMap.js +2 -1
  8. package/dist/cjs/components/dashboard/charts/Line.js +5 -3
  9. package/dist/cjs/components/dashboard/charts/Pareto.js +2 -1
  10. package/dist/cjs/components/dashboard/charts/Sankey.js +4 -3
  11. package/dist/cjs/components/dashboard/charts/Sparkline.js +2 -1
  12. package/dist/cjs/components/dashboard/charts/Treemap.js +2 -1
  13. package/dist/cjs/components/dashboard/charts/index.js +7 -1
  14. package/dist/cjs/components/dashboard/kpi/KpiBreakdown.js +3 -2
  15. package/dist/cjs/components/dashboard/kpi/KpiComparison.js +2 -1
  16. package/dist/cjs/components/dashboard/kpi/KpiQuickStats.js +2 -1
  17. package/dist/cjs/components/dashboard/kpi/KpiRating.js +3 -2
  18. package/dist/cjs/components/datagrid/columns/ColumnContextMenu.js +2 -1
  19. package/dist/cjs/components/datagrid/controls/menu/view/CardView.js +2 -1
  20. package/dist/cjs/components/datagrid/controls/menu/view/ChartView.js +2 -1
  21. package/dist/cjs/components/datagrid/utils/grid-helpers.js +6 -5
  22. package/dist/cjs/components/layout/footer.js +2 -1
  23. package/dist/cjs/components/layout/side-panel.js +4 -1
  24. package/dist/cjs/components/ui/display/stats-card.js +2 -1
  25. package/dist/cjs/lib/number-format.js +60 -0
  26. package/dist/cjs/lib/utils.js +4 -3
  27. package/dist/components/chat/ai-messages.js +3 -2
  28. package/dist/components/chat/chat.js +2 -1
  29. package/dist/components/dashboard/charts/Area.d.ts +5 -1
  30. package/dist/components/dashboard/charts/Area.js +6 -4
  31. package/dist/components/dashboard/charts/Bar.d.ts +5 -1
  32. package/dist/components/dashboard/charts/Bar.js +6 -4
  33. package/dist/components/dashboard/charts/FlowMap.js +2 -1
  34. package/dist/components/dashboard/charts/Funnel.js +2 -1
  35. package/dist/components/dashboard/charts/GeoMap.js +2 -1
  36. package/dist/components/dashboard/charts/Line.d.ts +5 -1
  37. package/dist/components/dashboard/charts/Line.js +5 -3
  38. package/dist/components/dashboard/charts/Pareto.js +2 -1
  39. package/dist/components/dashboard/charts/Sankey.js +4 -3
  40. package/dist/components/dashboard/charts/Sparkline.js +2 -1
  41. package/dist/components/dashboard/charts/Treemap.js +2 -1
  42. package/dist/components/dashboard/charts/index.d.ts +1 -0
  43. package/dist/components/dashboard/charts/index.js +2 -0
  44. package/dist/components/dashboard/kpi/KpiBreakdown.js +3 -2
  45. package/dist/components/dashboard/kpi/KpiComparison.js +2 -1
  46. package/dist/components/dashboard/kpi/KpiQuickStats.js +2 -1
  47. package/dist/components/dashboard/kpi/KpiRating.js +3 -2
  48. package/dist/components/datagrid/columns/ColumnContextMenu.js +2 -1
  49. package/dist/components/datagrid/controls/menu/view/CardView.js +2 -1
  50. package/dist/components/datagrid/controls/menu/view/ChartView.js +2 -1
  51. package/dist/components/datagrid/utils/grid-helpers.d.ts +1 -1
  52. package/dist/components/datagrid/utils/grid-helpers.js +3 -2
  53. package/dist/components/layout/footer.js +2 -1
  54. package/dist/components/layout/side-panel.js +4 -1
  55. package/dist/components/ui/display/stats-card.js +2 -1
  56. package/dist/lib/number-format.d.ts +18 -0
  57. package/dist/lib/number-format.js +54 -0
  58. package/dist/lib/utils.js +4 -3
  59. package/package.json +4 -4
@@ -6,6 +6,7 @@ exports.CompactFooter = CompactFooter;
6
6
  const jsx_runtime_1 = require("react/jsx-runtime");
7
7
  const utils_1 = require("../../lib/utils.js");
8
8
  const useTranslation_1 = require("../../hooks/useTranslation.js");
9
+ const number_format_1 = require("../../lib/number-format.js");
9
10
  function Footer({ kpis, actions, iconButtons, leftContent, className, sticky = false, compact = false, padding, variant = 'page', gradient = false }) {
10
11
  if (!kpis && !actions && !leftContent && !iconButtons) {
11
12
  return null;
@@ -45,7 +46,7 @@ function Footer({ kpis, actions, iconButtons, leftContent, className, sticky = f
45
46
  padding === 'normal' ? "py-3 sm:py-4" :
46
47
  compact ? "py-1" :
47
48
  isModal ? "py-1.5 sm:py-2" : "py-3 sm:py-4"), children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex flex-wrap items-center gap-2 sm:gap-4 w-full sm:w-auto", children: [leftContent, kpis && kpis.length > 0 && ((0, jsx_runtime_1.jsx)("div", { className: "flex flex-wrap items-center gap-3 sm:gap-6 md:gap-8", children: kpis.map((kpi) => ((0, jsx_runtime_1.jsxs)("div", { className: (0, utils_1.cn)("flex items-center gap-1.5 sm:gap-2 text-xs sm:text-sm", kpi.onClick && "cursor-pointer hover:text-[rgb(var(--color-primary-hover))] transition-colors"), onClick: kpi.onClick, children: [(0, jsx_runtime_1.jsxs)("span", { className: "text-[rgb(var(--fg-muted))] whitespace-nowrap", children: [kpi.label, ":"] }), (0, jsx_runtime_1.jsx)("span", { className: "font-medium text-[rgb(var(--fg-default))]", children: typeof kpi.value === 'number'
48
- ? kpi.value.toLocaleString()
49
+ ? (0, number_format_1.formatNumber)(kpi.value)
49
50
  : kpi.value }), kpi.trend && ((0, jsx_runtime_1.jsxs)("span", { className: (0, utils_1.cn)("text-xs font-medium", kpi.trend.direction === 'up'
50
51
  ? "text-[rgb(var(--color-success))]"
51
52
  : "text-[rgb(var(--color-error))]"), children: [kpi.trend.direction === 'up' ? '↑' : '↓', Math.abs(kpi.trend.value), "%"] }))] }, kpi.id))) }))] }), iconButtons && iconButtons.length > 0 && ((0, jsx_runtime_1.jsx)("div", { className: "flex flex-wrap items-center gap-1.5 sm:gap-2", children: iconButtons.map((button) => {
@@ -110,7 +110,10 @@ function SidePanel({ title, icon: Icon, categories, activeItemId, onItemSelect,
110
110
  return (0, jsx_runtime_1.jsx)(ItemIcon, { className: "w-4 h-4 shrink-0 opacity-50" });
111
111
  })(), (0, jsx_runtime_1.jsx)("span", { className: "truncate flex-1", children: item.label }), item.badge !== undefined && item.badge !== 0 && ((0, jsx_runtime_1.jsx)("span", { className: (0, utils_1.cn)("text-xs px-1.5 py-0.5 rounded-full shrink-0 tabular-nums", activeItemId === item.id
112
112
  ? "bg-[rgb(var(--color-primary))] text-white"
113
- : "bg-[rgb(var(--bg-subtle))] text-[rgb(var(--fg-muted))]"), children: item.badge }))] }), itemContextMenu && ((0, jsx_runtime_1.jsx)("button", { onClick: (e) => handleMenuToggle(e, item.id), className: "absolute right-1.5 top-1/2 -translate-y-1/2 w-6 h-6 rounded-md text-[rgb(var(--fg-muted))]/50 flex items-center justify-center hover:text-[rgb(var(--fg-default))] hover:bg-[rgb(var(--bg-subtle))] transition-all opacity-0 group-hover:opacity-100", children: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreVertical, { className: "w-3.5 h-3.5" }) }))] }, item.id))) }))] }, category.id))))] })) }), footer && !isCollapsed && ((0, jsx_runtime_1.jsx)("div", { className: (0, utils_1.cn)("shrink-0 border-t border-[rgb(var(--bd-default))] transition-opacity duration-200", isAnimating ? "opacity-0" : "opacity-100"), children: footer }))] }), menuOpen !== null && itemContextMenu && ((0, jsx_runtime_1.jsx)("div", { ref: menuRef, className: "fixed bg-[rgb(var(--bg-surface))] border border-[rgb(var(--bd-default))] rounded-lg min-w-[170px] shadow-lg overflow-hidden z-[9999]", style: {
113
+ : "bg-[rgb(var(--bg-subtle))] text-[rgb(var(--fg-muted))]"), children: item.badge }))] }), itemContextMenu && (
114
+ // Named for screen readers, and shown on keyboard focus as well as hover:
115
+ // it was an unlabelled button nobody tabbing could see.
116
+ (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: (e) => handleMenuToggle(e, item.id), "aria-label": `More actions for ${item.label}`, "aria-haspopup": "menu", "aria-expanded": menuOpen === item.id, className: "absolute right-1.5 top-1/2 -translate-y-1/2 w-6 h-6 rounded-md text-[rgb(var(--fg-muted))]/50 flex items-center justify-center hover:text-[rgb(var(--fg-default))] hover:bg-[rgb(var(--bg-subtle))] transition-all opacity-0 group-hover:opacity-100 focus-visible:opacity-100", children: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreVertical, { className: "w-3.5 h-3.5" }) }))] }, item.id))) }))] }, category.id))))] })) }), footer && !isCollapsed && ((0, jsx_runtime_1.jsx)("div", { className: (0, utils_1.cn)("shrink-0 border-t border-[rgb(var(--bd-default))] transition-opacity duration-200", isAnimating ? "opacity-0" : "opacity-100"), children: footer }))] }), menuOpen !== null && itemContextMenu && ((0, jsx_runtime_1.jsx)("div", { ref: menuRef, className: "fixed bg-[rgb(var(--bg-surface))] border border-[rgb(var(--bd-default))] rounded-lg min-w-[170px] shadow-lg overflow-hidden z-[9999]", style: {
114
117
  left: `${menuPosition.left}px`,
115
118
  top: `${menuPosition.top}px`,
116
119
  transform: 'translateY(-50%)'
@@ -7,6 +7,7 @@ const jsx_runtime_1 = require("react/jsx-runtime");
7
7
  const lucide_react_1 = require("lucide-react");
8
8
  const utils_1 = require("../../../lib/utils.js");
9
9
  const card_1 = require("./card.js");
10
+ const number_format_1 = require("../../../lib/number-format.js");
10
11
  const iconVariants = {
11
12
  default: "bg-primary text-white",
12
13
  accent: "bg-accent text-white",
@@ -16,7 +17,7 @@ const iconVariants = {
16
17
  function StatsCard({ title, value, icon: Icon, trend, description, className, variant = 'default' }) {
17
18
  const TrendIcon = trend?.direction === 'up' ? lucide_react_1.TrendingUp : lucide_react_1.TrendingDown;
18
19
  const trendColor = trend?.direction === 'up' ? 'text-success' : 'text-error';
19
- return ((0, jsx_runtime_1.jsx)(card_1.Card, { className: (0, utils_1.cn)("p-6 min-h-[140px] w-full", className), children: (0, jsx_runtime_1.jsxs)("div", { className: "flex items-start gap-4", children: [Icon && ((0, jsx_runtime_1.jsx)("div", { className: (0, utils_1.cn)("flex-shrink-0 p-3 rounded-lg", iconVariants[variant]), children: (0, jsx_runtime_1.jsx)(Icon, { className: "h-6 w-6" }) })), (0, jsx_runtime_1.jsxs)("div", { className: "flex-1 min-w-0", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-baseline flex-wrap gap-2", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-2xl font-semibold text-fg-default truncate", children: typeof value === 'number' ? value.toLocaleString() : value }), trend && ((0, jsx_runtime_1.jsxs)("div", { className: (0, utils_1.cn)("flex items-center text-sm font-semibold", trendColor), children: [(0, jsx_runtime_1.jsx)(TrendIcon, { className: "h-4 w-4 flex-shrink-0" }), (0, jsx_runtime_1.jsxs)("span", { className: "ml-1", children: [Math.abs(trend.value), "%", trend.label && ` ${trend.label}`] })] }))] }), (0, jsx_runtime_1.jsx)("p", { className: "text-sm font-medium text-fg-muted mt-2 line-clamp-2", children: title }), description && ((0, jsx_runtime_1.jsx)("p", { className: "text-sm text-fg-subtle mt-1 line-clamp-1", children: description }))] })] }) }));
20
+ return ((0, jsx_runtime_1.jsx)(card_1.Card, { className: (0, utils_1.cn)("p-6 min-h-[140px] w-full", className), children: (0, jsx_runtime_1.jsxs)("div", { className: "flex items-start gap-4", children: [Icon && ((0, jsx_runtime_1.jsx)("div", { className: (0, utils_1.cn)("flex-shrink-0 p-3 rounded-lg", iconVariants[variant]), children: (0, jsx_runtime_1.jsx)(Icon, { className: "h-6 w-6" }) })), (0, jsx_runtime_1.jsxs)("div", { className: "flex-1 min-w-0", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-baseline flex-wrap gap-2", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-2xl font-semibold text-fg-default truncate", children: typeof value === 'number' ? (0, number_format_1.formatNumber)(value) : value }), trend && ((0, jsx_runtime_1.jsxs)("div", { className: (0, utils_1.cn)("flex items-center text-sm font-semibold", trendColor), children: [(0, jsx_runtime_1.jsx)(TrendIcon, { className: "h-4 w-4 flex-shrink-0" }), (0, jsx_runtime_1.jsxs)("span", { className: "ml-1", children: [Math.abs(trend.value), "%", trend.label && ` ${trend.label}`] })] }))] }), (0, jsx_runtime_1.jsx)("p", { className: "text-sm font-medium text-fg-muted mt-2 line-clamp-2", children: title }), description && ((0, jsx_runtime_1.jsx)("p", { className: "text-sm text-fg-subtle mt-1 line-clamp-1", children: description }))] })] }) }));
20
21
  }
21
22
  function StatsGrid({ children, columns = 4, className }) {
22
23
  const gridCols = {
@@ -0,0 +1,60 @@
1
+ 'use client';
2
+ "use strict";
3
+ /**
4
+ * How every number in the library is written. Indian grouping by default (20,00,000, not
5
+ * 2,000,000): the products built on this library keep Indian books, and a bare `toLocaleString()`
6
+ * follows whatever locale the browser happens to be set to, so the same figure read differently on
7
+ * two machines.
8
+ *
9
+ * A product that needs another convention calls `setNumberLocale()` once at start-up.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.setNumberLocale = setNumberLocale;
13
+ exports.getNumberLocale = getNumberLocale;
14
+ exports.formatNumber = formatNumber;
15
+ exports.formatCompactNumber = formatCompactNumber;
16
+ let numberLocale = 'en-IN';
17
+ function setNumberLocale(locale) {
18
+ numberLocale = locale;
19
+ }
20
+ function getNumberLocale() {
21
+ return numberLocale;
22
+ }
23
+ /** A full number with grouping, up to two decimals. Empty for null, undefined or NaN. */
24
+ function formatNumber(value, maximumFractionDigits = 2) {
25
+ if (value == null || value === '')
26
+ return '';
27
+ const n = typeof value === 'number' ? value : Number(value);
28
+ if (!Number.isFinite(n))
29
+ return typeof value === 'string' ? value : '';
30
+ return n.toLocaleString(numberLocale, { maximumFractionDigits });
31
+ }
32
+ const trim = (n) => n.toFixed(n >= 100 ? 0 : n >= 10 ? 1 : 2).replace(/\.?0+$/, '');
33
+ /**
34
+ * A short number for chart axes, where a full figure runs into its neighbour: 12,500 · 20 L ·
35
+ * 1.4 Cr in Indian grouping; 12.5K · 2M · 1.4B otherwise. Below a lakh (or a thousand) it is the
36
+ * full number.
37
+ */
38
+ function formatCompactNumber(value) {
39
+ if (value == null || value === '')
40
+ return '';
41
+ const n = typeof value === 'number' ? value : Number(value);
42
+ if (!Number.isFinite(n))
43
+ return String(value);
44
+ const sign = n < 0 ? '-' : '';
45
+ const a = Math.abs(n);
46
+ if (numberLocale === 'en-IN') {
47
+ if (a >= 1e7)
48
+ return `${sign}${trim(a / 1e7)} Cr`;
49
+ if (a >= 1e5)
50
+ return `${sign}${trim(a / 1e5)} L`;
51
+ return formatNumber(n, 0);
52
+ }
53
+ if (a >= 1e9)
54
+ return `${sign}${trim(a / 1e9)}B`;
55
+ if (a >= 1e6)
56
+ return `${sign}${trim(a / 1e6)}M`;
57
+ if (a >= 1e3)
58
+ return `${sign}${trim(a / 1e3)}K`;
59
+ return formatNumber(n, 0);
60
+ }
@@ -11,11 +11,12 @@ exports.getModuleRoutePath = getModuleRoutePath;
11
11
  exports.naturalCompare = naturalCompare;
12
12
  const clsx_1 = require("clsx");
13
13
  const tailwind_merge_1 = require("tailwind-merge");
14
+ const number_format_1 = require("./number-format.js");
14
15
  function cn(...inputs) {
15
16
  return (0, tailwind_merge_1.twMerge)((0, clsx_1.clsx)(inputs));
16
17
  }
17
- function formatCurrency(amount, currency = 'USD') {
18
- return new Intl.NumberFormat('en-US', {
18
+ function formatCurrency(amount, currency = 'INR') {
19
+ return new Intl.NumberFormat((0, number_format_1.getNumberLocale)(), {
19
20
  style: 'currency',
20
21
  currency,
21
22
  }).format(amount);
@@ -28,7 +29,7 @@ function formatDate(date) {
28
29
  }).format(new Date(date));
29
30
  }
30
31
  function formatNumber(num, decimals = 0) {
31
- return new Intl.NumberFormat('en-US', {
32
+ return new Intl.NumberFormat((0, number_format_1.getNumberLocale)(), {
32
33
  minimumFractionDigits: decimals,
33
34
  maximumFractionDigits: decimals,
34
35
  }).format(num);
@@ -10,6 +10,7 @@ import { cn } from '../../lib/utils.js';
10
10
  import { User, Copy, Check, Download, Share2, Pencil, Bot, RotateCcw, ThumbsUp, ThumbsDown } from 'lucide-react';
11
11
  import ReactMarkdown from 'react-markdown';
12
12
  import remarkGfm from 'remark-gfm';
13
+ import { formatNumber } from '../../lib/number-format.js';
13
14
  // Individual Message Bubble Component
14
15
  function Message({ message, onSelectItem, onEditMessage, onRegenerateAi, onFeedback, botAvatar, renderBotContent }) {
15
16
  const isUser = message.role === 'user';
@@ -158,7 +159,7 @@ function Message({ message, onSelectItem, onEditMessage, onRegenerateAi, onFeedb
158
159
  return full;
159
160
  });
160
161
  return processed;
161
- })() }) })) })) })) }), !isEditing && (_jsxs("div", { className: cn('flex items-center gap-2 mt-1 opacity-0 group-hover:opacity-100 transition-opacity duration-200', isUser ? 'justify-end' : 'justify-start'), children: [formattedTimestamp && (_jsx("span", { className: "text-[0.7rem] text-[rgb(var(--fg-muted))]/70 select-none", children: formattedTimestamp })), isUser && onEditMessage && (_jsx("button", { onClick: handleRegenerate, className: "w-7 h-7 rounded-md flex items-center justify-center text-[rgb(var(--fg-muted))] hover:text-[rgb(var(--fg-default))] hover:bg-[rgb(var(--bg-subtle))] transition-colors", "aria-label": "Regenerate response", title: "Regenerate", children: _jsx(RotateCcw, { className: "w-3.5 h-3.5" }) })), isUser && onEditMessage && (_jsx("button", { onClick: handleStartEdit, className: "w-7 h-7 rounded-md flex items-center justify-center text-[rgb(var(--fg-muted))] hover:text-[rgb(var(--fg-default))] hover:bg-[rgb(var(--bg-subtle))] transition-colors", "aria-label": "Edit message", title: "Edit", children: _jsx(Pencil, { className: "w-3.5 h-3.5" }) })), _jsx("button", { onClick: handleCopy, className: "w-7 h-7 rounded-md flex items-center justify-center text-[rgb(var(--fg-muted))] hover:text-[rgb(var(--fg-default))] hover:bg-[rgb(var(--bg-subtle))] transition-colors", "aria-label": "Copy message", title: "Copy", children: copied ? (_jsx(Check, { className: "w-3.5 h-3.5" })) : (_jsx(Copy, { className: "w-3.5 h-3.5" })) }), !isUser && onRegenerateAi && (_jsx("button", { onClick: handleRegenerateAi, className: "w-7 h-7 rounded-md flex items-center justify-center text-[rgb(var(--fg-muted))] hover:text-[rgb(var(--fg-default))] hover:bg-[rgb(var(--bg-subtle))] transition-colors", "aria-label": "Regenerate response", title: "Regenerate", children: _jsx(RotateCcw, { className: "w-3.5 h-3.5" }) })), !isUser && (_jsxs(_Fragment, { children: [_jsx("button", { onClick: () => handleFeedback('up'), className: "w-7 h-7 rounded-md flex items-center justify-center text-[rgb(var(--fg-muted))] hover:text-[rgb(var(--fg-default))] hover:bg-[rgb(var(--bg-subtle))] transition-colors", "aria-label": "Good response", title: "Good response", children: _jsx(ThumbsUp, { className: "w-3.5 h-3.5", fill: feedback === 'up' ? 'currentColor' : 'none' }) }), _jsx("button", { onClick: () => handleFeedback('down'), className: "w-7 h-7 rounded-md flex items-center justify-center text-[rgb(var(--fg-muted))] hover:text-[rgb(var(--fg-default))] hover:bg-[rgb(var(--bg-subtle))] transition-colors", "aria-label": "Bad response", title: "Bad response", children: _jsx(ThumbsDown, { className: "w-3.5 h-3.5", fill: feedback === 'down' ? 'currentColor' : 'none' }) })] })), !isUser && message.tokens && process.env.NEXT_PUBLIC_SHOW_TOKENS === 'true' && (_jsxs("span", { className: "text-[0.65rem] text-[rgb(var(--fg-muted))]/70 font-mono select-none ml-1", children: ["P: ", message.tokens.prompt.toLocaleString(), " R: ", message.tokens.completion.toLocaleString(), " T: ", message.tokens.total.toLocaleString()] }))] }))] })] }));
162
+ })() }) })) })) })) }), !isEditing && (_jsxs("div", { className: cn('flex items-center gap-2 mt-1 opacity-0 group-hover:opacity-100 transition-opacity duration-200', isUser ? 'justify-end' : 'justify-start'), children: [formattedTimestamp && (_jsx("span", { className: "text-[0.7rem] text-[rgb(var(--fg-muted))]/70 select-none", children: formattedTimestamp })), isUser && onEditMessage && (_jsx("button", { onClick: handleRegenerate, className: "w-7 h-7 rounded-md flex items-center justify-center text-[rgb(var(--fg-muted))] hover:text-[rgb(var(--fg-default))] hover:bg-[rgb(var(--bg-subtle))] transition-colors", "aria-label": "Regenerate response", title: "Regenerate", children: _jsx(RotateCcw, { className: "w-3.5 h-3.5" }) })), isUser && onEditMessage && (_jsx("button", { onClick: handleStartEdit, className: "w-7 h-7 rounded-md flex items-center justify-center text-[rgb(var(--fg-muted))] hover:text-[rgb(var(--fg-default))] hover:bg-[rgb(var(--bg-subtle))] transition-colors", "aria-label": "Edit message", title: "Edit", children: _jsx(Pencil, { className: "w-3.5 h-3.5" }) })), _jsx("button", { onClick: handleCopy, className: "w-7 h-7 rounded-md flex items-center justify-center text-[rgb(var(--fg-muted))] hover:text-[rgb(var(--fg-default))] hover:bg-[rgb(var(--bg-subtle))] transition-colors", "aria-label": "Copy message", title: "Copy", children: copied ? (_jsx(Check, { className: "w-3.5 h-3.5" })) : (_jsx(Copy, { className: "w-3.5 h-3.5" })) }), !isUser && onRegenerateAi && (_jsx("button", { onClick: handleRegenerateAi, className: "w-7 h-7 rounded-md flex items-center justify-center text-[rgb(var(--fg-muted))] hover:text-[rgb(var(--fg-default))] hover:bg-[rgb(var(--bg-subtle))] transition-colors", "aria-label": "Regenerate response", title: "Regenerate", children: _jsx(RotateCcw, { className: "w-3.5 h-3.5" }) })), !isUser && (_jsxs(_Fragment, { children: [_jsx("button", { onClick: () => handleFeedback('up'), className: "w-7 h-7 rounded-md flex items-center justify-center text-[rgb(var(--fg-muted))] hover:text-[rgb(var(--fg-default))] hover:bg-[rgb(var(--bg-subtle))] transition-colors", "aria-label": "Good response", title: "Good response", children: _jsx(ThumbsUp, { className: "w-3.5 h-3.5", fill: feedback === 'up' ? 'currentColor' : 'none' }) }), _jsx("button", { onClick: () => handleFeedback('down'), className: "w-7 h-7 rounded-md flex items-center justify-center text-[rgb(var(--fg-muted))] hover:text-[rgb(var(--fg-default))] hover:bg-[rgb(var(--bg-subtle))] transition-colors", "aria-label": "Bad response", title: "Bad response", children: _jsx(ThumbsDown, { className: "w-3.5 h-3.5", fill: feedback === 'down' ? 'currentColor' : 'none' }) })] })), !isUser && message.tokens && process.env.NEXT_PUBLIC_SHOW_TOKENS === 'true' && (_jsxs("span", { className: "text-[0.65rem] text-[rgb(var(--fg-muted))]/70 font-mono select-none ml-1", children: ["P: ", formatNumber(message.tokens.prompt), " R: ", formatNumber(message.tokens.completion), " T: ", formatNumber(message.tokens.total)] }))] }))] })] }));
162
163
  }
163
164
  // Synthia thinking phrases — the fun ones
164
165
  const SYNTHIA_THINKING = [
@@ -260,6 +261,6 @@ export function Messages({ messages, isLoading = false, onExport, onShare, onSel
260
261
  }, { prompt: 0, completion: 0, total: 0 });
261
262
  if (totals.total === 0)
262
263
  return _jsx("div", {});
263
- return (_jsxs("span", { className: "text-[0.65rem] text-[rgb(var(--fg-muted))] font-mono", children: ["P:", totals.prompt.toLocaleString(), " \u00B7 C:", totals.completion.toLocaleString(), " \u00B7 T:", totals.total.toLocaleString(), " \u00B7 ", messages.filter(m => m.tokens).length, " calls"] }));
264
+ return (_jsxs("span", { className: "text-[0.65rem] text-[rgb(var(--fg-muted))] font-mono", children: ["P:", formatNumber(totals.prompt), " \u00B7 C:", formatNumber(totals.completion), " \u00B7 T:", formatNumber(totals.total), " \u00B7 ", messages.filter(m => m.tokens).length, " calls"] }));
264
265
  })() : _jsx("div", {}), _jsxs("div", { className: "flex gap-2", children: [onExport && (_jsxs("button", { onClick: onExport, className: "flex items-center gap-2 px-3 py-1.5 text-sm text-[rgb(var(--fg-muted))] hover:text-[rgb(var(--fg-default))] hover:bg-[rgb(var(--bg-subtle))] rounded-lg transition-colors", children: [_jsx(Download, { className: "w-4 h-4" }), "Export"] })), onShare && (_jsxs("button", { onClick: onShare, className: "flex items-center gap-2 px-3 py-1.5 text-sm text-[rgb(var(--fg-muted))] hover:text-[rgb(var(--fg-default))] hover:bg-[rgb(var(--bg-subtle))] rounded-lg transition-colors", children: [_jsx(Share2, { className: "w-4 h-4" }), "Share"] }))] })] })), _jsx("div", { className: "flex-1 overflow-y-auto", children: _jsxs("div", { className: "max-w-5xl mx-auto px-3 sm:px-4 py-6 space-y-6 animate-[fadeInUp_0.28s_ease-out]", children: [messages.map((message) => (_jsx(Message, { message: message, onSelectItem: onSelectItem, onEditMessage: onEditMessage, onRegenerateAi: onRegenerateAi, onFeedback: onFeedback, botAvatar: botAvatar, renderBotContent: renderBotContent }, message.id))), isLoading && _jsx(TypingIndicator, { lastUserMessage: messages.filter(m => m.role === 'user').at(-1)?.content }), _jsx("div", { ref: bottomRef })] }, fadeKey) })] }));
265
266
  }
@@ -18,6 +18,7 @@ import { MultimodalInput } from './multimodal-input.js';
18
18
  import { ChatPromptNavigator } from './chat-prompt-navigator.js';
19
19
  import { DatePicker } from '../forms/date-picker/index.js';
20
20
  import { getLocalDateString } from '../../lib/utils.js';
21
+ import { formatNumber } from '../../lib/number-format.js';
21
22
  function buildSendPayload(ctx, message, autoTitle) {
22
23
  if (ctx.mode === 'new') {
23
24
  return { message, conversationId: undefined, newChat: true, title: autoTitle };
@@ -1019,7 +1020,7 @@ export function Chat({ mode = 'fullpage', externalMuted, title = 'Chat', sidebar
1019
1020
  }, { prompt: 0, completion: 0, total: 0 });
1020
1021
  if (totals.total === 0)
1021
1022
  return null;
1022
- return (_jsxs("span", { className: "text-[0.65rem] text-[rgb(var(--fg-muted))] font-mono", children: ["Prompt: ", totals.prompt.toLocaleString(), " \u00B7 Response: ", totals.completion.toLocaleString(), " \u00B7 Total: ", totals.total.toLocaleString(), " tokens \u00B7 ", messages.filter(m => m.tokens).length, " calls"] }));
1023
+ return (_jsxs("span", { className: "text-[0.65rem] text-[rgb(var(--fg-muted))] font-mono", children: ["Prompt: ", formatNumber(totals.prompt), " \u00B7 Response: ", formatNumber(totals.completion), " \u00B7 Total: ", formatNumber(totals.total), " tokens \u00B7 ", messages.filter(m => m.tokens).length, " calls"] }));
1023
1024
  })(), _jsxs("div", { className: "absolute right-3 flex items-center gap-0.5", children: [headerActions, _jsx("button", { onClick: () => setIsMuted(!isMuted), className: "w-7 h-7 flex items-center justify-center text-[rgb(var(--fg-muted))] hover:text-[rgb(var(--fg-default))] hover:bg-[rgb(var(--bg-subtle))] rounded-md transition-colors", title: isMuted ? "Unmute sounds" : "Mute sounds", children: isMuted ? _jsx(VolumeX, { className: "w-3.5 h-3.5" }) : _jsx(Volume2, { className: "w-3.5 h-3.5" }) }), messages.length > 0 && (_jsxs(_Fragment, { children: [_jsx("button", { onClick: handleExport, className: "w-7 h-7 flex items-center justify-center text-[rgb(var(--fg-muted))] hover:text-[rgb(var(--fg-default))] hover:bg-[rgb(var(--bg-subtle))] rounded-md transition-colors", title: t('Export'), children: _jsx(Download, { className: "w-3.5 h-3.5" }) }), _jsx("button", { onClick: handleShare, className: "w-7 h-7 flex items-center justify-center text-[rgb(var(--fg-muted))] hover:text-[rgb(var(--fg-default))] hover:bg-[rgb(var(--bg-subtle))] rounded-md transition-colors", title: t('Share'), children: _jsx(Share2, { className: "w-3.5 h-3.5" }) })] })), fullpagePath && mode === 'modal' && (_jsx("button", { onClick: () => {
1024
1025
  const target = currentConversationId
1025
1026
  ? `${fullpagePath}?c=${currentConversationId}`
@@ -20,9 +20,13 @@ export interface AreaChartProps {
20
20
  showLegend?: boolean;
21
21
  showGrid?: boolean;
22
22
  smooth?: boolean;
23
+ /** How a value reads in the tooltip. Default: the full number, Indian grouping (20,00,000). */
24
+ valueFormatter?: (value: number) => string;
25
+ /** How a value reads on the value axis. Default: compact (12,500 · 20 L · 1.4 Cr). */
26
+ axisFormatter?: (value: number) => string;
23
27
  className?: string;
24
28
  }
25
29
  /**
26
30
  * AreaChart - ECharts Area Chart component
27
31
  */
28
- export declare function AreaChart({ data, xKey, series, height, showLegend, showGrid, smooth, className }: AreaChartProps): import("react").JSX.Element;
32
+ export declare function AreaChart({ data, xKey, series, height, showLegend, showGrid, smooth, valueFormatter, axisFormatter, className }: AreaChartProps): import("react").JSX.Element;
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import ReactECharts from 'echarts-for-react';
4
4
  import { cn } from '../../../lib/utils.js';
5
+ import { formatCompactNumber, formatNumber } from '../../../lib/number-format.js';
5
6
  // Convert any color (hex, rgb(), rgba(), space-separated rgb()) to rgba() with given alpha (0–1).
6
7
  // Hex+alpha-suffix concat (e.g. color + 'cc') breaks for non-hex inputs and crashes canvas gradients.
7
8
  function withAlpha(color, alpha) {
@@ -31,10 +32,11 @@ function withAlpha(color, alpha) {
31
32
  /**
32
33
  * AreaChart - ECharts Area Chart component
33
34
  */
34
- export function AreaChart({ data, xKey, series, height = 300, showLegend = true, showGrid = true, smooth = true, className }) {
35
+ export function AreaChart({ data, xKey, series, height = 300, showLegend = true, showGrid = true, smooth = true, valueFormatter, axisFormatter, className }) {
35
36
  const option = {
36
37
  tooltip: {
37
38
  trigger: 'axis',
39
+ valueFormatter: (v) => (typeof v === 'number' ? (valueFormatter ?? formatNumber)(v) : String(v ?? '')),
38
40
  backgroundColor: 'rgba(255, 255, 255, 0.95)',
39
41
  borderColor: '#e9ecef',
40
42
  borderWidth: 1,
@@ -70,13 +72,13 @@ export function AreaChart({ data, xKey, series, height = 300, showLegend = true,
70
72
  },
71
73
  yAxis: series.some(s => s.secondaryAxis)
72
74
  ? [
73
- { type: 'value', axisLine: { show: false }, axisLabel: { color: '#6c757d', fontSize: 11 }, splitLine: { show: showGrid, lineStyle: { color: '#f0f0f0', type: 'dashed' } } },
74
- { type: 'value', axisLine: { show: false }, axisLabel: { color: '#6c757d', fontSize: 11 }, splitLine: { show: false } },
75
+ { type: 'value', axisLine: { show: false }, axisLabel: { color: '#6c757d', fontSize: 11, formatter: (v) => (axisFormatter ?? formatCompactNumber)(v) }, splitLine: { show: showGrid, lineStyle: { color: '#f0f0f0', type: 'dashed' } } },
76
+ { type: 'value', axisLine: { show: false }, axisLabel: { color: '#6c757d', fontSize: 11, formatter: (v) => (axisFormatter ?? formatCompactNumber)(v) }, splitLine: { show: false } },
75
77
  ]
76
78
  : {
77
79
  type: 'value',
78
80
  axisLine: { show: false },
79
- axisLabel: { color: '#6c757d', fontSize: 11 },
81
+ axisLabel: { color: '#6c757d', fontSize: 11, formatter: (v) => (axisFormatter ?? formatCompactNumber)(v) },
80
82
  splitLine: { show: showGrid, lineStyle: { color: '#f0f0f0', type: 'dashed' } }
81
83
  },
82
84
  series: series.map((s, index) => {
@@ -19,9 +19,13 @@ export interface BarChartProps {
19
19
  * Renders a KPI-vs-target bullet (track + actual fill + target marker + achievement %).
20
20
  */
21
21
  targetKey?: string;
22
+ /** How a value reads in the tooltip. Default: the full number, Indian grouping (20,00,000). */
23
+ valueFormatter?: (value: number) => string;
24
+ /** How a value reads on the value axis. Default: compact (12,500 · 20 L · 1.4 Cr). */
25
+ axisFormatter?: (value: number) => string;
22
26
  className?: string;
23
27
  }
24
28
  /**
25
29
  * BarChart - ECharts Bar Chart component
26
30
  */
27
- export declare function BarChart({ data, xKey, series, height, showLegend, showGrid, horizontal, targetKey, className }: BarChartProps): import("react").JSX.Element;
31
+ export declare function BarChart({ data, xKey, series, height, showLegend, showGrid, horizontal, targetKey, valueFormatter, axisFormatter, className }: BarChartProps): import("react").JSX.Element;
@@ -2,10 +2,11 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import ReactECharts from 'echarts-for-react';
4
4
  import { cn } from '../../../lib/utils.js';
5
+ import { formatCompactNumber, formatNumber } from '../../../lib/number-format.js';
5
6
  /**
6
7
  * BarChart - ECharts Bar Chart component
7
8
  */
8
- export function BarChart({ data, xKey, series, height = 300, showLegend = true, showGrid = true, horizontal = false, targetKey, className }) {
9
+ export function BarChart({ data, xKey, series, height = 300, showLegend = true, showGrid = true, horizontal = false, targetKey, valueFormatter, axisFormatter, className }) {
9
10
  // Bullet mode: horizontal KPI-vs-target rows (CSS-rendered, not ECharts).
10
11
  if (horizontal && targetKey) {
11
12
  const actualKey = series[0]?.key;
@@ -25,13 +26,14 @@ export function BarChart({ data, xKey, series, height = 300, showLegend = true,
25
26
  : met
26
27
  ? 'linear-gradient(90deg, #34d399 0%, #10b981 100%)'
27
28
  : 'linear-gradient(90deg, #fbbf24 0%, #f59e0b 100%)';
28
- return (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsxs("div", { className: "flex items-baseline justify-between gap-2", children: [_jsx("span", { className: "text-xs font-medium text-[rgb(var(--fg-default))] truncate", children: name }), _jsxs("span", { className: "text-xs font-semibold tabular-nums shrink-0", style: { color: barColor }, children: [achievement.toFixed(0), "%"] })] }), _jsxs("div", { className: "relative h-2.5 rounded-full bg-[rgb(var(--bg-subtle))] overflow-visible", children: [_jsx("div", { className: "absolute inset-y-0 left-0 rounded-full transition-all duration-300", style: { width: `${actualPct}%`, background: barGradient } }), _jsx("div", { className: "absolute top-1/2 -translate-y-1/2 -translate-x-1/2 w-1 h-4 rounded-full ring-2 ring-[rgb(var(--bg-surface))]", style: { left: `${targetPct}%`, backgroundColor: '#475569' }, title: `Target: ${target.toLocaleString()}` })] }), _jsxs("div", { className: "flex items-baseline justify-between gap-2 text-[0.65rem] text-[rgb(var(--fg-muted))] tabular-nums", children: [_jsx("span", { children: actual.toLocaleString() }), _jsx("span", { children: `Target ${target.toLocaleString()}` })] })] }, `${name}-${idx}`));
29
+ return (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsxs("div", { className: "flex items-baseline justify-between gap-2", children: [_jsx("span", { className: "text-xs font-medium text-[rgb(var(--fg-default))] truncate", children: name }), _jsxs("span", { className: "text-xs font-semibold tabular-nums shrink-0", style: { color: barColor }, children: [achievement.toFixed(0), "%"] })] }), _jsxs("div", { className: "relative h-2.5 rounded-full bg-[rgb(var(--bg-subtle))] overflow-visible", children: [_jsx("div", { className: "absolute inset-y-0 left-0 rounded-full transition-all duration-300", style: { width: `${actualPct}%`, background: barGradient } }), _jsx("div", { className: "absolute top-1/2 -translate-y-1/2 -translate-x-1/2 w-1 h-4 rounded-full ring-2 ring-[rgb(var(--bg-surface))]", style: { left: `${targetPct}%`, backgroundColor: '#475569' }, title: `Target: ${formatNumber(target)}` })] }), _jsxs("div", { className: "flex items-baseline justify-between gap-2 text-[0.65rem] text-[rgb(var(--fg-muted))] tabular-nums", children: [_jsx("span", { children: formatNumber(actual) }), _jsx("span", { children: `Target ${formatNumber(target)}` })] })] }, `${name}-${idx}`));
29
30
  }) }));
30
31
  }
31
32
  const categoryData = data.map(item => item[xKey]);
32
33
  const option = {
33
34
  tooltip: {
34
35
  trigger: 'item',
36
+ valueFormatter: (v) => (typeof v === 'number' ? (valueFormatter ?? formatNumber)(v) : String(v ?? '')),
35
37
  backgroundColor: 'rgba(255, 255, 255, 0.98)',
36
38
  borderColor: '#e9ecef',
37
39
  borderWidth: 1,
@@ -64,7 +66,7 @@ export function BarChart({ data, xKey, series, height = 300, showLegend = true,
64
66
  type: horizontal ? 'value' : 'category',
65
67
  data: horizontal ? undefined : categoryData,
66
68
  axisLine: { lineStyle: { color: '#dee2e6' } },
67
- axisLabel: { color: '#6c757d', fontSize: 11 },
69
+ axisLabel: horizontal ? { color: '#6c757d', fontSize: 11, hideOverlap: true, alignMaxLabel: 'right', formatter: (v) => (axisFormatter ?? formatCompactNumber)(v) } : { color: '#6c757d', fontSize: 11 },
68
70
  axisTick: { show: false },
69
71
  splitLine: horizontal && showGrid ? {
70
72
  lineStyle: { color: '#f0f0f0', type: 'dashed' }
@@ -74,7 +76,7 @@ export function BarChart({ data, xKey, series, height = 300, showLegend = true,
74
76
  type: horizontal ? 'category' : 'value',
75
77
  data: horizontal ? categoryData : undefined,
76
78
  axisLine: { show: false },
77
- axisLabel: { color: '#6c757d', fontSize: 11 },
79
+ axisLabel: horizontal ? { color: '#6c757d', fontSize: 11 } : { color: '#6c757d', fontSize: 11, formatter: (v) => (axisFormatter ?? formatCompactNumber)(v) },
78
80
  splitLine: !horizontal && showGrid ? {
79
81
  lineStyle: { color: '#f0f0f0', type: 'dashed' }
80
82
  } : { show: false }
@@ -6,6 +6,7 @@ import * as echarts from 'echarts/core';
6
6
  import { cn } from '../../../lib/utils.js';
7
7
  import { INDIA_GEOJSON } from './india-map.js';
8
8
  import { getChartTheme } from './theme.js';
9
+ import { formatNumber } from '../../../lib/number-format.js';
9
10
  /**
10
11
  * FlowMap - Geographic flow/route visualization
11
12
  *
@@ -161,7 +162,7 @@ export function FlowMap({ markers = [], lines = [], height = 400, markerColor =
161
162
  }
162
163
  if (params.seriesType === 'effectScatter' || params.seriesType === 'scatter') {
163
164
  const value = params.value?.[2];
164
- return `<strong>${params.name}</strong>${value ? `<br/>Value: ${value.toLocaleString()}` : ''}`;
165
+ return `<strong>${params.name}</strong>${value ? `<br/>Value: ${formatNumber(value)}` : ''}`;
165
166
  }
166
167
  return `<strong>${params.name}</strong>`;
167
168
  }
@@ -2,6 +2,7 @@
2
2
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
3
3
  import { useState } from 'react';
4
4
  import { cn } from '../../../lib/utils.js';
5
+ import { formatNumber } from '../../../lib/number-format.js';
5
6
  const FUNNEL_COLORS = ['#5470c6', '#3ba272', '#fac858', '#ee6666', '#73c0de', '#9a60b4', '#fc8452', '#91cc75'];
6
7
  /**
7
8
  * FunnelChart — centered, tapering stage-bar funnel for conversion/process flows.
@@ -30,6 +31,6 @@ export function FunnelChart({ data, height = 300, sort = 'descending', className
30
31
  background: `linear-gradient(90deg, ${color}, ${color}cc)`,
31
32
  transform: isHover ? 'translateY(-1px)' : 'none',
32
33
  boxShadow: isHover ? `0 4px 14px ${color}55` : `0 1px 2px rgba(0,0,0,0.06)`,
33
- }, title: `${item.name}: ${item.value.toLocaleString()} (${pct.toFixed(1)}%)`, children: [_jsx("span", { className: "text-sm font-semibold text-white truncate", children: item.name }), _jsx("span", { className: "text-xs font-medium text-white/90 tabular-nums shrink-0", children: item.value.toLocaleString() })] }) })] }, item.name));
34
+ }, title: `${item.name}: ${formatNumber(item.value)} (${pct.toFixed(1)}%)`, children: [_jsx("span", { className: "text-sm font-semibold text-white truncate", children: item.name }), _jsx("span", { className: "text-xs font-medium text-white/90 tabular-nums shrink-0", children: formatNumber(item.value) })] }) })] }, item.name));
34
35
  }) }));
35
36
  }
@@ -5,6 +5,7 @@ import ReactECharts from 'echarts-for-react';
5
5
  import * as echarts from 'echarts/core';
6
6
  import { cn } from '../../../lib/utils.js';
7
7
  import { INDIA_GEOJSON } from './india-map.js';
8
+ import { formatNumber } from '../../../lib/number-format.js';
8
9
  /**
9
10
  * GeoMap - Geographic scatter/bubble map for India
10
11
  * Shows location markers with optional size based on values
@@ -47,7 +48,7 @@ export function GeoMap({ markers = [], height = 400, markerColor = '#5470c6', ma
47
48
  formatter: (params) => {
48
49
  if (params.seriesType === 'effectScatter' || params.seriesType === 'scatter') {
49
50
  const value = params.value?.[2];
50
- return `<strong>${params.name}</strong>${value ? `<br/>Production: ${value.toLocaleString()} units` : ''}`;
51
+ return `<strong>${params.name}</strong>${value ? `<br/>Production: ${formatNumber(value)} units` : ''}`;
51
52
  }
52
53
  return `<strong>${params.name}</strong>`;
53
54
  }
@@ -25,9 +25,13 @@ export interface LineChartProps {
25
25
  yMin?: number;
26
26
  /** Y-axis maximum value */
27
27
  yMax?: number;
28
+ /** How a value reads in the tooltip. Default: the full number, Indian grouping (20,00,000). */
29
+ valueFormatter?: (value: number) => string;
30
+ /** How a value reads on the value axis. Default: compact (12,500 · 20 L · 1.4 Cr). */
31
+ axisFormatter?: (value: number) => string;
28
32
  className?: string;
29
33
  }
30
34
  /**
31
35
  * LineChart - ECharts Line Chart component (no area fill)
32
36
  */
33
- export declare function LineChart({ data, xKey, series, height, showLegend, showGrid, smooth, showLabels, yMin, yMax, className }: LineChartProps): import("react").JSX.Element;
37
+ export declare function LineChart({ data, xKey, series, height, showLegend, showGrid, smooth, showLabels, yMin, yMax, valueFormatter, axisFormatter, className }: LineChartProps): import("react").JSX.Element;
@@ -3,16 +3,18 @@ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import ReactECharts from 'echarts-for-react';
4
4
  import { cn } from '../../../lib/utils.js';
5
5
  import { getChartTheme, getTooltipConfig, getLegendConfig, getAxisConfig, getGridConfig } from './theme.js';
6
+ import { formatCompactNumber, formatNumber } from '../../../lib/number-format.js';
6
7
  /**
7
8
  * LineChart - ECharts Line Chart component (no area fill)
8
9
  */
9
- export function LineChart({ data, xKey, series, height = 300, showLegend = true, showGrid = true, smooth = false, showLabels = false, yMin, yMax, className }) {
10
+ export function LineChart({ data, xKey, series, height = 300, showLegend = true, showGrid = true, smooth = false, showLabels = false, yMin, yMax, valueFormatter, axisFormatter, className }) {
10
11
  const theme = getChartTheme();
11
12
  const axisConfig = getAxisConfig();
12
13
  const option = {
13
14
  tooltip: {
14
15
  trigger: 'axis',
15
- ...getTooltipConfig()
16
+ ...getTooltipConfig(),
17
+ valueFormatter: (v) => (typeof v === 'number' ? (valueFormatter ?? formatNumber)(v) : String(v ?? '')),
16
18
  },
17
19
  legend: showLegend ? getLegendConfig('top') : undefined,
18
20
  grid: getGridConfig(showLegend),
@@ -27,7 +29,7 @@ export function LineChart({ data, xKey, series, height = 300, showLegend = true,
27
29
  min: yMin,
28
30
  max: yMax,
29
31
  axisLine: { show: false },
30
- axisLabel: axisConfig.axisLabel,
32
+ axisLabel: { ...axisConfig.axisLabel, formatter: (v) => (axisFormatter ?? formatCompactNumber)(v) },
31
33
  splitLine: {
32
34
  show: showGrid,
33
35
  ...axisConfig.splitLine
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import ReactECharts from 'echarts-for-react';
4
4
  import { cn } from '../../../lib/utils.js';
5
+ import { formatNumber } from '../../../lib/number-format.js';
5
6
  /**
6
7
  * ParetoChart - Combined bar and line chart showing values and cumulative percentage
7
8
  * Used for identifying the "vital few" from the "trivial many" (80/20 rule)
@@ -28,7 +29,7 @@ export function ParetoChart({ data, height = 300, showLegend = true, barColor =
28
29
  const bar = params.find((p) => p.seriesType === 'bar');
29
30
  const line = params.find((p) => p.seriesType === 'line');
30
31
  return `${bar?.name}<br/>
31
- Value: ${bar?.value?.toLocaleString()}<br/>
32
+ Value: ${formatNumber(bar?.value)}<br/>
32
33
  Cumulative: ${line?.value}%`;
33
34
  }
34
35
  },
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import ReactECharts from 'echarts-for-react';
4
4
  import { cn } from '../../../lib/utils.js';
5
+ import { formatNumber } from '../../../lib/number-format.js';
5
6
  /**
6
7
  * SankeyChart - Flow/relationship visualization
7
8
  * Great for: Material flow, process flow, cost flow, conversion funnels
@@ -35,9 +36,9 @@ export function SankeyChart({ nodes, links, height = 300, orient = 'horizontal',
35
36
  textStyle: { color: '#344767', fontSize: 12 },
36
37
  formatter: (params) => {
37
38
  if (params.dataType === 'edge') {
38
- return `<strong>${params.data.source}</strong> → <strong>${params.data.target}</strong><br/>Value: ${params.data.value?.toLocaleString()}`;
39
+ return `<strong>${params.data.source}</strong> → <strong>${params.data.target}</strong><br/>Value: ${formatNumber(params.data.value)}`;
39
40
  }
40
- return `<strong>${params.name}</strong><br/>Value: ${params.value?.toLocaleString() || '-'}`;
41
+ return `<strong>${params.name}</strong><br/>Value: ${formatNumber(params.value) || '-'}`;
41
42
  }
42
43
  },
43
44
  series: [{
@@ -73,7 +74,7 @@ export function SankeyChart({ nodes, links, height = 300, orient = 'horizontal',
73
74
  color: '#344767',
74
75
  fontSize: 11,
75
76
  fontWeight: 500,
76
- formatter: (p) => `${p.name} (${p.value?.toLocaleString() ?? '-'})`
77
+ formatter: (p) => `${p.name} (${formatNumber(p.value) || '-'})`
77
78
  },
78
79
  data: nodesWithColors,
79
80
  links: links.map(link => ({
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import ReactECharts from 'echarts-for-react';
4
4
  import { cn } from '../../../lib/utils.js';
5
+ import { formatNumber } from '../../../lib/number-format.js';
5
6
  // Convert any color (hex, rgb(), rgba(), space-separated rgb()) to rgba() with given alpha (0–1).
6
7
  // Hex+alpha-suffix concat (e.g. color + '40') breaks for non-hex inputs and crashes canvas gradients.
7
8
  function withAlpha(color, alpha) {
@@ -97,7 +98,7 @@ export function Sparkline({ data, type = 'line', height = 40, width = '100%', co
97
98
  formatter: (params) => {
98
99
  const p = Array.isArray(params) ? params[0] : params;
99
100
  const cat = hasLabels && p.axisValue ? `${p.axisValue}<br/>` : '';
100
- return `${cat}<strong>${valuePrefix}${Number(p.value).toLocaleString()}${valueSuffix}</strong>`;
101
+ return `${cat}<strong>${valuePrefix}${formatNumber(Number(p.value))}${valueSuffix}</strong>`;
101
102
  }
102
103
  } : undefined,
103
104
  grid: {
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import ReactECharts from 'echarts-for-react';
4
4
  import { cn } from '../../../lib/utils.js';
5
+ import { formatNumber } from '../../../lib/number-format.js';
5
6
  /**
6
7
  * TreemapChart - Hierarchical data visualization
7
8
  * Great for: Cost breakdown, category distribution, budget allocation
@@ -22,7 +23,7 @@ export function TreemapChart({ data, height = 300, showBreadcrumb = false, color
22
23
  .map((node) => node.name)
23
24
  .filter((name) => name)
24
25
  .join(' → ');
25
- return `<strong>${path || params.name}</strong><br/>Value: ${params.value?.toLocaleString() || '-'}`;
26
+ return `<strong>${path || params.name}</strong><br/>Value: ${formatNumber(params.value) || '-'}`;
26
27
  }
27
28
  },
28
29
  series: [{
@@ -36,3 +36,4 @@ export { FlowMap } from './FlowMap.js';
36
36
  export type { FlowMapProps, FlowMapLine, FlowMapMarker } from './FlowMap.js';
37
37
  export { ChartCard } from './DashboardCard.js';
38
38
  export type { ChartCardProps } from './DashboardCard.js';
39
+ export { formatNumber, formatCompactNumber, setNumberLocale, getNumberLocale } from '../../../lib/number-format.js';
@@ -21,3 +21,5 @@ export { GanttChart } from './Gantt.js';
21
21
  export { FlowMap } from './FlowMap.js';
22
22
  // Chart Wrapper
23
23
  export { ChartCard } from './DashboardCard.js';
24
+ // Number formatting: Indian grouping by default; setNumberLocale() switches it for a whole app
25
+ export { formatNumber, formatCompactNumber, setNumberLocale, getNumberLocale } from '../../../lib/number-format.js';
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { cn } from '../../../lib/utils.js';
4
4
  import { KpiCardSkeleton } from './kpi-shell.js';
5
+ import { formatNumber } from '../../../lib/number-format.js';
5
6
  /**
6
7
  * KpiBreakdown - KPI card with list breakdown
7
8
  *
@@ -29,11 +30,11 @@ export function KpiBreakdown({ title, total, items, showPercentage = false, load
29
30
  };
30
31
  const totalValue = typeof total === 'number' ? total :
31
32
  items.reduce((sum, item) => sum + (typeof item.value === 'number' ? item.value : 0), 0);
32
- return (_jsxs("div", { className: cn('bg-[rgb(var(--bg-surface))] rounded-xl border border-[rgb(var(--bd-default))] shadow-sm', compact ? 'p-4' : 'p-5', className), children: [_jsxs("div", { className: cn('flex items-center justify-between', compact ? 'mb-2' : 'mb-3'), children: [_jsx("p", { className: "text-sm text-[rgb(var(--fg-muted))]", children: title }), total !== undefined && (_jsx("p", { className: "text-lg font-bold text-[rgb(var(--fg-default))]", children: typeof total === 'number' ? total.toLocaleString() : total }))] }), _jsx("div", { className: compact ? 'space-y-1' : 'space-y-2', children: items.map((item, index) => {
33
+ return (_jsxs("div", { className: cn('bg-[rgb(var(--bg-surface))] rounded-xl border border-[rgb(var(--bd-default))] shadow-sm', compact ? 'p-4' : 'p-5', className), children: [_jsxs("div", { className: cn('flex items-center justify-between', compact ? 'mb-2' : 'mb-3'), children: [_jsx("p", { className: "text-sm text-[rgb(var(--fg-muted))]", children: title }), total !== undefined && (_jsx("p", { className: "text-lg font-bold text-[rgb(var(--fg-default))]", children: typeof total === 'number' ? formatNumber(total) : total }))] }), _jsx("div", { className: compact ? 'space-y-1' : 'space-y-2', children: items.map((item, index) => {
33
34
  const colorClass = getColorClass(item.color);
34
35
  const isCustomColor = item.color?.startsWith('#') || item.color?.startsWith('rgb');
35
36
  const numValue = typeof item.value === 'number' ? item.value : 0;
36
37
  const percentage = totalValue > 0 ? Math.round((numValue / totalValue) * 100) : 0;
37
- return (_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: cn('w-2 h-2 rounded-full', colorClass), style: isCustomColor ? { backgroundColor: item.color } : undefined }), _jsx("span", { className: "text-sm text-[rgb(var(--fg-default))]", children: item.label })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-sm font-medium", children: typeof item.value === 'number' ? item.value.toLocaleString() : item.value }), showPercentage && (_jsxs("span", { className: "text-xs text-[rgb(var(--fg-muted))]", children: ["(", percentage, "%)"] }))] })] }, index));
38
+ return (_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: cn('w-2 h-2 rounded-full', colorClass), style: isCustomColor ? { backgroundColor: item.color } : undefined }), _jsx("span", { className: "text-sm text-[rgb(var(--fg-default))]", children: item.label })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-sm font-medium", children: typeof item.value === 'number' ? formatNumber(item.value) : item.value }), showPercentage && (_jsxs("span", { className: "text-xs text-[rgb(var(--fg-muted))]", children: ["(", percentage, "%)"] }))] })] }, index));
38
39
  }) })] }));
39
40
  }
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { cn } from '../../../lib/utils.js';
4
4
  import { KpiCardSkeleton } from './kpi-shell.js';
5
+ import { formatNumber } from '../../../lib/number-format.js';
5
6
  /**
6
7
  * KpiComparison - KPI card with comparison progress bars
7
8
  *
@@ -34,5 +35,5 @@ export function KpiComparison({ title, value, target, status, bars, loading, cla
34
35
  return 'bg-[rgb(var(--color-error))]/10 text-[rgb(var(--color-error))]';
35
36
  }
36
37
  };
37
- return (_jsxs("div", { className: cn('bg-[rgb(var(--bg-surface))] rounded-xl p-5 border border-[rgb(var(--bd-default))] shadow-sm', className), children: [_jsxs("div", { className: "flex items-center justify-between mb-1", children: [_jsx("p", { className: "text-sm text-[rgb(var(--fg-muted))]", children: title }), status && (_jsx("span", { className: cn('text-xs px-2 py-0.5 rounded-full', getStatusStyles(status.type)), children: status.label }))] }), _jsxs("div", { className: "flex items-baseline gap-2", children: [_jsx("p", { className: "text-3xl font-bold text-[rgb(var(--fg-default))]", children: value }), target && (_jsxs("span", { className: "text-sm text-[rgb(var(--fg-muted))]", children: ["/ ", target, " target"] }))] }), _jsx("div", { className: "mt-4 space-y-2", children: bars.map((bar, index) => (_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("span", { className: "text-xs text-[rgb(var(--fg-muted))] w-16", children: bar.label }), _jsx("div", { className: "flex-1 h-3 bg-[rgb(var(--bg-subtle))] rounded", children: _jsx("div", { className: cn('h-full rounded', getBarColor(bar.color)), style: { width: `${Math.min(bar.percentage, 100)}%` } }) }), _jsx("span", { className: "text-xs font-medium w-12 text-right", children: typeof bar.value === 'number' ? bar.value.toLocaleString() : bar.value })] }, index))) })] }));
38
+ return (_jsxs("div", { className: cn('bg-[rgb(var(--bg-surface))] rounded-xl p-5 border border-[rgb(var(--bd-default))] shadow-sm', className), children: [_jsxs("div", { className: "flex items-center justify-between mb-1", children: [_jsx("p", { className: "text-sm text-[rgb(var(--fg-muted))]", children: title }), status && (_jsx("span", { className: cn('text-xs px-2 py-0.5 rounded-full', getStatusStyles(status.type)), children: status.label }))] }), _jsxs("div", { className: "flex items-baseline gap-2", children: [_jsx("p", { className: "text-3xl font-bold text-[rgb(var(--fg-default))]", children: value }), target && (_jsxs("span", { className: "text-sm text-[rgb(var(--fg-muted))]", children: ["/ ", target, " target"] }))] }), _jsx("div", { className: "mt-4 space-y-2", children: bars.map((bar, index) => (_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("span", { className: "text-xs text-[rgb(var(--fg-muted))] w-16", children: bar.label }), _jsx("div", { className: "flex-1 h-3 bg-[rgb(var(--bg-subtle))] rounded", children: _jsx("div", { className: cn('h-full rounded', getBarColor(bar.color)), style: { width: `${Math.min(bar.percentage, 100)}%` } }) }), _jsx("span", { className: "text-xs font-medium w-12 text-right", children: typeof bar.value === 'number' ? formatNumber(bar.value) : bar.value })] }, index))) })] }));
38
39
  }
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { cn } from '../../../lib/utils.js';
4
4
  import { KpiCardSkeleton } from './kpi-shell.js';
5
+ import { formatNumber } from '../../../lib/number-format.js';
5
6
  /**
6
7
  * KpiQuickStats - KPI card with grid of icon stats
7
8
  *
@@ -33,6 +34,6 @@ export function KpiQuickStats({ title, stats, columns = 2, loading, className })
33
34
  };
34
35
  return (_jsxs("div", { className: cn('bg-[rgb(var(--bg-surface))] rounded-xl p-5 border border-[rgb(var(--bd-default))] shadow-sm', className), children: [_jsx("p", { className: "text-sm text-[rgb(var(--fg-muted))] mb-3", children: title }), _jsx("div", { className: cn('grid gap-x-3 gap-y-2', gridClasses[columns]), children: stats.map((stat, index) => {
35
36
  const Icon = stat.icon;
36
- return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Icon, { className: cn('w-4 h-4', getIconColorClass(stat.iconColor)) }), _jsxs("div", { children: [_jsx("p", { className: "text-lg font-bold text-[rgb(var(--fg-default))]", children: typeof stat.value === 'number' ? stat.value.toLocaleString() : stat.value }), _jsx("p", { className: "text-xs text-[rgb(var(--fg-muted))]", children: stat.label })] })] }, index));
37
+ return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Icon, { className: cn('w-4 h-4', getIconColorClass(stat.iconColor)) }), _jsxs("div", { children: [_jsx("p", { className: "text-lg font-bold text-[rgb(var(--fg-default))]", children: typeof stat.value === 'number' ? formatNumber(stat.value) : stat.value }), _jsx("p", { className: "text-xs text-[rgb(var(--fg-muted))]", children: stat.label })] })] }, index));
37
38
  }) })] }));
38
39
  }