create-bluecopa-react-app 1.0.40 → 1.0.42

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 (102) hide show
  1. package/README.md +16 -14
  2. package/package.json +1 -1
  3. package/templates/latest/.claude/settings.local.json +56 -0
  4. package/templates/latest/.env.example +8 -0
  5. package/templates/latest/Agent.md +598 -775
  6. package/templates/latest/CLAUDE.md +759 -0
  7. package/templates/latest/README.md +17 -8
  8. package/templates/latest/app/app.css +292 -85
  9. package/templates/latest/app/app.tsx +48 -39
  10. package/templates/latest/app/components/bluecopa-logo.tsx +20 -0
  11. package/templates/latest/app/components/charts/bar-chart.tsx +132 -0
  12. package/templates/latest/app/components/charts/base-chart.tsx +149 -0
  13. package/templates/latest/app/components/charts/chart-provider.tsx +71 -0
  14. package/templates/latest/app/components/charts/chart-theme.ts +262 -0
  15. package/templates/latest/app/components/charts/chart-utils.ts +142 -0
  16. package/templates/latest/app/components/charts/donut-chart.tsx +110 -0
  17. package/templates/latest/app/components/charts/index.ts +5 -0
  18. package/templates/latest/app/components/layouts/app-layout.tsx +22 -0
  19. package/templates/latest/app/components/layouts/app-sidebar.tsx +88 -0
  20. package/templates/latest/app/components/layouts/nav-main.tsx +50 -0
  21. package/templates/latest/app/components/layouts/nav-user.tsx +38 -0
  22. package/templates/latest/app/components/layouts/site-header.tsx +93 -0
  23. package/templates/latest/app/components/loading-screen.tsx +41 -0
  24. package/templates/latest/app/components/ui/ag-grid-table.tsx +79 -0
  25. package/templates/latest/app/components/ui/button.tsx +23 -23
  26. package/templates/latest/app/components/ui/card.tsx +20 -20
  27. package/templates/latest/app/components/ui/dropdown-menu.tsx +54 -49
  28. package/templates/latest/app/components/ui/input.tsx +8 -8
  29. package/templates/latest/app/components/ui/label.tsx +8 -8
  30. package/templates/latest/app/components/ui/separator.tsx +7 -7
  31. package/templates/latest/app/components/ui/sheet.tsx +43 -32
  32. package/templates/latest/app/components/ui/sidebar.tsx +240 -235
  33. package/templates/latest/app/components/ui/skeleton.tsx +4 -4
  34. package/templates/latest/app/components/ui/sonner.tsx +6 -9
  35. package/templates/latest/app/components/ui/tabs.tsx +15 -15
  36. package/templates/latest/app/components/ui/tooltip.tsx +18 -12
  37. package/templates/latest/app/constants/index.ts +31 -0
  38. package/templates/latest/app/contexts/app-context.tsx +201 -0
  39. package/templates/latest/app/hooks/use-mobile.ts +13 -12
  40. package/templates/latest/app/main.tsx +1 -1
  41. package/templates/latest/app/pages/dashboard.tsx +246 -0
  42. package/templates/latest/app/pages/payments.tsx +182 -0
  43. package/templates/latest/app/pages/settings.tsx +128 -0
  44. package/templates/latest/app/routes/index.tsx +19 -0
  45. package/templates/latest/app/single-spa.tsx +68 -86
  46. package/templates/latest/app/types/index.ts +37 -0
  47. package/templates/latest/app/utils/ag-grid-datasource.ts +63 -0
  48. package/templates/latest/app/utils/ag-grid-license.ts +12 -0
  49. package/templates/latest/app/utils/ag-grid-theme.ts +9 -0
  50. package/templates/latest/app/utils/component-style.ts +7 -0
  51. package/templates/latest/app/utils/style-drivers.ts +24 -0
  52. package/templates/latest/app/utils/utils.ts +10 -0
  53. package/templates/latest/components.json +3 -3
  54. package/templates/latest/index.html +30 -2
  55. package/templates/latest/package-lock.json +30 -416
  56. package/templates/latest/package.json +8 -18
  57. package/templates/latest/preview/index.html +125 -285
  58. package/templates/latest/public/favicon.svg +1 -0
  59. package/templates/latest/vite.config.ts +2 -8
  60. package/templates/latest/app/components/app-sidebar.tsx +0 -182
  61. package/templates/latest/app/components/chart-area-interactive.tsx +0 -290
  62. package/templates/latest/app/components/data-table.tsx +0 -807
  63. package/templates/latest/app/components/nav-documents.tsx +0 -92
  64. package/templates/latest/app/components/nav-main.tsx +0 -40
  65. package/templates/latest/app/components/nav-secondary.tsx +0 -42
  66. package/templates/latest/app/components/nav-user.tsx +0 -111
  67. package/templates/latest/app/components/section-cards.tsx +0 -102
  68. package/templates/latest/app/components/site-header.tsx +0 -28
  69. package/templates/latest/app/components/ui/avatar.tsx +0 -53
  70. package/templates/latest/app/components/ui/badge.tsx +0 -46
  71. package/templates/latest/app/components/ui/breadcrumb.tsx +0 -109
  72. package/templates/latest/app/components/ui/chart.tsx +0 -352
  73. package/templates/latest/app/components/ui/checkbox.tsx +0 -30
  74. package/templates/latest/app/components/ui/drawer.tsx +0 -139
  75. package/templates/latest/app/components/ui/select.tsx +0 -183
  76. package/templates/latest/app/components/ui/table.tsx +0 -117
  77. package/templates/latest/app/components/ui/toggle-group.tsx +0 -73
  78. package/templates/latest/app/components/ui/toggle.tsx +0 -47
  79. package/templates/latest/app/data/data.json +0 -614
  80. package/templates/latest/app/data/mock-payments.json +0 -122
  81. package/templates/latest/app/data/mock-transactions.json +0 -128
  82. package/templates/latest/app/hooks/use-bluecopa-user.ts +0 -37
  83. package/templates/latest/app/lib/utils.ts +0 -6
  84. package/templates/latest/app/routes/apitest.tsx +0 -2118
  85. package/templates/latest/app/routes/comments.tsx +0 -588
  86. package/templates/latest/app/routes/dashboard.tsx +0 -36
  87. package/templates/latest/app/routes/payments.tsx +0 -342
  88. package/templates/latest/app/routes/statements.tsx +0 -493
  89. package/templates/latest/app/routes/websocket.tsx +0 -450
  90. package/templates/latest/app/routes.tsx +0 -22
  91. package/templates/latest/dist/assets/__federation_expose_App-OFfdinOR.js +0 -97
  92. package/templates/latest/dist/assets/__federation_fn_import-CzfA7kmP.js +0 -438
  93. package/templates/latest/dist/assets/__federation_shared_react-Bp6HVBS4.js +0 -16
  94. package/templates/latest/dist/assets/__federation_shared_react-dom-BCcRGiYp.js +0 -17
  95. package/templates/latest/dist/assets/client-CkHcT_xc.js +0 -76035
  96. package/templates/latest/dist/assets/index-B3cD3sP_.js +0 -60
  97. package/templates/latest/dist/assets/index-BzNimew1.js +0 -69
  98. package/templates/latest/dist/assets/index-DMFtQdNS.js +0 -412
  99. package/templates/latest/dist/assets/remoteEntry.css +0 -3996
  100. package/templates/latest/dist/assets/remoteEntry.js +0 -88
  101. package/templates/latest/dist/favicon.ico +0 -0
  102. package/templates/latest/dist/index.html +0 -19
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Composable Chart Option Builders
3
+ *
4
+ * Each function returns a partial ECharts option that can be spread
5
+ * into the final chart configuration.
6
+ */
7
+
8
+ import type { EChartsOption } from "echarts";
9
+ import { resolveChartTokens } from "./chart-theme";
10
+
11
+ function tokens() {
12
+ return resolveChartTokens();
13
+ }
14
+
15
+ type DeepPartial<T> = { [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P] };
16
+
17
+ function merge<T extends Record<string, any>>(base: T, overrides?: DeepPartial<T>): T {
18
+ if (!overrides) return base;
19
+ const result = { ...base };
20
+ for (const key of Object.keys(overrides) as (keyof T)[]) {
21
+ const val = overrides[key];
22
+ if (val && typeof val === "object" && !Array.isArray(val) && typeof base[key] === "object") {
23
+ result[key] = merge(base[key] as any, val as any);
24
+ } else if (val !== undefined) {
25
+ result[key] = val as any;
26
+ }
27
+ }
28
+ return result;
29
+ }
30
+
31
+ // Tooltip
32
+ export function chartTooltip(overrides?: Record<string, any>): Pick<EChartsOption, "tooltip"> {
33
+ const t = tokens();
34
+ const base = {
35
+ trigger: "axis" as const,
36
+ confine: true,
37
+ backgroundColor: t.backgroundColor,
38
+ borderColor: t.borderColor,
39
+ borderWidth: 1,
40
+ borderRadius: 8,
41
+ padding: [10, 14],
42
+ textStyle: { fontFamily: t.fontFamily, fontSize: 12, color: t.textColor },
43
+ extraCssText: "box-shadow: 0 4px 16px rgba(0,0,0,0.1); backdrop-filter: blur(8px);",
44
+ axisPointer: { type: "shadow" as const, shadowStyle: { color: "rgba(0,0,0,0.04)" } },
45
+ };
46
+ return { tooltip: merge(base, overrides as any) };
47
+ }
48
+
49
+ // Legend
50
+ export function chartLegend(overrides?: Record<string, any>): Pick<EChartsOption, "legend"> {
51
+ const t = tokens();
52
+ const pos = overrides?.position ?? "bottom";
53
+ const positionMap: Record<string, any> = {
54
+ top: { top: 0, left: "center" },
55
+ bottom: { bottom: 0, left: "center" },
56
+ left: { left: 0, top: "middle", orient: "vertical" },
57
+ right: { right: 0, top: "middle", orient: "vertical" },
58
+ };
59
+ const base = {
60
+ show: true, icon: "roundRect", itemWidth: 12, itemHeight: 12, itemGap: 16,
61
+ textStyle: { fontFamily: t.fontFamily, fontSize: 12, color: t.textColorSecondary },
62
+ ...positionMap[pos],
63
+ };
64
+ const { position, ...rest } = overrides ?? {};
65
+ return { legend: merge(base, rest) };
66
+ }
67
+
68
+ // Category Axis
69
+ export function chartCategoryAxis(data?: string[], overrides?: Record<string, any>): any {
70
+ const t = tokens();
71
+ const base: any = {
72
+ type: "category", data,
73
+ axisLine: { show: true, lineStyle: { color: t.borderColor } },
74
+ axisTick: { show: false },
75
+ axisLabel: { fontFamily: t.fontFamily, fontSize: 11, color: t.textColorSecondary, rotate: overrides?.rotate ?? 0 },
76
+ splitLine: { show: false },
77
+ };
78
+ return merge(base, overrides);
79
+ }
80
+
81
+ // Value Axis
82
+ export function chartValueAxis(overrides?: Record<string, any>): any {
83
+ const t = tokens();
84
+ const base: any = {
85
+ type: "value",
86
+ axisLine: { show: false }, axisTick: { show: false },
87
+ axisLabel: { fontFamily: t.fontFamily, fontSize: 11, color: t.textColorSecondary },
88
+ splitLine: { show: true, lineStyle: { color: t.borderColor, type: "dashed", opacity: 0.6 } },
89
+ };
90
+ return merge(base, overrides);
91
+ }
92
+
93
+ // Title
94
+ export function chartTitle(text: string, overrides?: { subtitle?: string; left?: string | number }): Pick<EChartsOption, "title"> {
95
+ const t = tokens();
96
+ const base = {
97
+ text, subtext: overrides?.subtitle ?? "", left: overrides?.left ?? 0, top: 0, padding: [0, 0, 8, 0],
98
+ textStyle: { fontFamily: t.fontFamily, fontSize: 16, fontWeight: 600 as const, color: t.textColor },
99
+ subtextStyle: { fontFamily: t.fontFamily, fontSize: 12, color: t.textColorSecondary },
100
+ };
101
+ return { title: base };
102
+ }
103
+
104
+ // Grid
105
+ export function chartGrid(overrides?: Record<string, any>): Pick<EChartsOption, "grid"> {
106
+ const base = { top: 48, right: 16, bottom: 48, left: 16, containLabel: true };
107
+ return { grid: merge(base, overrides as any) };
108
+ }
109
+
110
+ // Animation
111
+ export function chartAnimation(overrides?: Record<string, any>): Record<string, any> {
112
+ return {
113
+ animation: true,
114
+ animationDuration: overrides?.duration ?? 800,
115
+ animationEasing: overrides?.easing ?? "cubicInOut",
116
+ animationDelay: overrides?.delay ?? ((idx: number) => idx * 60),
117
+ animationDurationUpdate: 400,
118
+ animationEasingUpdate: "cubicInOut",
119
+ };
120
+ }
121
+
122
+ // Colors
123
+ export function chartColors(): string[] {
124
+ return tokens().colors;
125
+ }
126
+
127
+ export function chartColor(index: number): string {
128
+ const colors = chartColors();
129
+ return colors[index % colors.length];
130
+ }
131
+
132
+ // Formatters
133
+ export function formatChartValue(value: number, decimals = 1): string {
134
+ if (Math.abs(value) >= 1e9) return (value / 1e9).toFixed(decimals) + "B";
135
+ if (Math.abs(value) >= 1e6) return (value / 1e6).toFixed(decimals) + "M";
136
+ if (Math.abs(value) >= 1e3) return (value / 1e3).toFixed(decimals) + "K";
137
+ return value.toFixed(decimals);
138
+ }
139
+
140
+ export function formatChartCurrency(value: number, currency = "USD", decimals = 0): string {
141
+ return new Intl.NumberFormat("en-US", { style: "currency", currency, minimumFractionDigits: decimals, maximumFractionDigits: decimals }).format(value);
142
+ }
@@ -0,0 +1,110 @@
1
+ import React, { useMemo } from "react";
2
+ import { BaseChart } from "./base-chart";
3
+ import { chartTooltip, chartLegend, chartAnimation, chartColors } from "./chart-utils";
4
+ import { resolveChartTokens } from "./chart-theme";
5
+
6
+ export interface DonutChartData {
7
+ items: { name: string; value: number; color?: string }[];
8
+ }
9
+
10
+ export interface DonutChartProps {
11
+ data: DonutChartData;
12
+ title?: string;
13
+ subtitle?: string;
14
+ height?: string | number;
15
+ className?: string;
16
+ showLegend?: boolean;
17
+ showLabels?: boolean;
18
+ centerText?: string;
19
+ centerSubText?: string;
20
+ thickness?: number;
21
+ loading?: boolean;
22
+ empty?: boolean;
23
+ onSliceClick?: (params: any) => void;
24
+ ariaLabel?: string;
25
+ }
26
+
27
+ export function DonutChart({
28
+ data, title, subtitle, height = 300, className, showLegend = true, showLabels = false,
29
+ centerText, centerSubText, thickness = 60, loading = false, empty = false, onSliceClick, ariaLabel,
30
+ }: DonutChartProps) {
31
+ const option = useMemo(() => {
32
+ const colors = chartColors();
33
+ const tokens = resolveChartTokens();
34
+ const innerRadius = `${Math.max(100 - thickness, 20)}%`;
35
+ const outerRadius = "70%";
36
+
37
+ const graphic: any[] = [];
38
+ if (centerText) {
39
+ graphic.push({
40
+ type: "text", left: "center", top: "42%",
41
+ style: { text: centerText, fontSize: 24, fontWeight: 700, fontFamily: tokens.fontFamily, fill: tokens.textColor, textAlign: "center" },
42
+ });
43
+ }
44
+ if (centerSubText) {
45
+ graphic.push({
46
+ type: "text", left: "center", top: "54%",
47
+ style: { text: centerSubText, fontSize: 12, fontFamily: tokens.fontFamily, fill: tokens.textColorSecondary, textAlign: "center" },
48
+ });
49
+ }
50
+
51
+ return {
52
+ ...(title ? {
53
+ title: {
54
+ text: title, subtext: subtitle || "", left: 0, top: 0,
55
+ textStyle: { fontSize: 16, fontWeight: 600, fontFamily: tokens.fontFamily, color: tokens.textColor },
56
+ subtextStyle: { fontSize: 12, fontFamily: tokens.fontFamily, color: tokens.textColorSecondary },
57
+ },
58
+ } : {}),
59
+ ...chartTooltip({ trigger: "item" }),
60
+ ...(showLegend ? chartLegend({ position: "bottom" }) : {}),
61
+ ...chartAnimation(),
62
+ graphic,
63
+ series: [{
64
+ type: "pie" as const,
65
+ radius: [innerRadius, outerRadius],
66
+ center: ["50%", "50%"],
67
+ avoidLabelOverlap: true,
68
+ padAngle: 2,
69
+ itemStyle: { borderColor: tokens.backgroundColor, borderWidth: 2, borderRadius: 4 },
70
+ label: { show: showLabels, formatter: "{b}: {d}%", fontSize: 12, fontFamily: tokens.fontFamily },
71
+ labelLine: { show: showLabels },
72
+ emphasis: {
73
+ focus: "self",
74
+ scale: false, scaleSize: 0,
75
+ itemStyle: { shadowBlur: 16, shadowOffsetY: 4, shadowColor: "rgba(0,0,0,0.25)", borderWidth: 3, borderColor: "rgba(255,255,255,0.6)" },
76
+ },
77
+ data: data.items.map((item, i) => ({
78
+ name: item.name, value: item.value,
79
+ itemStyle: item.color ? { color: item.color } : { color: colors[i % colors.length] },
80
+ })),
81
+ animationType: "expansion",
82
+ animationEasing: "cubicInOut",
83
+ animationDelay: (idx: number) => idx * 60,
84
+ }],
85
+ };
86
+ }, [data, title, subtitle, showLegend, showLabels, centerText, centerSubText, thickness]);
87
+
88
+ const dataSummary = useMemo(() => {
89
+ const total = data.items.reduce((sum, item) => sum + item.value, 0);
90
+ return data.items.map((item) => {
91
+ const pct = total > 0 ? ((item.value / total) * 100).toFixed(1) : "0";
92
+ return `${item.name}: ${item.value.toLocaleString()} (${pct}%)`;
93
+ }).join(", ");
94
+ }, [data]);
95
+
96
+ return (
97
+ <BaseChart
98
+ option={option}
99
+ height={height}
100
+ className={className}
101
+ loading={loading}
102
+ empty={empty || data.items.length === 0}
103
+ onEvents={onSliceClick ? { click: onSliceClick } : undefined}
104
+ ariaLabel={ariaLabel || `Donut chart${title ? `: ${title}` : ""}`}
105
+ dataSummary={dataSummary}
106
+ />
107
+ );
108
+ }
109
+
110
+ export default DonutChart;
@@ -0,0 +1,5 @@
1
+ export { ChartProvider, useChartContext } from "./chart-provider";
2
+ export { BaseChart, type BaseChartProps, type BaseChartRef } from "./base-chart";
3
+ export { BarChart, type BarChartProps, type BarChartData, type BarChartSeries } from "./bar-chart";
4
+ export { DonutChart, type DonutChartProps, type DonutChartData } from "./donut-chart";
5
+ export { chartColors, chartColor, formatChartValue, formatChartCurrency } from "./chart-utils";
@@ -0,0 +1,22 @@
1
+ import { Outlet } from "react-router-dom";
2
+ import { AppSidebar } from "~/components/layouts/app-sidebar";
3
+ import { SiteHeader } from "~/components/layouts/site-header";
4
+ import { SidebarInset, SidebarProvider } from "~/components/ui/sidebar";
5
+
6
+ export function AppLayout() {
7
+ return (
8
+ <SidebarProvider
9
+ style={
10
+ { "--sidebar-width": "16rem" } as React.CSSProperties
11
+ }
12
+ >
13
+ <AppSidebar />
14
+ <SidebarInset>
15
+ <SiteHeader />
16
+ <div className="copa:flex copa:flex-1 copa:flex-col copa:gap-6 copa:px-6 copa:py-6">
17
+ <Outlet />
18
+ </div>
19
+ </SidebarInset>
20
+ </SidebarProvider>
21
+ );
22
+ }
@@ -0,0 +1,88 @@
1
+ import * as React from "react";
2
+ import {
3
+ IconChartBar,
4
+ IconCreditCard,
5
+ IconDashboard,
6
+ IconFileText,
7
+ IconFolder,
8
+ IconLayoutList,
9
+ IconReceipt,
10
+ IconSettings,
11
+ IconTransfer,
12
+ IconUsers,
13
+ } from "@tabler/icons-react";
14
+
15
+ import { BluecopaLogo } from "~/components/bluecopa-logo";
16
+ import { NavMain, type NavSection } from "~/components/layouts/nav-main";
17
+ import { NavFooter } from "~/components/layouts/nav-user";
18
+ import { ROUTES } from "~/constants";
19
+ import {
20
+ Sidebar,
21
+ SidebarContent,
22
+ SidebarFooter,
23
+ SidebarHeader,
24
+ SidebarMenu,
25
+ SidebarMenuButton,
26
+ SidebarMenuItem,
27
+ SidebarSeparator,
28
+ } from "~/components/ui/sidebar";
29
+
30
+ const navSections: NavSection[] = [
31
+ {
32
+ label: "Overview",
33
+ items: [
34
+ { title: "Dashboard", url: ROUTES.DASHBOARD, icon: IconDashboard },
35
+ { title: "Analytics", url: "/analytics", icon: IconChartBar },
36
+ ],
37
+ },
38
+ {
39
+ label: "Finance",
40
+ items: [
41
+ { title: "Transactions", url: "/transactions", icon: IconTransfer },
42
+ { title: "Invoices", url: "/invoices", icon: IconReceipt },
43
+ { title: "Payments", url: ROUTES.PAYMENTS, icon: IconCreditCard },
44
+ ],
45
+ },
46
+ {
47
+ label: "Workspace",
48
+ items: [
49
+ { title: "Reports", url: "/reports", icon: IconFileText },
50
+ { title: "Projects", url: "/projects", icon: IconFolder },
51
+ { title: "Workbooks", url: "/workbooks", icon: IconLayoutList },
52
+ { title: "Team", url: "/team", icon: IconUsers },
53
+ ],
54
+ },
55
+ {
56
+ items: [{ title: "Settings", url: ROUTES.SETTINGS, icon: IconSettings }],
57
+ },
58
+ ];
59
+
60
+ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
61
+ return (
62
+ <Sidebar collapsible="offcanvas" {...props}>
63
+ <SidebarHeader>
64
+ <SidebarMenu>
65
+ <SidebarMenuItem>
66
+ <SidebarMenuButton
67
+ asChild
68
+ className="copa:data-[slot=sidebar-menu-button]:!p-1.5"
69
+ >
70
+ <a href="https://bluecopa.com">
71
+ <BluecopaLogo className="copa:size-8" />
72
+ <span className="copa:text-base copa:font-semibold">Bluecopa</span>
73
+ </a>
74
+ </SidebarMenuButton>
75
+ </SidebarMenuItem>
76
+ </SidebarMenu>
77
+ </SidebarHeader>
78
+ <SidebarSeparator className="copa:mx-0" />
79
+ <SidebarContent>
80
+ <NavMain sections={navSections} />
81
+ </SidebarContent>
82
+ <SidebarSeparator className="copa:mx-0" />
83
+ <SidebarFooter>
84
+ <NavFooter />
85
+ </SidebarFooter>
86
+ </Sidebar>
87
+ );
88
+ }
@@ -0,0 +1,50 @@
1
+ import { type Icon } from "@tabler/icons-react";
2
+ import { Link } from "react-router-dom";
3
+
4
+ import {
5
+ SidebarGroup,
6
+ SidebarGroupContent,
7
+ SidebarGroupLabel,
8
+ SidebarMenu,
9
+ SidebarMenuButton,
10
+ SidebarMenuItem,
11
+ } from "~/components/ui/sidebar";
12
+
13
+ export type NavItem = {
14
+ title: string;
15
+ url: string;
16
+ icon?: Icon;
17
+ };
18
+
19
+ export type NavSection = {
20
+ label?: string;
21
+ items: NavItem[];
22
+ };
23
+
24
+ export function NavMain({ sections }: { sections: NavSection[] }) {
25
+ return (
26
+ <>
27
+ {sections.map((section, i) => (
28
+ <SidebarGroup key={section.label ?? i}>
29
+ {section.label && (
30
+ <SidebarGroupLabel>{section.label}</SidebarGroupLabel>
31
+ )}
32
+ <SidebarGroupContent>
33
+ <SidebarMenu>
34
+ {section.items.map((item) => (
35
+ <SidebarMenuItem key={item.title}>
36
+ <SidebarMenuButton tooltip={item.title} asChild>
37
+ <Link to={item.url}>
38
+ {item.icon && <item.icon />}
39
+ <span>{item.title}</span>
40
+ </Link>
41
+ </SidebarMenuButton>
42
+ </SidebarMenuItem>
43
+ ))}
44
+ </SidebarMenu>
45
+ </SidebarGroupContent>
46
+ </SidebarGroup>
47
+ ))}
48
+ </>
49
+ );
50
+ }
@@ -0,0 +1,38 @@
1
+ import { IconHelpCircle, IconMessageCircle } from "@tabler/icons-react";
2
+
3
+ import {
4
+ SidebarMenu,
5
+ SidebarMenuButton,
6
+ SidebarMenuItem,
7
+ } from "~/components/ui/sidebar";
8
+
9
+ export function NavFooter() {
10
+ return (
11
+ <SidebarMenu>
12
+ <SidebarMenuItem>
13
+ <SidebarMenuButton asChild>
14
+ <a
15
+ href="https://docs.bluecopa.com"
16
+ target="_blank"
17
+ rel="noopener noreferrer"
18
+ >
19
+ <IconHelpCircle className="copa:size-4" />
20
+ <span>Help & Docs</span>
21
+ </a>
22
+ </SidebarMenuButton>
23
+ </SidebarMenuItem>
24
+ <SidebarMenuItem>
25
+ <SidebarMenuButton asChild>
26
+ <a
27
+ href="https://bluecopa.com/support"
28
+ target="_blank"
29
+ rel="noopener noreferrer"
30
+ >
31
+ <IconMessageCircle className="copa:size-4" />
32
+ <span>Support</span>
33
+ </a>
34
+ </SidebarMenuButton>
35
+ </SidebarMenuItem>
36
+ </SidebarMenu>
37
+ );
38
+ }
@@ -0,0 +1,93 @@
1
+ import {
2
+ IconChevronDown,
3
+ IconLogout,
4
+ IconSettings,
5
+ IconUserCircle,
6
+ } from "@tabler/icons-react";
7
+
8
+ import { BluecopaLogo } from "~/components/bluecopa-logo";
9
+ import {
10
+ DropdownMenu,
11
+ DropdownMenuContent,
12
+ DropdownMenuItem,
13
+ DropdownMenuLabel,
14
+ DropdownMenuSeparator,
15
+ DropdownMenuTrigger,
16
+ } from "~/components/ui/dropdown-menu";
17
+ import { Separator } from "~/components/ui/separator";
18
+ import { SidebarTrigger } from "~/components/ui/sidebar";
19
+ import { useAppContext } from "~/contexts/app-context";
20
+
21
+ function UserInitials({ name }: { name?: string }) {
22
+ const initials = (name ?? "")
23
+ .split(" ")
24
+ .map((w) => w[0])
25
+ .join("")
26
+ .slice(0, 2)
27
+ .toUpperCase();
28
+
29
+ return (
30
+ <span className="copa:flex copa:size-8 copa:shrink-0 copa:items-center copa:justify-center copa:rounded-full copa:bg-primary/10 copa:text-xs copa:font-semibold copa:text-primary">
31
+ {initials || "?"}
32
+ </span>
33
+ );
34
+ }
35
+
36
+ export function SiteHeader() {
37
+ const { user } = useAppContext();
38
+
39
+ return (
40
+ <header className="copa:flex copa:h-14 copa:shrink-0 copa:items-center copa:gap-2 copa:px-6 copa:bg-sidebar copa:border-b copa:border-sidebar-border">
41
+ <SidebarTrigger className="copa:-ml-1" />
42
+ <Separator
43
+ orientation="vertical"
44
+ className="copa:mx-2 copa:data-[orientation=vertical]:h-4"
45
+ />
46
+ <BluecopaLogo className="copa:size-7" />
47
+ <span className="copa:text-sm copa:font-semibold">Bluecopa</span>
48
+
49
+ <div className="copa:ml-auto">
50
+ <DropdownMenu>
51
+ <DropdownMenuTrigger asChild>
52
+ <button className="copa:flex copa:items-center copa:gap-2.5 copa:rounded-full copa:border copa:border-border copa:py-1 copa:pl-1 copa:pr-3 copa:text-sm copa:shadow-sm copa:transition-colors copa:hover:bg-accent copa:focus-visible:outline-none copa:focus-visible:ring-2 copa:focus-visible:ring-ring">
53
+ <UserInitials name={user?.name} />
54
+ <span className="copa:hidden copa:sm:inline copa:font-medium copa:text-foreground">
55
+ {user?.name}
56
+ </span>
57
+ <IconChevronDown className="copa:size-3.5 copa:text-muted-foreground" />
58
+ </button>
59
+ </DropdownMenuTrigger>
60
+ <DropdownMenuContent align="end" sideOffset={8} className="copa:w-60 copa:rounded-lg copa:p-1.5 copa:shadow-lg">
61
+ <DropdownMenuLabel className="copa:px-2 copa:py-2 copa:font-normal">
62
+ <div className="copa:flex copa:items-center copa:gap-2.5">
63
+ <UserInitials name={user?.name} />
64
+ <div className="copa:min-w-0 copa:flex copa:flex-col">
65
+ <span className="copa:truncate copa:text-sm copa:font-medium">
66
+ {user?.name}
67
+ </span>
68
+ <span className="copa:truncate copa:text-xs copa:text-muted-foreground">
69
+ {user?.email}
70
+ </span>
71
+ </div>
72
+ </div>
73
+ </DropdownMenuLabel>
74
+ <DropdownMenuSeparator className="copa:my-1.5" />
75
+ <DropdownMenuItem className="copa:gap-2 copa:px-2 copa:py-1.5 copa:text-sm">
76
+ <IconUserCircle className="copa:size-4 copa:text-muted-foreground" />
77
+ Profile
78
+ </DropdownMenuItem>
79
+ <DropdownMenuItem className="copa:gap-2 copa:px-2 copa:py-1.5 copa:text-sm">
80
+ <IconSettings className="copa:size-4 copa:text-muted-foreground" />
81
+ Settings
82
+ </DropdownMenuItem>
83
+ <DropdownMenuSeparator className="copa:my-1.5" />
84
+ <DropdownMenuItem className="copa:gap-2 copa:px-2 copa:py-1.5 copa:text-sm copa:text-destructive">
85
+ <IconLogout className="copa:size-4" />
86
+ Log out
87
+ </DropdownMenuItem>
88
+ </DropdownMenuContent>
89
+ </DropdownMenu>
90
+ </div>
91
+ </header>
92
+ );
93
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Full-screen loading indicator shown while the app is initializing
3
+ * (config, CSS, user data). Uses inline styles so it renders correctly
4
+ * even before Tailwind CSS is fully loaded.
5
+ */
6
+ export function LoadingScreen({ message = "Loading..." }: { message?: string }) {
7
+ return (
8
+ <div
9
+ style={{
10
+ display: "flex",
11
+ alignItems: "center",
12
+ justifyContent: "center",
13
+ height: "100vh",
14
+ width: "100%",
15
+ background: "#f8f8fa",
16
+ fontFamily: "'Satoshi', system-ui, sans-serif",
17
+ }}
18
+ >
19
+ <div
20
+ style={{
21
+ display: "flex",
22
+ flexDirection: "column",
23
+ alignItems: "center",
24
+ gap: "16px",
25
+ }}
26
+ >
27
+ <div
28
+ style={{
29
+ width: "40px",
30
+ height: "40px",
31
+ border: "3px solid #e5e7eb",
32
+ borderTopColor: "#3548ff",
33
+ borderRadius: "50%",
34
+ animation: "init-spin 0.7s linear infinite",
35
+ }}
36
+ />
37
+ <span style={{ color: "#6b7280", fontSize: "0.835rem" }}>{message}</span>
38
+ </div>
39
+ </div>
40
+ );
41
+ }
@@ -0,0 +1,79 @@
1
+ import type { ReactNode } from "react";
2
+ import { AgGridReact } from "ag-grid-react";
3
+ import type { ColDef, GridOptions } from "ag-grid-community";
4
+ import {
5
+ AllCommunityModule,
6
+ ClientSideRowModelModule,
7
+ ModuleRegistry,
8
+ NumberEditorModule,
9
+ NumberFilterModule,
10
+ TextFilterModule,
11
+ ValidationModule,
12
+ } from "ag-grid-community";
13
+ import { AgChartsEnterpriseModule } from "ag-charts-enterprise";
14
+ import {
15
+ MasterDetailModule,
16
+ ServerSideRowModelModule,
17
+ SetFilterModule,
18
+ SparklinesModule,
19
+ ContextMenuModule,
20
+ } from "ag-grid-enterprise";
21
+ import { gridTheme } from "~/utils/ag-grid-theme";
22
+
23
+ ModuleRegistry.registerModules([
24
+ AllCommunityModule,
25
+ ClientSideRowModelModule,
26
+ MasterDetailModule,
27
+ SetFilterModule,
28
+ ServerSideRowModelModule,
29
+ SparklinesModule.with(AgChartsEnterpriseModule),
30
+ ContextMenuModule,
31
+ ValidationModule,
32
+ NumberFilterModule,
33
+ NumberEditorModule,
34
+ TextFilterModule,
35
+ ]);
36
+
37
+ export interface AgGridTableProps<TData extends object = any> {
38
+ columnDefs: ColDef<TData>[];
39
+ rowData?: TData[];
40
+ gridOptions?: GridOptions<TData>;
41
+ height?: number | string;
42
+ className?: string;
43
+ header?: ReactNode;
44
+ quickFilterText?: string;
45
+ }
46
+
47
+ export function AgGridTable<TData extends object = any>({
48
+ columnDefs,
49
+ rowData,
50
+ gridOptions,
51
+ height = 500,
52
+ className = "",
53
+ header,
54
+ quickFilterText,
55
+ }: AgGridTableProps<TData>) {
56
+ const isInfiniteOrServerSide =
57
+ gridOptions?.rowModelType === "infinite" ||
58
+ gridOptions?.rowModelType === "serverSide";
59
+
60
+ return (
61
+ <div className={`copa:flex copa:flex-col copa:gap-2 ${className}`}>
62
+ {header ? <div>{header}</div> : null}
63
+ <div
64
+ style={{
65
+ height: typeof height === "number" ? `${height}px` : height,
66
+ }}
67
+ >
68
+ <AgGridReact<TData>
69
+ theme={gridTheme}
70
+ columnDefs={columnDefs}
71
+ {...(!isInfiniteOrServerSide && rowData ? { rowData } : {})}
72
+ animateRows
73
+ quickFilterText={quickFilterText}
74
+ {...gridOptions}
75
+ />
76
+ </div>
77
+ </div>
78
+ );
79
+ }