@olympusoss/canvas 2.6.19

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 (128) hide show
  1. package/package.json +179 -0
  2. package/src/components/atoms/README.md +11 -0
  3. package/src/components/atoms/aspect-ratio.tsx +32 -0
  4. package/src/components/atoms/avatar.tsx +98 -0
  5. package/src/components/atoms/badge.tsx +44 -0
  6. package/src/components/atoms/brand-mark.tsx +74 -0
  7. package/src/components/atoms/button.tsx +104 -0
  8. package/src/components/atoms/checkbox.tsx +63 -0
  9. package/src/components/atoms/flex-box.tsx +105 -0
  10. package/src/components/atoms/icon.tsx +34 -0
  11. package/src/components/atoms/input.tsx +91 -0
  12. package/src/components/atoms/label.tsx +41 -0
  13. package/src/components/atoms/logo.tsx +89 -0
  14. package/src/components/atoms/progress.tsx +55 -0
  15. package/src/components/atoms/radio-group.tsx +122 -0
  16. package/src/components/atoms/scroll-area.tsx +106 -0
  17. package/src/components/atoms/section.tsx +48 -0
  18. package/src/components/atoms/separator.tsx +45 -0
  19. package/src/components/atoms/skeleton.tsx +17 -0
  20. package/src/components/atoms/slider.tsx +93 -0
  21. package/src/components/atoms/switch.tsx +60 -0
  22. package/src/components/atoms/textarea.tsx +78 -0
  23. package/src/components/atoms/toggle.tsx +80 -0
  24. package/src/components/charts/activity-heatmap.tsx +96 -0
  25. package/src/components/charts/axes.tsx +21 -0
  26. package/src/components/charts/chart-container.tsx +195 -0
  27. package/src/components/charts/chart-legend.tsx +67 -0
  28. package/src/components/charts/chart-tooltip.tsx +161 -0
  29. package/src/components/charts/chart-types.tsx +49 -0
  30. package/src/components/charts/containers.tsx +11 -0
  31. package/src/components/charts/data.tsx +16 -0
  32. package/src/components/charts/details.tsx +25 -0
  33. package/src/components/charts/gauge.tsx +106 -0
  34. package/src/components/charts/grids.tsx +8 -0
  35. package/src/components/charts/index.ts +62 -0
  36. package/src/components/charts/labeled-bar-list.tsx +85 -0
  37. package/src/components/charts/references.tsx +8 -0
  38. package/src/components/charts/service-health-list.tsx +73 -0
  39. package/src/components/charts/sparkline.tsx +52 -0
  40. package/src/components/charts/stacked-bar.tsx +104 -0
  41. package/src/components/charts/text.tsx +10 -0
  42. package/src/components/charts/world-heat-map-inner.tsx +317 -0
  43. package/src/components/charts/world-heat-map.tsx +184 -0
  44. package/src/components/molecules/README.md +12 -0
  45. package/src/components/molecules/action-bar.tsx +73 -0
  46. package/src/components/molecules/activity-item.tsx +74 -0
  47. package/src/components/molecules/alert.tsx +80 -0
  48. package/src/components/molecules/animated-background.tsx +92 -0
  49. package/src/components/molecules/brand-lockup.tsx +48 -0
  50. package/src/components/molecules/breadcrumb.tsx +161 -0
  51. package/src/components/molecules/button-group.tsx +104 -0
  52. package/src/components/molecules/calendar.tsx +216 -0
  53. package/src/components/molecules/card.tsx +101 -0
  54. package/src/components/molecules/code-block.tsx +48 -0
  55. package/src/components/molecules/empty-state.tsx +55 -0
  56. package/src/components/molecules/error-state.tsx +42 -0
  57. package/src/components/molecules/field-display.tsx +35 -0
  58. package/src/components/molecules/input-otp.tsx +74 -0
  59. package/src/components/molecules/loading-state.tsx +36 -0
  60. package/src/components/molecules/notification-item.tsx +67 -0
  61. package/src/components/molecules/notification-list.tsx +45 -0
  62. package/src/components/molecules/number-badge.tsx +53 -0
  63. package/src/components/molecules/page-header.tsx +88 -0
  64. package/src/components/molecules/page-tabs.tsx +94 -0
  65. package/src/components/molecules/pagination.tsx +150 -0
  66. package/src/components/molecules/phone-input.tsx +200 -0
  67. package/src/components/molecules/search-bar.tsx +64 -0
  68. package/src/components/molecules/secret-field.tsx +158 -0
  69. package/src/components/molecules/section-card.tsx +91 -0
  70. package/src/components/molecules/stat-card.tsx +96 -0
  71. package/src/components/molecules/status-badge.tsx +42 -0
  72. package/src/components/molecules/stepper.tsx +96 -0
  73. package/src/components/molecules/table.tsx +157 -0
  74. package/src/components/molecules/toggle-group.tsx +145 -0
  75. package/src/components/molecules/tooltip.tsx +150 -0
  76. package/src/components/molecules/user-avatar-chip.tsx +71 -0
  77. package/src/components/organisms/README.md +14 -0
  78. package/src/components/organisms/accordion.tsx +149 -0
  79. package/src/components/organisms/alert-dialog.tsx +269 -0
  80. package/src/components/organisms/carousel.tsx +244 -0
  81. package/src/components/organisms/collapsible.tsx +69 -0
  82. package/src/components/organisms/command.tsx +143 -0
  83. package/src/components/organisms/context-menu.tsx +333 -0
  84. package/src/components/organisms/dashboard-grid.tsx +360 -0
  85. package/src/components/organisms/data-table.tsx +330 -0
  86. package/src/components/organisms/dialog.tsx +304 -0
  87. package/src/components/organisms/drawer.tsx +100 -0
  88. package/src/components/organisms/dropdown-menu.tsx +434 -0
  89. package/src/components/organisms/editors/code-editor.tsx +144 -0
  90. package/src/components/organisms/editors/index.ts +4 -0
  91. package/src/components/organisms/editors/markdown-editor.tsx +153 -0
  92. package/src/components/organisms/editors/markdown-renderer.ts +27 -0
  93. package/src/components/organisms/editors/prose-canvas-classes.ts +45 -0
  94. package/src/components/organisms/editors/rich-text-editor.tsx +126 -0
  95. package/src/components/organisms/editors/toolbar/md-toolbar.tsx +129 -0
  96. package/src/components/organisms/editors/toolbar/rte-toolbar.tsx +211 -0
  97. package/src/components/organisms/editors/toolbar/toolbar-shell.tsx +45 -0
  98. package/src/components/organisms/editors/use-codemirror-theme.ts +61 -0
  99. package/src/components/organisms/error-boundary.tsx +61 -0
  100. package/src/components/organisms/form.tsx +174 -0
  101. package/src/components/organisms/hover-card.tsx +114 -0
  102. package/src/components/organisms/menubar.tsx +491 -0
  103. package/src/components/organisms/navbar.tsx +101 -0
  104. package/src/components/organisms/navigation-menu.tsx +234 -0
  105. package/src/components/organisms/popover.tsx +144 -0
  106. package/src/components/organisms/resizable.tsx +39 -0
  107. package/src/components/organisms/schema-form.tsx +232 -0
  108. package/src/components/organisms/select.tsx +303 -0
  109. package/src/components/organisms/sheet.tsx +256 -0
  110. package/src/components/organisms/sidebar.tsx +1037 -0
  111. package/src/components/organisms/sonner.tsx +96 -0
  112. package/src/components/organisms/tabs.tsx +132 -0
  113. package/src/components/organisms/theme-provider.tsx +101 -0
  114. package/src/hooks/use-mobile.tsx +19 -0
  115. package/src/index.ts +547 -0
  116. package/src/lib/portal-container.tsx +35 -0
  117. package/src/lib/utils.ts +6 -0
  118. package/src/native.ts +23 -0
  119. package/src/tokens/colors.ts +91 -0
  120. package/src/tokens/index.ts +3 -0
  121. package/src/tokens/spacing.ts +55 -0
  122. package/src/tokens/typography.ts +27 -0
  123. package/styles/canvas.css +55 -0
  124. package/styles/dashboard-grid.css +47 -0
  125. package/styles/leaflet.css +13 -0
  126. package/styles/tokens.css +234 -0
  127. package/tailwind.config.ts +70 -0
  128. package/tsconfig.json +23 -0
