@git-snitch/renderer 0.0.3

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 (74) hide show
  1. package/dist/build.d.ts +7 -0
  2. package/dist/build.d.ts.map +1 -0
  3. package/dist/build.js +53 -0
  4. package/dist/charts.d.ts +106 -0
  5. package/dist/charts.d.ts.map +1 -0
  6. package/dist/charts.js +212 -0
  7. package/dist/custom-templates.d.ts +3 -0
  8. package/dist/custom-templates.d.ts.map +1 -0
  9. package/dist/custom-templates.js +1 -0
  10. package/dist/data.d.ts +24 -0
  11. package/dist/data.d.ts.map +1 -0
  12. package/dist/data.js +30 -0
  13. package/dist/empty-state.d.ts +13 -0
  14. package/dist/empty-state.d.ts.map +1 -0
  15. package/dist/empty-state.js +9 -0
  16. package/dist/export.d.ts +15 -0
  17. package/dist/export.d.ts.map +1 -0
  18. package/dist/export.js +53 -0
  19. package/dist/index.d.ts +15 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +9 -0
  22. package/dist/inline-plugin.d.ts +13 -0
  23. package/dist/inline-plugin.d.ts.map +1 -0
  24. package/dist/inline-plugin.js +81 -0
  25. package/dist/layout.d.ts +43 -0
  26. package/dist/layout.d.ts.map +1 -0
  27. package/dist/layout.js +25 -0
  28. package/dist/remote-urls.d.ts +6 -0
  29. package/dist/remote-urls.d.ts.map +1 -0
  30. package/dist/remote-urls.js +82 -0
  31. package/dist/serialization.d.ts +5 -0
  32. package/dist/serialization.d.ts.map +1 -0
  33. package/dist/serialization.js +46 -0
  34. package/dist/tables.d.ts +50 -0
  35. package/dist/tables.d.ts.map +1 -0
  36. package/dist/tables.js +228 -0
  37. package/dist/template/report-template.html +135 -0
  38. package/dist/template.d.ts +21 -0
  39. package/dist/template.d.ts.map +1 -0
  40. package/dist/template.js +1 -0
  41. package/dist/theme-toggle.d.ts +2 -0
  42. package/dist/theme-toggle.d.ts.map +1 -0
  43. package/dist/theme-toggle.js +9 -0
  44. package/dist/theme.d.ts +16 -0
  45. package/dist/theme.d.ts.map +1 -0
  46. package/dist/theme.js +70 -0
  47. package/package.json +57 -0
  48. package/report-template.html +15 -0
  49. package/src/app.tsx +351 -0
  50. package/src/build.ts +68 -0
  51. package/src/charts-route.tsx +158 -0
  52. package/src/charts.tsx +482 -0
  53. package/src/custom-template-module.d.ts +5 -0
  54. package/src/custom-templates.ts +3 -0
  55. package/src/data.ts +52 -0
  56. package/src/empty-state.tsx +31 -0
  57. package/src/export.ts +77 -0
  58. package/src/index.ts +52 -0
  59. package/src/inline-plugin.ts +123 -0
  60. package/src/layout.tsx +152 -0
  61. package/src/main.tsx +17 -0
  62. package/src/overview.tsx +253 -0
  63. package/src/quality-hotspots-routes.tsx +340 -0
  64. package/src/remote-urls.ts +97 -0
  65. package/src/repo-routes.tsx +285 -0
  66. package/src/scan-routes.tsx +393 -0
  67. package/src/serialization.ts +58 -0
  68. package/src/styles.css +2 -0
  69. package/src/tables.tsx +467 -0
  70. package/src/template.ts +30 -0
  71. package/src/theme-toggle.tsx +24 -0
  72. package/src/theme.tsx +108 -0
  73. package/src/vite-env.d.ts +1 -0
  74. package/vite.config.ts +41 -0