@@ -0,0 +1,45 @@
1
+ "use client";
2
+
3
+ import * as SeparatorPrimitive from "@radix-ui/react-separator";
4
+ import * as React from "react";
5
+
6
+ import { cn } from "../../lib/utils";
7
+
8
+ export interface SeparatorProps
9
+ extends React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root> {
10
+ /**
11
+ * Layout direction. `horizontal` is a 1px row spanning width;
12
+ * `vertical` is a 1px column spanning height.
13
+ * @default "horizontal"
14
+ */
15
+ orientation?: "horizontal" | "vertical";
16
+ /**
17
+ * When true (default), the separator is hidden from screen readers. Set
18
+ * to false for separators that have semantic meaning.
19
+ * @default true
20
+ */
21
+ decorative?: boolean;
22
+ asChild?: boolean;
23
+ className?: string;
24
+ }
25
+
26
+ /** Thin divider line — horizontal (default) or vertical. */
27
+ const Separator = React.forwardRef<
28
+ React.ElementRef<typeof SeparatorPrimitive.Root>,
29
+ SeparatorProps
30
+ >(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
31
+ <SeparatorPrimitive.Root
32
+ ref={ref}
33
+ decorative={decorative}
34
+ orientation={orientation}
35
+ className={cn(
36
+ "shrink-0 bg-border",
37
+ orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
38
+ className,
39
+ )}
40
+ {...props}
41
+ />
42
+ ));
43
+ Separator.displayName = SeparatorPrimitive.Root.displayName;
44
+
45
+ export { Separator };
@@ -0,0 +1,17 @@
1
+ import type * as React from "react";
2
+
3
+ import { cn } from "../../lib/utils";
4
+
5
+ export interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {
6
+ className?: string;
7
+ }
8
+
9
+ /**
10
+ * Animated placeholder for content that's loading. Set width/height via
11
+ * className; Skeleton fills with a pulsing tinted block.
12
+ */
13
+ function Skeleton({ className, ...props }: SkeletonProps) {
14
+ return <div className={cn("animate-pulse rounded-md bg-primary/10", className)} {...props} />;
15
+ }
16
+
17
+ export { Skeleton };
@@ -0,0 +1,93 @@
1
+ "use client";
2
+
3
+ import * as SliderPrimitive from "@radix-ui/react-slider";
4
+ import * as React from "react";
5
+
6
+ import { cn } from "../../lib/utils";
7
+
8
+ export interface SliderProps extends React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> {
9
+ /**
10
+ * Controlled value. Pass an array — single-thumb sliders use a length-1
11
+ * tuple (`[40]`), range sliders use length-2 (`[20, 80]`). Pair with
12
+ * `onValueChange`.
13
+ */
14
+ value?: number[];
15
+ /**
16
+ * Initial value for uncontrolled usage. Same array shape as `value`.
17
+ * @default [0]
18
+ */
19
+ defaultValue?: number[];
20
+ /** Fires on every move. Use this for live previews. */
21
+ onValueChange?: (value: number[]) => void;
22
+ /** Fires when the user releases the thumb (commit handler). */
23
+ onValueCommit?: (value: number[]) => void;
24
+ /**
25
+ * Minimum value the slider can reach.
26
+ * @default 0
27
+ */
28
+ min?: number;
29
+ /**
30
+ * Maximum value the slider can reach.
31
+ * @default 100
32
+ */
33
+ max?: number;
34
+ /**
35
+ * Quantum the slider snaps to. Set to `1` for integer-only values.
36
+ * @default 1
37
+ */
38
+ step?: number;
39
+ /**
40
+ * Minimum gap between two thumbs in a range slider. Ignored for
41
+ * single-thumb sliders.
42
+ * @default 0
43
+ */
44
+ minStepsBetweenThumbs?: number;
45
+ /**
46
+ * Layout direction.
47
+ * @default "horizontal"
48
+ */
49
+ orientation?: "horizontal" | "vertical";
50
+ /**
51
+ * Reading direction. Affects which end is "min" on the visual axis.
52
+ * @default "ltr"
53
+ */
54
+ dir?: "ltr" | "rtl";
55
+ /**
56
+ * Whether the slider is inverted — flips which end is "min".
57
+ * @default false
58
+ */
59
+ inverted?: boolean;
60
+ /**
61
+ * Disable the slider.
62
+ * @default false
63
+ */
64
+ disabled?: boolean;
65
+ /** Form field name. Required when relying on uncontrolled native form submission. */
66
+ name?: string;
67
+ /**
68
+ * Render as a Radix Slot — forwards props onto the immediate child
69
+ * element instead of rendering a wrapper `<span>`.
70
+ * @default false
71
+ */
72
+ asChild?: boolean;
73
+ /** Tailwind / CSS classes merged onto the slider via `cn()`. */
74
+ className?: string;
75
+ }
76
+
77
+ const Slider = React.forwardRef<React.ElementRef<typeof SliderPrimitive.Root>, SliderProps>(
78
+ ({ className, ...props }, ref) => (
79
+ <SliderPrimitive.Root
80
+ ref={ref}
81
+ className={cn("relative flex w-full touch-none select-none items-center", className)}
82
+ {...props}
83
+ >
84
+ <SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20">
85
+ <SliderPrimitive.Range className="absolute h-full bg-primary" />
86
+ </SliderPrimitive.Track>
87
+ <SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
88
+ </SliderPrimitive.Root>
89
+ ),
90
+ );
91
+ Slider.displayName = SliderPrimitive.Root.displayName;
92
+
93
+ export { Slider };
@@ -0,0 +1,60 @@
1
+ "use client";
2
+
3
+ import * as SwitchPrimitives from "@radix-ui/react-switch";
4
+ import * as React from "react";
5
+
6
+ import { cn } from "../../lib/utils";
7
+
8
+ export interface SwitchProps extends React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> {
9
+ /** Controlled state. */
10
+ checked?: boolean;
11
+ /**
12
+ * Initial state for uncontrolled usage.
13
+ * @default false
14
+ */
15
+ defaultChecked?: boolean;
16
+ /** Fires when the user flips the switch. */
17
+ onCheckedChange?: (checked: boolean) => void;
18
+ /**
19
+ * Dims and blocks input.
20
+ * @default false
21
+ */
22
+ disabled?: boolean;
23
+ /**
24
+ * Block native form submit unless the switch is on.
25
+ * @default false
26
+ */
27
+ required?: boolean;
28
+ /** Form field name for native form submission. */
29
+ name?: string;
30
+ /** Form field value when submitted. */
31
+ value?: string;
32
+ asChild?: boolean;
33
+ className?: string;
34
+ }
35
+
36
+ /**
37
+ * Wraps Radix's `Switch.Root` — a binary on/off toggle. Pair with `<Label>`
38
+ * for accessibility.
39
+ */
40
+ const Switch = React.forwardRef<React.ElementRef<typeof SwitchPrimitives.Root>, SwitchProps>(
41
+ ({ className, ...props }, ref) => (
42
+ <SwitchPrimitives.Root
43
+ className={cn(
44
+ "peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
45
+ className,
46
+ )}
47
+ {...props}
48
+ ref={ref}
49
+ >
50
+ <SwitchPrimitives.Thumb
51
+ className={cn(
52
+ "pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0",
53
+ )}
54
+ />
55
+ </SwitchPrimitives.Root>
56
+ ),
57
+ );
58
+ Switch.displayName = SwitchPrimitives.Root.displayName;
59
+
60
+ export { Switch };
@@ -0,0 +1,78 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "../../lib/utils";
4
+
5
+ export interface TextareaProps extends React.ComponentProps<"textarea"> {
6
+ /** Controlled value. Pair with `onChange`. */
7
+ value?: string | number | readonly string[];
8
+ /** Uncontrolled initial value. */
9
+ defaultValue?: string | number | readonly string[];
10
+ /** Hint text shown when empty. */
11
+ placeholder?: string;
12
+ /**
13
+ * Visible row count. Canvas's default min-height is 60px regardless;
14
+ * `rows` overrides.
15
+ */
16
+ rows?: number;
17
+ /** Visible column count (width). Canvas overrides this with `w-full`. */
18
+ cols?: number;
19
+ /**
20
+ * Dim and block input.
21
+ * @default false
22
+ */
23
+ disabled?: boolean;
24
+ /**
25
+ * Selectable but not editable.
26
+ * @default false
27
+ */
28
+ readOnly?: boolean;
29
+ /**
30
+ * Block native form submit unless the textarea has a value.
31
+ * @default false
32
+ */
33
+ required?: boolean;
34
+ /** Form field name for native form submission. */
35
+ name?: string;
36
+ /** DOM id for `<Label htmlFor>` association. */
37
+ id?: string;
38
+ /** Browser autofill hint. */
39
+ autoComplete?: string;
40
+ /**
41
+ * Focus on mount.
42
+ * @default false
43
+ */
44
+ autoFocus?: boolean;
45
+ /** Character count upper limit. */
46
+ maxLength?: number;
47
+ /** Character count lower limit. */
48
+ minLength?: number;
49
+ /** Fires on every keystroke. */
50
+ onChange?: React.ChangeEventHandler<HTMLTextAreaElement>;
51
+ /** Fires on focus. */
52
+ onFocus?: React.FocusEventHandler<HTMLTextAreaElement>;
53
+ /** Fires on blur. */
54
+ onBlur?: React.FocusEventHandler<HTMLTextAreaElement>;
55
+ className?: string;
56
+ }
57
+
58
+ /**
59
+ * Styled wrapper around the native `<textarea>`. 60px min-height, rounded-md,
60
+ * 1px border, transparent background.
61
+ */
62
+ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
63
+ ({ className, ...props }, ref) => {
64
+ return (
65
+ <textarea
66
+ className={cn(
67
+ "flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
68
+ className,
69
+ )}
70
+ ref={ref}
71
+ {...props}
72
+ />
73
+ );
74
+ },
75
+ );
76
+ Textarea.displayName = "Textarea";
77
+
78
+ export { Textarea };
@@ -0,0 +1,80 @@
1
+ "use client";
2
+
3
+ import * as TogglePrimitive from "@radix-ui/react-toggle";
4
+ import { cva, type VariantProps } from "class-variance-authority";
5
+ import * as React from "react";
6
+
7
+ import { cn } from "../../lib/utils";
8
+
9
+ const toggleVariants = cva(
10
+ "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-brand data-[state=on]:text-brand-foreground data-[state=on]:hover:bg-brand/90 data-[state=on]:hover:text-brand-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
11
+ {
12
+ variants: {
13
+ variant: {
14
+ default: "bg-transparent",
15
+ outline:
16
+ "border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
17
+ },
18
+ size: {
19
+ default: "h-9 px-2 min-w-9",
20
+ sm: "h-8 px-1.5 min-w-8",
21
+ lg: "h-10 px-2.5 min-w-10",
22
+ },
23
+ },
24
+ defaultVariants: {
25
+ variant: "default",
26
+ size: "default",
27
+ },
28
+ },
29
+ );
30
+
31
+ export interface ToggleProps
32
+ extends React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root>,
33
+ VariantProps<typeof toggleVariants> {
34
+ /**
35
+ * `default` (filled when on) or `outline` (bordered).
36
+ * @default "default"
37
+ */
38
+ variant?: "default" | "outline";
39
+ /**
40
+ * Square size — `sm` (32px), `default` (36px), or `lg` (40px).
41
+ * @default "default"
42
+ */
43
+ size?: "default" | "sm" | "lg";
44
+ /** Controlled pressed state. Pair with `onPressedChange`. */
45
+ pressed?: boolean;
46
+ /**
47
+ * Initial pressed state for uncontrolled usage.
48
+ * @default false
49
+ */
50
+ defaultPressed?: boolean;
51
+ /** Fires when the user toggles the pressed state. */
52
+ onPressedChange?: (pressed: boolean) => void;
53
+ /**
54
+ * Disable the toggle.
55
+ * @default false
56
+ */
57
+ disabled?: boolean;
58
+ /**
59
+ * Render as a Radix Slot.
60
+ * @default false
61
+ */
62
+ asChild?: boolean;
63
+ /** Toggle content (typically an icon or short label). */
64
+ children?: React.ReactNode;
65
+ className?: string;
66
+ }
67
+
68
+ const Toggle = React.forwardRef<React.ElementRef<typeof TogglePrimitive.Root>, ToggleProps>(
69
+ ({ className, variant, size, ...props }, ref) => (
70
+ <TogglePrimitive.Root
71
+ ref={ref}
72
+ className={cn(toggleVariants({ variant, size, className }))}
73
+ {...props}
74
+ />
75
+ ),
76
+ );
77
+
78
+ Toggle.displayName = TogglePrimitive.Root.displayName;
79
+
80
+ export { Toggle, toggleVariants };
@@ -0,0 +1,96 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "../../lib/utils";
4
+
5
+ export interface ActivityHeatmapProps extends React.HTMLAttributes<HTMLDivElement> {
6
+ /**
7
+ * Cell values in row-major order. Each entry is a number in `[0, 1]`
8
+ * where `0` paints the lowest tint and `1` paints the highest. Values are
9
+ * clamped on render. The grid dimensions come from `data.length` and
10
+ * `data[0].length`; pass jagged arrays only if you accept short rows.
11
+ */
12
+ data: number[][];
13
+ /**
14
+ * CSS variable name (without leading `--`) used for the cell hue. Default
15
+ * `chart-1`. Cells render as `hsl(var(--{colorVar}) / opacity)`.
16
+ */
17
+ colorVar?: string;
18
+ /** Pixel height of each cell row. Default `14`. */
19
+ cellHeight?: number;
20
+ /** Pixel gap between cells. Default `2`. */
21
+ gap?: number;
22
+ /** Pixel border-radius on each cell. Default `3`. */
23
+ cellRadius?: number;
24
+ /**
25
+ * Render the cell `title` attribute (browser tooltip on hover) for each
26
+ * coordinate. Receives `(rowIndex, colIndex, value)` and should return a
27
+ * string. Returns nothing → no title set.
28
+ */
29
+ cellTitle?: (row: number, col: number, value: number) => string | undefined;
30
+ }
31
+
32
+ /**
33
+ * CSS-grid heatmap of opacity-tinted cells. Useful for time-of-day × day-of-week
34
+ * matrices (token issuance, sign-in concentration, queue depth) where a full
35
+ * chart would be overkill. Rendering is a flat `display: grid` — no canvas, no
36
+ * SVG, fully interactive via hover titles.
37
+ */
38
+ export const ActivityHeatmap = React.forwardRef<HTMLDivElement, ActivityHeatmapProps>(
39
+ (
40
+ {
41
+ data,
42
+ colorVar = "chart-1",
43
+ cellHeight = 14,
44
+ gap = 2,
45
+ cellRadius = 3,
46
+ cellTitle,
47
+ className,
48
+ ...props
49
+ },
50
+ ref,
51
+ ) => {
52
+ const cols = data[0]?.length ?? 0;
53
+ return (
54
+ <div
55
+ ref={ref}
56
+ className={cn("w-full", className)}
57
+ style={{
58
+ display: "grid",
59
+ gridTemplateRows: `repeat(${data.length}, ${cellHeight}px)`,
60
+ gap,
61
+ }}
62
+ {...props}
63
+ >
64
+ {data.map((row, r) => (
65
+ <div
66
+ key={`r-${r}`}
67
+ style={{
68
+ display: "grid",
69
+ gridTemplateColumns: `repeat(${cols}, 1fr)`,
70
+ gap,
71
+ }}
72
+ >
73
+ {row.map((raw, c) => {
74
+ const v = Math.max(0, Math.min(1, raw));
75
+ const opacity = 0.08 + v * 0.85;
76
+ const title = cellTitle?.(r, c, v);
77
+ return (
78
+ <div
79
+ key={`c-${r}-${c}`}
80
+ data-cell=""
81
+ title={title}
82
+ aria-hidden
83
+ style={{
84
+ borderRadius: cellRadius,
85
+ background: `hsl(var(--${colorVar}) / ${opacity})`,
86
+ }}
87
+ />
88
+ );
89
+ })}
90
+ </div>
91
+ ))}
92
+ </div>
93
+ );
94
+ },
95
+ );
96
+ ActivityHeatmap.displayName = "ActivityHeatmap";
@@ -0,0 +1,21 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Axis re-exports — pure pass-throughs of the Recharts primitives. Wrapping
5
+ * them in function components breaks Recharts' `findAllByType` introspection
6
+ * (chart types like LineChart look for `child.type === XAxis` to detect axes;
7
+ * our wrapper would have a different type, so the chart can't find them and
8
+ * crashes with "xAxisId requires a corresponding xAxisId on the targeted
9
+ * graphical component" errors).
10
+ *
11
+ * Theming is applied via CSS selectors on the `<ChartContainer>` wrapping div
12
+ * (`[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground`, etc.).
13
+ */
14
+ export {
15
+ CartesianAxis,
16
+ PolarAngleAxis,
17
+ PolarRadiusAxis,
18
+ XAxis,
19
+ YAxis,
20
+ ZAxis,
21
+ } from "recharts";
@@ -0,0 +1,195 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as RechartsPrimitive from "recharts";
5
+
6
+ import { cn } from "../../lib/utils";
7
+
8
+ // Format: { THEME_NAME: CSS_SELECTOR }
9
+ const THEMES = { light: "", dark: ".dark" } as const;
10
+
11
+ /**
12
+ * Recharts data primitives that should auto-cycle through `--chart-N`
13
+ * colours when the consumer didn't pass an explicit `fill` / `stroke`.
14
+ *
15
+ * We compare by reference (the actual class component from `recharts`) — the
16
+ * deep-clone walker uses `child.type === T` to detect them. This works because
17
+ * canvas re-exports these symbols from `recharts` directly (see `data.tsx`).
18
+ */
19
+ const PALETTE_TARGETS = new Set<unknown>([
20
+ RechartsPrimitive.Line,
21
+ RechartsPrimitive.Bar,
22
+ RechartsPrimitive.Area,
23
+ RechartsPrimitive.Pie,
24
+ RechartsPrimitive.Scatter,
25
+ RechartsPrimitive.Radar,
26
+ RechartsPrimitive.RadialBar,
27
+ RechartsPrimitive.Funnel,
28
+ ]);
29
+
30
+ const PALETTE_SIZE = 5;
31
+
32
+ /**
33
+ * Recursively walk the children, cloning any data primitive that's missing
34
+ * `fill`/`stroke` and injecting an `hsl(var(--chart-N))` default. Counter is
35
+ * passed by reference so order is deterministic across the entire tree.
36
+ */
37
+ function applyPalette(children: React.ReactNode, counter: { i: number }): React.ReactNode {
38
+ return React.Children.map(children, (child) => {
39
+ if (!React.isValidElement(child)) return child;
40
+ const props = child.props as { fill?: unknown; stroke?: unknown; children?: React.ReactNode };
41
+
42
+ if (PALETTE_TARGETS.has(child.type)) {
43
+ if (props.fill === undefined && props.stroke === undefined) {
44
+ const idx = counter.i++ % PALETTE_SIZE;
45
+ const colour = `hsl(var(--chart-${idx + 1}))`;
46
+ // Pie/Radar/Area also benefit from a matching stroke; the
47
+ // component decides which one applies (Bar reads fill, Line
48
+ // reads stroke, etc.).
49
+ return React.cloneElement(child, {
50
+ fill: colour,
51
+ stroke: colour,
52
+ } as Partial<typeof props>);
53
+ }
54
+ return child;
55
+ }
56
+
57
+ // Recurse into children of layout / chart-type / fragment elements so
58
+ // data primitives nested inside (e.g. <BarChart><Bar/></BarChart>)
59
+ // still get assigned a palette colour.
60
+ if (props.children !== undefined) {
61
+ return React.cloneElement(
62
+ child,
63
+ {} as Partial<typeof props>,
64
+ applyPalette(props.children, counter),
65
+ );
66
+ }
67
+ return child;
68
+ });
69
+ }
70
+
71
+ export type ChartConfig = {
72
+ [k in string]: {
73
+ label?: React.ReactNode;
74
+ icon?: React.ComponentType;
75
+ } & (
76
+ | { color?: string; theme?: never }
77
+ | { color?: never; theme: Record<keyof typeof THEMES, string> }
78
+ );
79
+ };
80
+
81
+ interface ChartContextProps {
82
+ config: ChartConfig;
83
+ }
84
+
85
+ const ChartContext = React.createContext<ChartContextProps | null>(null);
86
+
87
+ export function useChart() {
88
+ const context = React.useContext(ChartContext);
89
+
90
+ if (!context) {
91
+ throw new Error("useChart must be used within a <ChartContainer />");
92
+ }
93
+
94
+ return context;
95
+ }
96
+
97
+ const ChartContainer = React.forwardRef<
98
+ HTMLDivElement,
99
+ React.ComponentProps<"div"> & {
100
+ config: ChartConfig;
101
+ children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>["children"];
102
+ }
103
+ >(({ id, className, children, config, ...props }, ref) => {
104
+ const uniqueId = React.useId();
105
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
106
+
107
+ const themedChildren = React.useMemo(
108
+ () => applyPalette(children, { i: 0 }) as typeof children,
109
+ [children],
110
+ );
111
+
112
+ return (
113
+ <ChartContext.Provider value={{ config }}>
114
+ <div
115
+ data-chart={chartId}
116
+ ref={ref}
117
+ className={cn(
118
+ "flex aspect-video w-full justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
119
+ className,
120
+ )}
121
+ {...props}
122
+ >
123
+ <ChartStyle id={chartId} config={config} />
124
+ <RechartsPrimitive.ResponsiveContainer>
125
+ {themedChildren}
126
+ </RechartsPrimitive.ResponsiveContainer>
127
+ </div>
128
+ </ChartContext.Provider>
129
+ );
130
+ });
131
+ ChartContainer.displayName = "Chart";
132
+
133
+ export const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
134
+ const colorConfig = Object.entries(config).filter(([, config]) => config.theme || config.color);
135
+
136
+ if (!colorConfig.length) {
137
+ return null;
138
+ }
139
+
140
+ return (
141
+ <style
142
+ dangerouslySetInnerHTML={{
143
+ __html: Object.entries(THEMES)
144
+ .map(
145
+ ([theme, prefix]) => `
146
+ ${prefix} [data-chart=${id}] {
147
+ ${colorConfig
148
+ .map(([key, itemConfig]) => {
149
+ const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color;
150
+ return color ? ` --color-${key}: ${color};` : null;
151
+ })
152
+ .join("\n")}
153
+ }
154
+ `,
155
+ )
156
+ .join("\n"),
157
+ }}
158
+ />
159
+ );
160
+ };
161
+
162
+ /**
163
+ * Look up the canvas config entry that matches a Recharts payload item.
164
+ * Used by tooltip and legend components to resolve labels / icons / colours
165
+ * from the ChartConfig prop.
166
+ */
167
+ export function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {
168
+ /* c8 ignore next 3 -- defensive guard: payload is always a recharts item object at call sites */
169
+ if (typeof payload !== "object" || payload === null) {
170
+ return undefined;
171
+ }
172
+
173
+ const payloadPayload =
174
+ "payload" in payload && typeof payload.payload === "object" && payload.payload !== null
175
+ ? payload.payload
176
+ : undefined;
177
+
178
+ let configLabelKey: string = key;
179
+
180
+ /* c8 ignore next 3 -- defensive name-key lookup: real recharts payloads don't carry this shape */
181
+ if (key in payload && typeof payload[key as keyof typeof payload] === "string") {
182
+ configLabelKey = payload[key as keyof typeof payload] as string;
183
+ } else if (
184
+ /* c8 ignore next 3 -- defensive nested-name lookup: only reachable with custom dataset shape */
185
+ payloadPayload &&
186
+ key in payloadPayload &&
187
+ typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
188
+ ) {
189
+ configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;
190
+ }
191
+
192
+ return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config];
193
+ }
194
+
195
+ export { ChartContainer };