package/src/data.ts ADDED
@@ -0,0 +1,52 @@
1
+ import { useMemo } from "react";
2
+ import { isRepoReportData, isScanReportData, reportDataDiscriminantSchema } from "@git-snitch/core/report-data";
3
+ import type { RepoReportData, ReportData, ScanReportData } from "@git-snitch/core";
4
+
5
+ declare global {
6
+ interface Window {
7
+ readonly __GIT_SNITCH_REPORT_DATA__?: unknown;
8
+ }
9
+ }
10
+
11
+ export type ReportDataState =
12
+ | { readonly status: "ready"; readonly report: ReportData }
13
+ | { readonly status: "missing" }
14
+ | { readonly status: "invalid"; readonly reason: string };
15
+
16
+ export function readInjectedReportData(): ReportDataState {
17
+ if (typeof window === "undefined") {
18
+ return { status: "missing" };
19
+ }
20
+
21
+ const candidate = window.__GIT_SNITCH_REPORT_DATA__;
22
+
23
+ if (candidate === undefined || typeof candidate === "string") {
24
+ return { status: "missing" };
25
+ }
26
+
27
+ if (isRepoReportData(candidate) || isScanReportData(candidate)) {
28
+ return { status: "ready", report: candidate };
29
+ }
30
+
31
+ if (reportDataDiscriminantSchema.safeParse(candidate).success) {
32
+ return { status: "invalid", reason: "Injected report data is missing required report sections." };
33
+ }
34
+
35
+ return { status: "invalid", reason: "Injected report data does not match the git-snitch report contract." };
36
+ }
37
+
38
+ export function isReadyReportData(state: ReportDataState): state is { readonly status: "ready"; readonly report: ReportData } {
39
+ return state.status === "ready";
40
+ }
41
+
42
+ export function useReportData(): ReportDataState {
43
+ return useMemo(() => readInjectedReportData(), []);
44
+ }
45
+
46
+ export function useIsRepoReport(report: ReportData | null | undefined): report is RepoReportData {
47
+ return Boolean(report && report.kind === "repo");
48
+ }
49
+
50
+ export function useIsScanReport(report: ReportData | null | undefined): report is ScanReportData {
51
+ return Boolean(report && report.kind === "scan");
52
+ }
@@ -0,0 +1,31 @@
1
+ import { buttonVariants } from "@git-snitch/ui/components/button";
2
+ import { Card, CardContent } from "@git-snitch/ui/components/card";
3
+ import type { ReactNode } from "react";
4
+
5
+ type EmptyStateProps = {
6
+ readonly title: string;
7
+ readonly description: string;
8
+ readonly action?: ReactNode;
9
+ };
10
+
11
+ export function EmptyState({ title, description, action }: EmptyStateProps) {
12
+ return (
13
+ <Card className="border-dashed bg-card/70 shadow-none">
14
+ <CardContent className="flex flex-col items-start gap-4 py-8 sm:flex-row sm:items-center sm:justify-between">
15
+ <div className="max-w-xl">
16
+ <h2 className="text-base font-semibold tracking-tight text-foreground">{title}</h2>
17
+ <p className="mt-2 text-sm leading-6 text-muted-foreground">{description}</p>
18
+ </div>
19
+ {action ? <div className="shrink-0">{action}</div> : null}
20
+ </CardContent>
21
+ </Card>
22
+ );
23
+ }
24
+
25
+ export function EmptyStateAction({ href, children }: { readonly href: string; readonly children: ReactNode }) {
26
+ return (
27
+ <a href={href} className={buttonVariants({ variant: "outline", size: "sm" })}>
28
+ {children}
29
+ </a>
30
+ );
31
+ }
package/src/export.ts ADDED
@@ -0,0 +1,77 @@
1
+ import type { JsonValue, ReportData } from "@git-snitch/core";
2
+
3
+ export type CsvCell = string | number | boolean | null | undefined;
4
+ export type CsvRow = Readonly<Record<string, CsvCell>>;
5
+
6
+ export type DownloadResult =
7
+ | { readonly status: "downloaded" }
8
+ | { readonly status: "unavailable"; readonly reason: string };
9
+
10
+ const JSON_INDENT = 2;
11
+
12
+ function hasBrowserDownloadApis(): boolean {
13
+ return (
14
+ typeof document !== "undefined" &&
15
+ typeof Blob !== "undefined" &&
16
+ typeof URL !== "undefined" &&
17
+ typeof URL.createObjectURL === "function" &&
18
+ typeof URL.revokeObjectURL === "function"
19
+ );
20
+ }
21
+
22
+ function escapeCsvCell(cell: CsvCell): string {
23
+ if (cell === null || cell === undefined) {
24
+ return "";
25
+ }
26
+
27
+ const value = String(cell);
28
+ const escaped = value.replaceAll('"', '""');
29
+ return /[",\n\r]/.test(escaped) ? `"${escaped}"` : escaped;
30
+ }
31
+
32
+ export function serializeCsv(rows: readonly CsvRow[], columns?: readonly string[]): string {
33
+ const resolvedColumns = columns ?? Array.from(new Set(rows.flatMap((row) => Object.keys(row))));
34
+
35
+ if (resolvedColumns.length === 0) {
36
+ return "";
37
+ }
38
+
39
+ const header = resolvedColumns.map(escapeCsvCell).join(",");
40
+ const body = rows.map((row) => resolvedColumns.map((column) => escapeCsvCell(row[column])).join(","));
41
+ return [header, ...body].join("\n");
42
+ }
43
+
44
+ export function serializeReportJson(report: ReportData | JsonValue): string {
45
+ return JSON.stringify(report, null, JSON_INDENT);
46
+ }
47
+
48
+ export function downloadTextFile(filename: string, content: string, mimeType: string): DownloadResult {
49
+ if (!hasBrowserDownloadApis()) {
50
+ return { status: "unavailable", reason: "Browser download APIs are not available in this environment." };
51
+ }
52
+
53
+ const blob = new Blob([content], { type: mimeType });
54
+ const objectUrl = URL.createObjectURL(blob);
55
+ const anchor = document.createElement("a");
56
+
57
+ anchor.href = objectUrl;
58
+ anchor.download = filename;
59
+ anchor.rel = "noopener";
60
+ anchor.style.display = "none";
61
+ document.body.append(anchor);
62
+ anchor.click();
63
+ anchor.remove();
64
+ URL.revokeObjectURL(objectUrl);
65
+
66
+ return { status: "downloaded" };
67
+ }
68
+
69
+ export function downloadCsv(filename: string, rows: readonly CsvRow[], columns?: readonly string[]): DownloadResult {
70
+ const csv = serializeCsv(rows, columns);
71
+ return downloadTextFile(filename, csv, "text/csv;charset=utf-8");
72
+ }
73
+
74
+ export function downloadJson(filename: string, report: ReportData | JsonValue): DownloadResult {
75
+ const json = serializeReportJson(report);
76
+ return downloadTextFile(filename, json, "application/json;charset=utf-8");
77
+ }
package/src/index.ts ADDED
@@ -0,0 +1,52 @@
1
+ export {
2
+ ActivityHeatmap,
3
+ AdditionsVsDeletionsChart,
4
+ CodeOwnershipChart,
5
+ CommitActivityChart,
6
+ CommitSizeDistributionChart,
7
+ ContributionCalendar,
8
+ ContributorPieChart,
9
+ LanguageDistributionChart,
10
+ ProjectsComparisonChart,
11
+ TimeOfDayChart,
12
+ VelocityChart,
13
+ WeeklyActivityChart,
14
+ deriveActivityHeatmapData,
15
+ deriveAdditionsVsDeletionsData,
16
+ deriveCodeOwnershipData,
17
+ deriveCommitActivityData,
18
+ deriveCommitSizeDistributionData,
19
+ deriveContributionCalendarData,
20
+ deriveContributorPieData,
21
+ deriveLanguageDistributionData,
22
+ deriveProjectsComparisonData,
23
+ deriveTimeOfDayData,
24
+ deriveVelocityData,
25
+ deriveWeeklyActivityData,
26
+ } from "./charts.js";
27
+ export { buildStandaloneReportHtml } from "./build.js";
28
+ export { readInjectedReportData, isReadyReportData, useIsRepoReport, useIsScanReport, useReportData } from "./data.js";
29
+ export { EmptyState, EmptyStateAction } from "./empty-state.js";
30
+ export { downloadCsv, downloadJson, downloadTextFile, serializeCsv, serializeReportJson } from "./export.js";
31
+ export { createInlineHtmlPlugin, inlineHtmlAssets } from "./inline-plugin.js";
32
+ export { AppShell, Header, Navigation, StatsGrid } from "./layout.js";
33
+ export { injectReportDataIntoHtml, REPORT_DATA_PLACEHOLDER, serializeReportDataForHtml } from "./serialization.js";
34
+ export { CommitsTable, ContributorsTable, DataTable, HotspotsTable } from "./tables.js";
35
+ export type {
36
+ ActivityHeatmapCell,
37
+ AdditionsVsDeletionsPoint,
38
+ CodeOwnershipPoint,
39
+ CommitActivityPoint,
40
+ CommitSizeBucket,
41
+ ContributionCalendarDay,
42
+ ContributorPieSlice,
43
+ LanguageDistributionSlice,
44
+ ProjectComparisonPoint,
45
+ TimeOfDayPoint,
46
+ VelocityPoint,
47
+ WeeklyActivityPoint,
48
+ } from "./charts.js";
49
+ export type { CsvCell, CsvRow, DownloadResult } from "./export.js";
50
+ export type { NavigationItem, StatItem } from "./layout.js";
51
+ export type { CommitsTableProps, ContributorsTableProps, DataTableEmptyState, DataTableExport, DataTableProps, HotspotsTableProps } from "./tables.js";
52
+ export type { RouteTemplateOverrides, RouteTemplatePropsById, TemplateComponent, TemplateModule } from "./template.js";
@@ -0,0 +1,123 @@
1
+ import type { Plugin } from "vite";
2
+
3
+ export interface InlineHtmlAsset {
4
+ readonly fileName: string;
5
+ readonly source: string;
6
+ readonly kind: "script" | "style";
7
+ }
8
+
9
+ export interface InlineHtmlResult {
10
+ readonly html: string;
11
+ readonly inlinedFileNames: readonly string[];
12
+ }
13
+
14
+ function normalizeAssetReference(reference: string): string {
15
+ return reference.replace(/^\.\//, "").replace(/^\//, "");
16
+ }
17
+
18
+ function findInlineAsset(assets: readonly InlineHtmlAsset[], reference: string, kind: InlineHtmlAsset["kind"]): InlineHtmlAsset | undefined {
19
+ const normalizedReference = normalizeAssetReference(reference);
20
+
21
+ return assets.find((asset) => asset.kind === kind && asset.fileName === normalizedReference);
22
+ }
23
+
24
+ export function inlineHtmlAssets(html: string, assets: readonly InlineHtmlAsset[]): InlineHtmlResult {
25
+ const inlined = new Set<string>();
26
+ let output = html.replace(/<link\s+[^>]*rel=["']modulepreload["'][^>]*>/g, "");
27
+
28
+ output = output.replace(/<script\s+([^>]*?)src=["']([^"']+)["']([^>]*)><\/script>/g, (tag, beforeAttributes, source, afterAttributes) => {
29
+ const asset = findInlineAsset(assets, source, "script");
30
+
31
+ if (!asset) {
32
+ return tag;
33
+ }
34
+
35
+ inlined.add(asset.fileName);
36
+ const attributes = `${beforeAttributes} ${afterAttributes}`.replace(/\s*crossorigin(?:=["'][^"']*["'])?/, "").trim();
37
+ const attributeText = attributes.length > 0 ? ` ${attributes}` : "";
38
+
39
+ return `<script${attributeText}>\n${asset.source}\n</script>`;
40
+ });
41
+
42
+ output = output.replace(/<link\s+([^>]*?)rel=["']stylesheet["']([^>]*?)href=["']([^"']+)["']([^>]*)>/g, (tag, beforeAttributes, middleAttributes, href) => {
43
+ const asset = findInlineAsset(assets, href, "style");
44
+
45
+ if (!asset) {
46
+ return tag;
47
+ }
48
+
49
+ inlined.add(asset.fileName);
50
+ const mediaMatch = `${beforeAttributes} ${middleAttributes}`.match(/\smedia=["']([^"']+)["']/);
51
+ const mediaAttribute = mediaMatch ? ` media="${mediaMatch[1]}"` : "";
52
+
53
+ return `<style${mediaAttribute}>\n${asset.source}\n</style>`;
54
+ });
55
+
56
+ output = output.replace(/<link\s+([^>]*?)href=["']([^"']+)["']([^>]*?)rel=["']stylesheet["']([^>]*)>/g, (tag, beforeAttributes, href, middleAttributes) => {
57
+ const asset = findInlineAsset(assets, href, "style");
58
+
59
+ if (!asset) {
60
+ return tag;
61
+ }
62
+
63
+ inlined.add(asset.fileName);
64
+ const mediaMatch = `${beforeAttributes} ${middleAttributes}`.match(/\smedia=["']([^"']+)["']/);
65
+ const mediaAttribute = mediaMatch ? ` media="${mediaMatch[1]}"` : "";
66
+
67
+ return `<style${mediaAttribute}>\n${asset.source}\n</style>`;
68
+ });
69
+
70
+ return { html: output, inlinedFileNames: [...inlined] };
71
+ }
72
+
73
+ function sourceToString(source: string | Uint8Array): string {
74
+ return typeof source === "string" ? source : new TextDecoder().decode(source);
75
+ }
76
+
77
+ export function createInlineHtmlPlugin(): Plugin {
78
+ return {
79
+ name: "git-snitch-inline-html",
80
+ enforce: "post",
81
+ generateBundle(_options, bundle) {
82
+ type BundleAsset = Extract<(typeof bundle)[string], { type: "asset" }>;
83
+
84
+ const htmlEntries: { readonly fileName: string; readonly asset: BundleAsset }[] = [];
85
+ const assets: InlineHtmlAsset[] = [];
86
+
87
+ for (const [fileName, entry] of Object.entries(bundle)) {
88
+ if (entry.type === "chunk") {
89
+ assets.push({ fileName: entry.fileName, source: entry.code, kind: "script" });
90
+ }
91
+
92
+ if (entry.type === "asset" && entry.fileName.endsWith(".css")) {
93
+ assets.push({ fileName: entry.fileName, source: sourceToString(entry.source), kind: "style" });
94
+ }
95
+
96
+ if (entry.type === "asset" && entry.fileName.endsWith(".html")) {
97
+ htmlEntries.push({ fileName, asset: entry });
98
+ }
99
+ }
100
+
101
+ if (htmlEntries.length !== 1) {
102
+ throw new Error(`Expected exactly one HTML asset to inline, found ${htmlEntries.length}.`);
103
+ }
104
+
105
+ const htmlEntry = htmlEntries[0];
106
+
107
+ if (!htmlEntry) {
108
+ throw new Error("Expected one HTML asset to inline, but none was available after validation.");
109
+ }
110
+
111
+ const result = inlineHtmlAssets(sourceToString(htmlEntry.asset.source), assets);
112
+ htmlEntry.asset.source = result.html;
113
+
114
+ for (const fileName of result.inlinedFileNames) {
115
+ delete bundle[fileName];
116
+ }
117
+
118
+ if (htmlEntry.fileName !== "report-template.html") {
119
+ throw new Error(`Expected Vite to emit report-template.html, received ${htmlEntry.fileName}.`);
120
+ }
121
+ },
122
+ };
123
+ }
package/src/layout.tsx ADDED
@@ -0,0 +1,152 @@
1
+ import { buttonVariants } from "@git-snitch/ui/components/button";
2
+ import { Card, CardContent, CardHeader, CardTitle } from "@git-snitch/ui/components/card";
3
+ import { cn } from "@git-snitch/ui/lib/utils";
4
+ import type { ReactNode } from "react";
5
+
6
+ import { EmptyState } from "./empty-state.js";
7
+ import { ThemeToggle } from "./theme-toggle.js";
8
+
9
+ export type NavigationItem = {
10
+ readonly label: string;
11
+ readonly href: string;
12
+ readonly current?: boolean;
13
+ readonly disabled?: boolean;
14
+ };
15
+
16
+ export type StatItem = {
17
+ readonly label: string;
18
+ readonly value: string | number;
19
+ readonly description?: string;
20
+ };
21
+
22
+ type HeaderProps = {
23
+ readonly title: string;
24
+ readonly titleHref?: string;
25
+ readonly eyebrow?: string;
26
+ readonly description?: string;
27
+ readonly actions?: ReactNode;
28
+ };
29
+
30
+ type NavigationProps = {
31
+ readonly items: readonly NavigationItem[];
32
+ readonly label?: string;
33
+ };
34
+
35
+ type StatsGridProps = {
36
+ readonly stats: readonly StatItem[];
37
+ readonly emptyTitle?: string;
38
+ readonly emptyDescription?: string;
39
+ };
40
+
41
+ type AppShellProps = {
42
+ readonly title: string;
43
+ readonly titleHref?: string;
44
+ readonly eyebrow?: string;
45
+ readonly description?: string;
46
+ readonly navigationItems?: readonly NavigationItem[];
47
+ readonly headerActions?: ReactNode;
48
+ readonly children: ReactNode;
49
+ };
50
+
51
+ export function Header({ title, titleHref, eyebrow, description, actions }: HeaderProps) {
52
+ const titleContent = <h1 className="mt-3 text-3xl font-semibold tracking-tight text-foreground sm:text-4xl">{title}</h1>;
53
+
54
+ return (
55
+ <header className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/75">
56
+ <div className="mx-auto flex w-full max-w-7xl flex-col gap-6 px-5 py-8 sm:px-8 lg:flex-row lg:items-end lg:justify-between">
57
+ <div className="max-w-4xl">
58
+ {eyebrow ? <p className="text-xs font-medium uppercase tracking-[0.24em] text-muted-foreground">{eyebrow}</p> : null}
59
+ {titleHref ? (
60
+ <a href={titleHref} target="_blank" rel="noopener noreferrer" className="hover:underline">
61
+ {titleContent}
62
+ </a>
63
+ ) : (
64
+ titleContent
65
+ )}
66
+ {description ? <p className="mt-3 max-w-2xl text-sm leading-6 text-muted-foreground">{description}</p> : null}
67
+ </div>
68
+ <div className="flex flex-wrap items-center gap-2">
69
+ {actions}
70
+ <ThemeToggle />
71
+ </div>
72
+ </div>
73
+ </header>
74
+ );
75
+ }
76
+
77
+ export function Navigation({ items, label = "Report sections" }: NavigationProps) {
78
+ if (items.length === 0) {
79
+ return null;
80
+ }
81
+
82
+ return (
83
+ <nav aria-label={label} className="border-b bg-background/80">
84
+ <div className="mx-auto flex w-full max-w-7xl gap-2 overflow-x-auto px-5 py-3 sm:px-8">
85
+ {items.map((item) =>
86
+ item.disabled ? (
87
+ <span
88
+ key={item.href}
89
+ aria-disabled="true"
90
+ className="inline-flex h-8 shrink-0 items-center border border-transparent px-3 text-xs font-medium text-muted-foreground/60"
91
+ >
92
+ {item.label}
93
+ </span>
94
+ ) : (
95
+ <a
96
+ key={item.href}
97
+ href={item.href}
98
+ aria-current={item.current ? "page" : undefined}
99
+ className={buttonVariants({ variant: item.current ? "secondary" : "ghost", size: "sm", className: "shrink-0" })}
100
+ >
101
+ {item.label}
102
+ </a>
103
+ ),
104
+ )}
105
+ </div>
106
+ </nav>
107
+ );
108
+ }
109
+
110
+ export function StatsGrid({
111
+ stats,
112
+ emptyTitle = "No report statistics yet",
113
+ emptyDescription = "This report does not include enough data for summary statistics.",
114
+ }: StatsGridProps) {
115
+ if (stats.length === 0) {
116
+ return <EmptyState title={emptyTitle} description={emptyDescription} />;
117
+ }
118
+
119
+ return (
120
+ <section aria-label="Report summary statistics" className="grid grid-flow-dense gap-3 sm:grid-cols-2 lg:grid-cols-4">
121
+ {stats.map((stat) => (
122
+ <Card key={stat.label} className="min-h-32 shadow-none">
123
+ <CardHeader>
124
+ <CardTitle className="text-xs font-medium uppercase tracking-[0.18em] text-muted-foreground">{stat.label}</CardTitle>
125
+ </CardHeader>
126
+ <CardContent>
127
+ <p className="text-3xl font-semibold tracking-tight text-foreground">{stat.value}</p>
128
+ {stat.description ? <p className="mt-2 text-xs leading-5 text-muted-foreground">{stat.description}</p> : null}
129
+ </CardContent>
130
+ </Card>
131
+ ))}
132
+ </section>
133
+ );
134
+ }
135
+
136
+ export function AppShell({
137
+ title,
138
+ titleHref,
139
+ eyebrow,
140
+ description,
141
+ navigationItems = [],
142
+ headerActions,
143
+ children,
144
+ }: AppShellProps) {
145
+ return (
146
+ <main className="min-h-screen w-full max-w-full overflow-x-hidden bg-background text-foreground transition-colors">
147
+ <Header title={title} titleHref={titleHref} eyebrow={eyebrow} description={description} actions={headerActions} />
148
+ <Navigation items={navigationItems} />
149
+ <div className={cn("mx-auto flex w-full max-w-7xl flex-col gap-8 px-5 py-8 sm:px-8 sm:py-10")}>{children}</div>
150
+ </main>
151
+ );
152
+ }
package/src/main.tsx ADDED
@@ -0,0 +1,17 @@
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+
4
+ import "./styles.css";
5
+ import { App } from "./app.js";
6
+
7
+ const rootElement = document.getElementById("root");
8
+
9
+ if (!rootElement) {
10
+ throw new Error("Renderer root element #root was not found in the report template.");
11
+ }
12
+
13
+ createRoot(rootElement).render(
14
+ <StrictMode>
15
+ <App />
16
+ </StrictMode>,
17
+ );