@olympusoss/canvas 2.20.1 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -35
- package/package.json +45 -177
- package/src/cn.ts +3 -0
- package/src/index.ts +12 -603
- package/src/theme.ts +62 -0
- package/src/tokens.ts +11 -0
- package/styles/base.css +17 -0
- package/styles/canvas.css +77 -52
- package/styles/components/alert.css +66 -0
- package/styles/components/app-shell.css +46 -0
- package/styles/components/avatar.css +22 -0
- package/styles/components/badge.css +83 -0
- package/styles/components/breadcrumb.css +35 -0
- package/styles/components/button-group.css +23 -0
- package/styles/components/button.css +107 -0
- package/styles/components/calendar.css +73 -0
- package/styles/components/card.css +58 -0
- package/styles/components/checkbox.css +55 -0
- package/styles/components/code-block.css +18 -0
- package/styles/components/combobox.css +75 -0
- package/styles/components/command.css +94 -0
- package/styles/components/data-table.css +142 -0
- package/styles/components/dialog.css +72 -0
- package/styles/components/dropdown.css +54 -0
- package/styles/components/empty-state.css +17 -0
- package/styles/components/field.css +27 -0
- package/styles/components/filter-panel.css +58 -0
- package/styles/components/form.css +27 -0
- package/styles/components/icon.css +8 -0
- package/styles/components/input-group.css +45 -0
- package/styles/components/input.css +56 -0
- package/styles/components/kbd.css +15 -0
- package/styles/components/page-header.css +52 -0
- package/styles/components/pagination.css +48 -0
- package/styles/components/popover.css +14 -0
- package/styles/components/radio.css +28 -0
- package/styles/components/row-menu.css +69 -0
- package/styles/components/section-card.css +49 -0
- package/styles/components/select.css +57 -0
- package/styles/components/separator.css +32 -0
- package/styles/components/sheet.css +70 -0
- package/styles/components/sidebar.css +146 -0
- package/styles/components/skeleton.css +32 -0
- package/styles/components/spinner.css +26 -0
- package/styles/components/stat-card.css +71 -0
- package/styles/components/stepper.css +63 -0
- package/styles/components/switch.css +45 -0
- package/styles/components/tabs.css +40 -0
- package/styles/components/textarea.css +31 -0
- package/styles/components/toast.css +95 -0
- package/styles/components/tooltip.css +53 -0
- package/styles/components/topbar.css +24 -0
- package/styles/components/typography.css +105 -0
- package/styles/patterns/backdrops.css +35 -0
- package/styles/patterns/density.css +66 -0
- package/styles/patterns/focus.css +22 -0
- package/styles/patterns/glass.css +85 -0
- package/styles/patterns/high-contrast.css +70 -0
- package/styles/patterns/reduced-motion.css +12 -0
- package/styles/patterns/scrollbar.css +10 -0
- package/styles/reset.css +89 -0
- package/styles/tokens/colors.css +106 -0
- package/styles/tokens/motion.css +33 -0
- package/styles/tokens/radius.css +10 -0
- package/styles/tokens/shadows.css +35 -0
- package/styles/tokens/spacing.css +19 -0
- package/styles/tokens/typography.css +6 -0
- package/styles/tokens/z-index.css +12 -0
- package/styles/utilities/display.css +66 -0
- package/styles/utilities/flexbox.css +240 -0
- package/styles/utilities/gap.css +288 -0
- package/styles/utilities/grid.css +138 -0
- package/styles/utilities/position.css +78 -0
- package/styles/utilities/sizing.css +138 -0
- package/tsconfig.json +20 -21
- package/src/components/atoms/README.md +0 -11
- package/src/components/atoms/aspect-ratio.tsx +0 -32
- package/src/components/atoms/avatar.tsx +0 -98
- package/src/components/atoms/badge.tsx +0 -44
- package/src/components/atoms/brand-mark.tsx +0 -74
- package/src/components/atoms/button.tsx +0 -105
- package/src/components/atoms/checkbox.tsx +0 -63
- package/src/components/atoms/flex-box.tsx +0 -105
- package/src/components/atoms/icon.tsx +0 -34
- package/src/components/atoms/input.tsx +0 -92
- package/src/components/atoms/label.tsx +0 -41
- package/src/components/atoms/logo.tsx +0 -89
- package/src/components/atoms/progress.tsx +0 -55
- package/src/components/atoms/radio-group.tsx +0 -122
- package/src/components/atoms/scroll-area.tsx +0 -106
- package/src/components/atoms/section.tsx +0 -48
- package/src/components/atoms/separator.tsx +0 -45
- package/src/components/atoms/skeleton.tsx +0 -17
- package/src/components/atoms/slider.tsx +0 -93
- package/src/components/atoms/spinner.tsx +0 -47
- package/src/components/atoms/switch.tsx +0 -60
- package/src/components/atoms/textarea.tsx +0 -78
- package/src/components/atoms/toggle.tsx +0 -80
- package/src/components/charts/activity-heatmap.tsx +0 -186
- package/src/components/charts/axes.tsx +0 -21
- package/src/components/charts/chart-container.tsx +0 -254
- package/src/components/charts/chart-legend.tsx +0 -67
- package/src/components/charts/chart-tooltip.tsx +0 -161
- package/src/components/charts/chart-types.tsx +0 -49
- package/src/components/charts/containers.tsx +0 -11
- package/src/components/charts/data.tsx +0 -16
- package/src/components/charts/details.tsx +0 -25
- package/src/components/charts/dot-pulse.tsx +0 -61
- package/src/components/charts/gauge.tsx +0 -106
- package/src/components/charts/grids.tsx +0 -8
- package/src/components/charts/index.ts +0 -62
- package/src/components/charts/labeled-bar-list.tsx +0 -85
- package/src/components/charts/metric-breakdown.tsx +0 -316
- package/src/components/charts/references.tsx +0 -8
- package/src/components/charts/service-health-list.tsx +0 -85
- package/src/components/charts/sparkline-area.tsx +0 -80
- package/src/components/charts/sparkline.tsx +0 -52
- package/src/components/charts/stacked-bar.tsx +0 -104
- package/src/components/charts/text.tsx +0 -10
- package/src/components/charts/world-heat-map-inner.tsx +0 -317
- package/src/components/charts/world-heat-map.tsx +0 -184
- package/src/components/molecules/README.md +0 -12
- package/src/components/molecules/action-bar.tsx +0 -73
- package/src/components/molecules/activity-item.tsx +0 -74
- package/src/components/molecules/alert.tsx +0 -86
- package/src/components/molecules/animated-background.tsx +0 -92
- package/src/components/molecules/auth-shell.tsx +0 -95
- package/src/components/molecules/brand-lockup.tsx +0 -48
- package/src/components/molecules/breadcrumb.tsx +0 -157
- package/src/components/molecules/button-group.tsx +0 -104
- package/src/components/molecules/calendar.tsx +0 -217
- package/src/components/molecules/card.tsx +0 -102
- package/src/components/molecules/client-brand.tsx +0 -95
- package/src/components/molecules/code-block.tsx +0 -86
- package/src/components/molecules/countdown-button.tsx +0 -92
- package/src/components/molecules/empty-state.tsx +0 -56
- package/src/components/molecules/error-state.tsx +0 -42
- package/src/components/molecules/field-display.tsx +0 -35
- package/src/components/molecules/input-otp.tsx +0 -74
- package/src/components/molecules/launcher-card.tsx +0 -152
- package/src/components/molecules/loading-state.tsx +0 -36
- package/src/components/molecules/notification-item.tsx +0 -67
- package/src/components/molecules/notification-list.tsx +0 -45
- package/src/components/molecules/number-badge.tsx +0 -53
- package/src/components/molecules/or-separator.tsx +0 -38
- package/src/components/molecules/page-header.tsx +0 -88
- package/src/components/molecules/page-tabs.tsx +0 -94
- package/src/components/molecules/pagination.tsx +0 -150
- package/src/components/molecules/password-input.tsx +0 -83
- package/src/components/molecules/password-strength-meter.tsx +0 -104
- package/src/components/molecules/phone-input.tsx +0 -200
- package/src/components/molecules/search-bar.tsx +0 -64
- package/src/components/molecules/secret-field.tsx +0 -158
- package/src/components/molecules/section-card.tsx +0 -91
- package/src/components/molecules/social-buttons.tsx +0 -165
- package/src/components/molecules/stat-card.tsx +0 -100
- package/src/components/molecules/status-badge.tsx +0 -42
- package/src/components/molecules/stepper.tsx +0 -96
- package/src/components/molecules/table.tsx +0 -157
- package/src/components/molecules/terminal.tsx +0 -74
- package/src/components/molecules/toggle-group.tsx +0 -145
- package/src/components/molecules/tooltip.tsx +0 -155
- package/src/components/molecules/user-avatar-chip.tsx +0 -71
- package/src/components/organisms/README.md +0 -14
- package/src/components/organisms/accordion.tsx +0 -154
- package/src/components/organisms/alert-dialog.tsx +0 -277
- package/src/components/organisms/carousel.tsx +0 -244
- package/src/components/organisms/collapsible.tsx +0 -69
- package/src/components/organisms/command.tsx +0 -144
- package/src/components/organisms/context-menu.tsx +0 -339
- package/src/components/organisms/dashboard-grid.tsx +0 -369
- package/src/components/organisms/data-table.tsx +0 -330
- package/src/components/organisms/dialog.tsx +0 -312
- package/src/components/organisms/drawer.tsx +0 -123
- package/src/components/organisms/dropdown-menu.tsx +0 -440
- package/src/components/organisms/editors/code-editor.tsx +0 -144
- package/src/components/organisms/editors/index.ts +0 -4
- package/src/components/organisms/editors/markdown-editor.tsx +0 -153
- package/src/components/organisms/editors/markdown-renderer.ts +0 -27
- package/src/components/organisms/editors/prose-canvas-classes.ts +0 -45
- package/src/components/organisms/editors/rich-text-editor.tsx +0 -126
- package/src/components/organisms/editors/toolbar/md-toolbar.tsx +0 -129
- package/src/components/organisms/editors/toolbar/rte-toolbar.tsx +0 -211
- package/src/components/organisms/editors/toolbar/toolbar-shell.tsx +0 -45
- package/src/components/organisms/editors/use-codemirror-theme.ts +0 -61
- package/src/components/organisms/error-boundary.tsx +0 -61
- package/src/components/organisms/form.tsx +0 -174
- package/src/components/organisms/hover-card.tsx +0 -115
- package/src/components/organisms/menubar.tsx +0 -498
- package/src/components/organisms/navbar.tsx +0 -104
- package/src/components/organisms/navigation-menu.tsx +0 -235
- package/src/components/organisms/popover.tsx +0 -149
- package/src/components/organisms/resizable.tsx +0 -58
- package/src/components/organisms/schema-form.tsx +0 -232
- package/src/components/organisms/select.tsx +0 -309
- package/src/components/organisms/sheet.tsx +0 -265
- package/src/components/organisms/sidebar.tsx +0 -1040
- package/src/components/organisms/sonner.tsx +0 -96
- package/src/components/organisms/tabs.tsx +0 -133
- package/src/components/organisms/theme-provider.tsx +0 -101
- package/src/hooks/use-mobile.tsx +0 -19
- package/src/lib/portal-container.tsx +0 -35
- package/src/lib/utils.ts +0 -6
- package/src/native.ts +0 -23
- package/src/tokens/colors.ts +0 -91
- package/src/tokens/index.ts +0 -3
- package/src/tokens/spacing.ts +0 -55
- package/src/tokens/typography.ts +0 -27
- package/styles/dashboard-grid.css +0 -47
- package/styles/fonts/Roboto-VariableFont_wdth_wght.ttf +0 -0
- package/styles/glass.css +0 -171
- package/styles/leaflet.css +0 -13
- package/styles/tokens.css +0 -317
- package/tailwind.config.ts +0 -70
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
|
|
3
|
-
import { cn } from "../../lib/utils";
|
|
4
|
-
|
|
5
|
-
export interface SparklineAreaProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
6
|
-
/** Values to plot. Each entry maps to a point on the area line. Needs at least 2 points. */
|
|
7
|
-
data: number[];
|
|
8
|
-
/** Pixel height of the chart area. */
|
|
9
|
-
height?: number;
|
|
10
|
-
/**
|
|
11
|
-
* CSS variable name (without leading `--`) used for the stroke and fill.
|
|
12
|
-
* Default `chart-1`. The variable should resolve to an HSL triplet.
|
|
13
|
-
*/
|
|
14
|
-
colorVar?: string;
|
|
15
|
-
/** Caption rendered below the chart. */
|
|
16
|
-
caption?: React.ReactNode;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Pure-SVG area sparkline for inline embedding in `<StatCard>` /
|
|
21
|
-
* `<SectionCard>`. Renders a filled area with a gradient fade, a stroke line,
|
|
22
|
-
* and a dot on the last data point. Decorative only (no tooltips, no axes).
|
|
23
|
-
*/
|
|
24
|
-
export const SparklineArea = React.forwardRef<HTMLDivElement, SparklineAreaProps>(
|
|
25
|
-
({ data, height = 48, colorVar = "chart-1", caption, className, ...props }, ref) => {
|
|
26
|
-
const fillId = React.useId();
|
|
27
|
-
|
|
28
|
-
if (data.length < 2) return null;
|
|
29
|
-
|
|
30
|
-
const max = Math.max(...data);
|
|
31
|
-
const min = Math.min(...data);
|
|
32
|
-
const range = max - min || 1;
|
|
33
|
-
const w = (data.length - 1) * 10;
|
|
34
|
-
const h = height;
|
|
35
|
-
const padding = 3; // vertical padding for the end-dot
|
|
36
|
-
|
|
37
|
-
const pts = data
|
|
38
|
-
.map((v, i) => {
|
|
39
|
-
const x = i * 10;
|
|
40
|
-
const y = h - padding - ((v - min) / range) * (h - padding * 2);
|
|
41
|
-
return `${x},${y}`;
|
|
42
|
-
})
|
|
43
|
-
.join(" ");
|
|
44
|
-
|
|
45
|
-
const area = `0,${h} ${pts} ${w},${h}`;
|
|
46
|
-
const last = data[data.length - 1];
|
|
47
|
-
const lastY = h - padding - ((last - min) / range) * (h - padding * 2);
|
|
48
|
-
const color = `hsl(var(--${colorVar}))`;
|
|
49
|
-
|
|
50
|
-
return (
|
|
51
|
-
<div ref={ref} className={cn("w-full", className)} {...props}>
|
|
52
|
-
<svg
|
|
53
|
-
viewBox={`0 0 ${w} ${h}`}
|
|
54
|
-
preserveAspectRatio="none"
|
|
55
|
-
className="h-full w-full overflow-visible"
|
|
56
|
-
style={{ height }}
|
|
57
|
-
aria-hidden
|
|
58
|
-
>
|
|
59
|
-
<defs>
|
|
60
|
-
<linearGradient id={fillId} x1="0" x2="0" y1="0" y2="1">
|
|
61
|
-
<stop offset="0%" stopColor={color} stopOpacity="0.25" />
|
|
62
|
-
<stop offset="100%" stopColor={color} stopOpacity="0" />
|
|
63
|
-
</linearGradient>
|
|
64
|
-
</defs>
|
|
65
|
-
<polygon points={area} fill={`url(#${fillId})`} />
|
|
66
|
-
<polyline
|
|
67
|
-
points={pts}
|
|
68
|
-
fill="none"
|
|
69
|
-
stroke={color}
|
|
70
|
-
strokeWidth="1.5"
|
|
71
|
-
vectorEffect="non-scaling-stroke"
|
|
72
|
-
/>
|
|
73
|
-
<circle cx={w} cy={lastY} r="2.5" fill={color} />
|
|
74
|
-
</svg>
|
|
75
|
-
{caption && <p className="mt-2 text-xs text-muted-foreground">{caption}</p>}
|
|
76
|
-
</div>
|
|
77
|
-
);
|
|
78
|
-
},
|
|
79
|
-
);
|
|
80
|
-
SparklineArea.displayName = "SparklineArea";
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
|
|
3
|
-
import { cn } from "../../lib/utils";
|
|
4
|
-
|
|
5
|
-
export interface SparklineProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
6
|
-
/** Values to plot. Each entry maps to a bar. */
|
|
7
|
-
data: number[];
|
|
8
|
-
/** Pixel height of the chart. */
|
|
9
|
-
height?: number;
|
|
10
|
-
/**
|
|
11
|
-
* CSS variable name (without leading `--`) used for the bar fill. Default
|
|
12
|
-
* `chart-1`. The variable should resolve to an HSL triplet for `hsl()`.
|
|
13
|
-
*/
|
|
14
|
-
colorVar?: string;
|
|
15
|
-
/** Pixel gap between bars. */
|
|
16
|
-
gap?: number;
|
|
17
|
-
/** Caption rendered below the bars. */
|
|
18
|
-
caption?: React.ReactNode;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Pure-CSS mini bar chart for inline embedding in `<StatCard>` /
|
|
23
|
-
* `<SectionCard>`. Decorative — for live data, use a `<BarChart>` from the
|
|
24
|
-
* charts tier with proper axes and tooltips.
|
|
25
|
-
*/
|
|
26
|
-
export const Sparkline = React.forwardRef<HTMLDivElement, SparklineProps>(
|
|
27
|
-
({ data, height = 160, colorVar = "chart-1", gap = 4, caption, className, ...props }, ref) => {
|
|
28
|
-
const max = Math.max(1, ...data);
|
|
29
|
-
return (
|
|
30
|
-
<div ref={ref} className={cn("w-full", className)} {...props}>
|
|
31
|
-
<div className="flex w-full items-end" style={{ height, gap }}>
|
|
32
|
-
{data.map((value, i) => {
|
|
33
|
-
const pct = Math.max(0, Math.min(100, (value / max) * 100));
|
|
34
|
-
return (
|
|
35
|
-
<div
|
|
36
|
-
key={`bar-${i}-${value}`}
|
|
37
|
-
className="flex-1 rounded-sm"
|
|
38
|
-
style={{
|
|
39
|
-
height: `${pct}%`,
|
|
40
|
-
background: `hsl(var(--${colorVar}))`,
|
|
41
|
-
}}
|
|
42
|
-
aria-hidden
|
|
43
|
-
/>
|
|
44
|
-
);
|
|
45
|
-
})}
|
|
46
|
-
</div>
|
|
47
|
-
{caption && <p className="mt-3 text-xs text-muted-foreground">{caption}</p>}
|
|
48
|
-
</div>
|
|
49
|
-
);
|
|
50
|
-
},
|
|
51
|
-
);
|
|
52
|
-
Sparkline.displayName = "Sparkline";
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
|
|
3
|
-
import { cn } from "../../lib/utils";
|
|
4
|
-
|
|
5
|
-
export interface StackedBarSegment {
|
|
6
|
-
/** Label shown in the legend. Used as the React key. */
|
|
7
|
-
label: string;
|
|
8
|
-
/** Numeric value (raw count or percentage). Segments size proportionally to the sum. */
|
|
9
|
-
value: number;
|
|
10
|
-
/**
|
|
11
|
-
* CSS variable name (without leading `--`) used for the segment fill. Default
|
|
12
|
-
* `chart-1`. Each segment can override; unspecified segments fall through
|
|
13
|
-
* to the wrapper's `defaultColorVar`.
|
|
14
|
-
*/
|
|
15
|
-
colorVar?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface StackedBarProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
19
|
-
/** Segments rendered left-to-right with width proportional to `value`. */
|
|
20
|
-
segments: StackedBarSegment[];
|
|
21
|
-
/**
|
|
22
|
-
* Fallback CSS variable name for segments that omit `colorVar`. Default
|
|
23
|
-
* `chart-1`.
|
|
24
|
-
*/
|
|
25
|
-
defaultColorVar?: string;
|
|
26
|
-
/** Pixel height of the bar pill. Default `10`. */
|
|
27
|
-
height?: number;
|
|
28
|
-
/** Show the legend list below the bar. Default `true`. */
|
|
29
|
-
showLegend?: boolean;
|
|
30
|
-
/** Format the per-segment value in the legend. Default `(v) => `${v}%``. */
|
|
31
|
-
valueFormatter?: (value: number) => string;
|
|
32
|
-
/** Caption rendered below the legend. */
|
|
33
|
-
caption?: React.ReactNode;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Horizontal stacked-percent bar with an optional swatch legend below. Segments
|
|
38
|
-
* are sized proportionally to their value's share of the total — pass raw
|
|
39
|
-
* counts or pre-computed percentages, the rendering is the same.
|
|
40
|
-
*/
|
|
41
|
-
export const StackedBar = React.forwardRef<HTMLDivElement, StackedBarProps>(
|
|
42
|
-
(
|
|
43
|
-
{
|
|
44
|
-
segments,
|
|
45
|
-
defaultColorVar = "chart-1",
|
|
46
|
-
height = 10,
|
|
47
|
-
showLegend = true,
|
|
48
|
-
valueFormatter = (v) => `${v}%`,
|
|
49
|
-
caption,
|
|
50
|
-
className,
|
|
51
|
-
...props
|
|
52
|
-
},
|
|
53
|
-
ref,
|
|
54
|
-
) => {
|
|
55
|
-
const total = segments.reduce((sum, s) => sum + s.value, 0) || 1;
|
|
56
|
-
return (
|
|
57
|
-
<div ref={ref} className={cn("w-full", className)} {...props}>
|
|
58
|
-
<div
|
|
59
|
-
className="flex w-full overflow-hidden rounded-full bg-muted"
|
|
60
|
-
style={{ height }}
|
|
61
|
-
role="presentation"
|
|
62
|
-
>
|
|
63
|
-
{segments.map((seg) => {
|
|
64
|
-
const pct = (seg.value / total) * 100;
|
|
65
|
-
const colorVar = seg.colorVar ?? defaultColorVar;
|
|
66
|
-
return (
|
|
67
|
-
<div
|
|
68
|
-
key={seg.label}
|
|
69
|
-
style={{
|
|
70
|
-
width: `${pct}%`,
|
|
71
|
-
background: `hsl(var(--${colorVar}))`,
|
|
72
|
-
}}
|
|
73
|
-
title={`${seg.label}: ${valueFormatter(seg.value)}`}
|
|
74
|
-
aria-hidden
|
|
75
|
-
/>
|
|
76
|
-
);
|
|
77
|
-
})}
|
|
78
|
-
</div>
|
|
79
|
-
{showLegend && (
|
|
80
|
-
<ul className="mt-3 flex flex-col gap-2">
|
|
81
|
-
{segments.map((seg) => {
|
|
82
|
-
const colorVar = seg.colorVar ?? defaultColorVar;
|
|
83
|
-
return (
|
|
84
|
-
<li key={seg.label} className="flex items-center gap-2.5 text-[13px]">
|
|
85
|
-
<span
|
|
86
|
-
className="size-2 shrink-0 rounded-full"
|
|
87
|
-
style={{ background: `hsl(var(--${colorVar}))` }}
|
|
88
|
-
aria-hidden
|
|
89
|
-
/>
|
|
90
|
-
<span className="flex-1">{seg.label}</span>
|
|
91
|
-
<span className="font-mono text-xs text-muted-foreground">
|
|
92
|
-
{valueFormatter(seg.value)}
|
|
93
|
-
</span>
|
|
94
|
-
</li>
|
|
95
|
-
);
|
|
96
|
-
})}
|
|
97
|
-
</ul>
|
|
98
|
-
)}
|
|
99
|
-
{caption && <p className="mt-3 text-xs text-muted-foreground">{caption}</p>}
|
|
100
|
-
</div>
|
|
101
|
-
);
|
|
102
|
-
},
|
|
103
|
-
);
|
|
104
|
-
StackedBar.displayName = "StackedBar";
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Text-rendering primitives. `Label` is aliased to `ChartLabel` to avoid
|
|
5
|
-
* collision with canvas's form `Label` atom.
|
|
6
|
-
*
|
|
7
|
-
* Re-exported via `export { … }` so TypeScript doesn't have to name internal
|
|
8
|
-
* Recharts prop types in the d.ts emit.
|
|
9
|
-
*/
|
|
10
|
-
export { Label as ChartLabel, LabelList, Text } from "recharts";
|
|
@@ -1,317 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
/* c8 ignore file -- Leaflet imports require real DOM measurements and are
|
|
4
|
-
* jsdom-incompatible. This file is covered structurally via the lazy
|
|
5
|
-
* boundary + loading shell + error-fallback in world-heat-map.test.tsx;
|
|
6
|
-
* the rendered Leaflet tree is verified visually in the docs. */
|
|
7
|
-
|
|
8
|
-
import * as React from "react";
|
|
9
|
-
import { CircleMarker, MapContainer, TileLayer, Tooltip } from "react-leaflet";
|
|
10
|
-
|
|
11
|
-
import { cn } from "../../lib/utils";
|
|
12
|
-
import { useTheme } from "../organisms/theme-provider";
|
|
13
|
-
import type { WorldHeatMapPoint, WorldHeatMapProps } from "./world-heat-map";
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Client-only Leaflet tree. Loaded via `React.lazy()` from the orchestrator
|
|
17
|
-
* `world-heat-map.tsx` so neither `leaflet` nor `react-leaflet` lands in the
|
|
18
|
-
* main canvas bundle. SSR / loading / error are handled by the orchestrator —
|
|
19
|
-
* this file assumes a browser environment.
|
|
20
|
-
*
|
|
21
|
-
* Visual identity matches the deployed admin spec extracted from
|
|
22
|
-
* `https://admin.ciam.nannier.com/dashboard`:
|
|
23
|
-
* - CartoDB Basemaps (`dark_all` / `light_all`, `@2x` retina)
|
|
24
|
-
* - Hidden controls (zoomControl, attributionControl)
|
|
25
|
-
* - Triple-layered CircleMarker per point (glow / main / centre)
|
|
26
|
-
* - Log-scaled radius (`log(1+count) / log(1+maxCount)`)
|
|
27
|
-
*
|
|
28
|
-
* Consumers must import `@olympusoss/canvas/styles/leaflet.css` once at app
|
|
29
|
-
* entry — Leaflet's base stylesheet doesn't ship with this component.
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
const SCOPED_CSS = `
|
|
33
|
-
.world-heat-map .leaflet-container {
|
|
34
|
-
border-radius: inherit;
|
|
35
|
-
background: transparent;
|
|
36
|
-
font: inherit;
|
|
37
|
-
touch-action: none;
|
|
38
|
-
}
|
|
39
|
-
.world-heat-map .leaflet-control-attribution {
|
|
40
|
-
display: none;
|
|
41
|
-
}
|
|
42
|
-
.world-heat-map.world-heat-map--no-controls .leaflet-control-zoom {
|
|
43
|
-
display: none;
|
|
44
|
-
}
|
|
45
|
-
.world-heat-map .leaflet-control-zoom {
|
|
46
|
-
border: 1px solid hsl(var(--border) / 0.5) !important;
|
|
47
|
-
border-radius: 0.5rem !important;
|
|
48
|
-
overflow: hidden;
|
|
49
|
-
box-shadow: 0 2px 8px rgb(0 0 0 / 0.1) !important;
|
|
50
|
-
}
|
|
51
|
-
.world-heat-map .leaflet-control-zoom a {
|
|
52
|
-
background: hsl(var(--popover)) !important;
|
|
53
|
-
color: hsl(var(--popover-foreground)) !important;
|
|
54
|
-
border-bottom-color: hsl(var(--border) / 0.3) !important;
|
|
55
|
-
width: 32px !important;
|
|
56
|
-
height: 32px !important;
|
|
57
|
-
line-height: 32px !important;
|
|
58
|
-
font-size: 16px !important;
|
|
59
|
-
}
|
|
60
|
-
.world-heat-map .leaflet-control-zoom a:hover {
|
|
61
|
-
background: hsl(var(--accent)) !important;
|
|
62
|
-
color: hsl(var(--accent-foreground)) !important;
|
|
63
|
-
}
|
|
64
|
-
.whm-tooltip-popup {
|
|
65
|
-
background: hsl(var(--popover)) !important;
|
|
66
|
-
color: hsl(var(--popover-foreground)) !important;
|
|
67
|
-
border: 1px solid hsl(var(--border) / 0.5) !important;
|
|
68
|
-
border-radius: 0.5rem !important;
|
|
69
|
-
padding: 0.5rem 0.75rem !important;
|
|
70
|
-
font-size: 0.75rem !important;
|
|
71
|
-
font-family: inherit !important;
|
|
72
|
-
box-shadow: 0 4px 12px rgb(0 0 0 / 0.15) !important;
|
|
73
|
-
}
|
|
74
|
-
.whm-tooltip-popup::before {
|
|
75
|
-
border-top-color: hsl(var(--popover)) !important;
|
|
76
|
-
}
|
|
77
|
-
.whm-tooltip-label {
|
|
78
|
-
font-weight: 600;
|
|
79
|
-
font-size: 13px;
|
|
80
|
-
line-height: 1.2;
|
|
81
|
-
}
|
|
82
|
-
.whm-tooltip-count {
|
|
83
|
-
display: flex;
|
|
84
|
-
align-items: center;
|
|
85
|
-
gap: 6px;
|
|
86
|
-
margin-top: 2px;
|
|
87
|
-
color: hsl(var(--muted-foreground));
|
|
88
|
-
font-variant-numeric: tabular-nums;
|
|
89
|
-
font-size: 12px;
|
|
90
|
-
}
|
|
91
|
-
.whm-tooltip-dot {
|
|
92
|
-
display: inline-block;
|
|
93
|
-
height: 8px;
|
|
94
|
-
width: 8px;
|
|
95
|
-
border-radius: 50%;
|
|
96
|
-
}
|
|
97
|
-
`;
|
|
98
|
-
|
|
99
|
-
const TILE_URL_DARK = "https://basemaps.cartocdn.com/dark_all/{z}/{x}/{y}@2x.png";
|
|
100
|
-
const TILE_URL_LIGHT = "https://basemaps.cartocdn.com/light_all/{z}/{x}/{y}@2x.png";
|
|
101
|
-
|
|
102
|
-
const ATTRIBUTION =
|
|
103
|
-
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> © <a href="https://carto.com/attributions">CARTO</a>';
|
|
104
|
-
|
|
105
|
-
function resolveHeight(height: number | string | undefined): string {
|
|
106
|
-
if (height === undefined) return "100%";
|
|
107
|
-
return typeof height === "number" ? `${height}px` : height;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
interface MarkerLayers {
|
|
111
|
-
glowRadius: number;
|
|
112
|
-
mainRadius: number;
|
|
113
|
-
centreRadius: number;
|
|
114
|
-
mainOpacity: number;
|
|
115
|
-
glowOpacity: number;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function deriveMarker(
|
|
119
|
-
count: number,
|
|
120
|
-
maxCount: number,
|
|
121
|
-
[rMin, rMax]: [number, number],
|
|
122
|
-
): MarkerLayers {
|
|
123
|
-
const t = maxCount > 0 ? Math.log(1 + count) / Math.log(1 + maxCount) : 0;
|
|
124
|
-
const r = rMin + t * (rMax - rMin);
|
|
125
|
-
const opacity = 0.4 + t * 0.55;
|
|
126
|
-
return {
|
|
127
|
-
glowRadius: r * 1.8,
|
|
128
|
-
mainRadius: r,
|
|
129
|
-
centreRadius: Math.max(r * 0.35, 1.5),
|
|
130
|
-
mainOpacity: opacity,
|
|
131
|
-
glowOpacity: opacity * 0.15,
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export default function WorldHeatMapInner({
|
|
136
|
-
points,
|
|
137
|
-
height,
|
|
138
|
-
center = [20, 0],
|
|
139
|
-
zoom = 3,
|
|
140
|
-
tileTheme = "auto",
|
|
141
|
-
showControls = false,
|
|
142
|
-
markerColor = "hsl(var(--chart-1))",
|
|
143
|
-
markerRadiusRange = [4, 20],
|
|
144
|
-
onMarkerClick,
|
|
145
|
-
showLegend = true,
|
|
146
|
-
title,
|
|
147
|
-
emptyState,
|
|
148
|
-
className,
|
|
149
|
-
}: WorldHeatMapProps) {
|
|
150
|
-
const { resolvedTheme } = useTheme();
|
|
151
|
-
const isDark = tileTheme === "dark" || (tileTheme === "auto" && resolvedTheme === "dark");
|
|
152
|
-
const tileUrl = isDark ? TILE_URL_DARK : TILE_URL_LIGHT;
|
|
153
|
-
|
|
154
|
-
const heightStr = resolveHeight(height);
|
|
155
|
-
const maxCount = React.useMemo(
|
|
156
|
-
() => points.reduce((acc, p) => Math.max(acc, p.count), 1),
|
|
157
|
-
[points],
|
|
158
|
-
);
|
|
159
|
-
const hasData = points.length > 0;
|
|
160
|
-
|
|
161
|
-
return (
|
|
162
|
-
<div
|
|
163
|
-
className={cn(
|
|
164
|
-
"relative overflow-hidden rounded-xl world-heat-map",
|
|
165
|
-
!showControls && "world-heat-map--no-controls",
|
|
166
|
-
className,
|
|
167
|
-
)}
|
|
168
|
-
style={{ height: heightStr }}
|
|
169
|
-
>
|
|
170
|
-
<style dangerouslySetInnerHTML={{ __html: SCOPED_CSS }} />
|
|
171
|
-
|
|
172
|
-
<MapContainer
|
|
173
|
-
center={center}
|
|
174
|
-
zoom={zoom}
|
|
175
|
-
minZoom={2}
|
|
176
|
-
maxZoom={19}
|
|
177
|
-
zoomControl={showControls}
|
|
178
|
-
attributionControl={showControls}
|
|
179
|
-
worldCopyJump={false}
|
|
180
|
-
maxBounds={[
|
|
181
|
-
[-85, -180],
|
|
182
|
-
[85, 180],
|
|
183
|
-
]}
|
|
184
|
-
maxBoundsViscosity={1}
|
|
185
|
-
className="h-full w-full"
|
|
186
|
-
>
|
|
187
|
-
<TileLayer key={tileUrl} url={tileUrl} attribution={ATTRIBUTION} />
|
|
188
|
-
|
|
189
|
-
{points.map((point) => (
|
|
190
|
-
<MarkerGroup
|
|
191
|
-
key={`${point.lat},${point.lng},${point.label}`}
|
|
192
|
-
point={point}
|
|
193
|
-
maxCount={maxCount}
|
|
194
|
-
markerColor={markerColor}
|
|
195
|
-
markerRadiusRange={markerRadiusRange}
|
|
196
|
-
onMarkerClick={onMarkerClick}
|
|
197
|
-
/>
|
|
198
|
-
))}
|
|
199
|
-
</MapContainer>
|
|
200
|
-
|
|
201
|
-
{title && (
|
|
202
|
-
<div className="pointer-events-none absolute left-4 top-4 z-[1000] text-sm font-semibold tracking-tight text-foreground">
|
|
203
|
-
{title}
|
|
204
|
-
</div>
|
|
205
|
-
)}
|
|
206
|
-
|
|
207
|
-
{showLegend && hasData && (
|
|
208
|
-
<div className="pointer-events-none absolute bottom-3 left-4 z-[1000] flex items-center gap-2.5 text-[10px] text-muted-foreground">
|
|
209
|
-
<div className="flex items-center gap-1">
|
|
210
|
-
<svg width="8" height="8" aria-hidden="true">
|
|
211
|
-
<title>Smaller marker</title>
|
|
212
|
-
<circle cx="4" cy="4" r="2.5" fill={markerColor} fillOpacity={0.5} />
|
|
213
|
-
</svg>
|
|
214
|
-
<span>Fewer</span>
|
|
215
|
-
</div>
|
|
216
|
-
<div className="flex items-center gap-1">
|
|
217
|
-
<svg width="14" height="14" aria-hidden="true">
|
|
218
|
-
<title>Larger marker</title>
|
|
219
|
-
<circle cx="7" cy="7" r="5" fill={markerColor} fillOpacity={0.9} />
|
|
220
|
-
</svg>
|
|
221
|
-
<span>More</span>
|
|
222
|
-
</div>
|
|
223
|
-
</div>
|
|
224
|
-
)}
|
|
225
|
-
|
|
226
|
-
{!hasData && (
|
|
227
|
-
<div className="pointer-events-none absolute inset-0 z-[1000] flex items-center justify-center">
|
|
228
|
-
{emptyState ?? (
|
|
229
|
-
<div className="rounded-lg bg-card px-4 py-2 text-xs text-muted-foreground shadow">
|
|
230
|
-
No geographic data available
|
|
231
|
-
</div>
|
|
232
|
-
)}
|
|
233
|
-
</div>
|
|
234
|
-
)}
|
|
235
|
-
|
|
236
|
-
{/* CartoDB attribution — always visible per TOS, even when controls are hidden */}
|
|
237
|
-
{!showControls && (
|
|
238
|
-
<div
|
|
239
|
-
className="pointer-events-auto absolute bottom-1 right-1 z-[1000] text-[9px] text-muted-foreground/60"
|
|
240
|
-
dangerouslySetInnerHTML={{ __html: ATTRIBUTION }}
|
|
241
|
-
/>
|
|
242
|
-
)}
|
|
243
|
-
</div>
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Three concentric `<CircleMarker>`s per data point: outer glow, main dot
|
|
249
|
-
* (interactive + tooltip), bright centre. Pulled into its own component so the
|
|
250
|
-
* tooltip's stable `<Tooltip>` child slot lives next to the marker that owns
|
|
251
|
-
* it (react-leaflet attaches by parent identity).
|
|
252
|
-
*/
|
|
253
|
-
function MarkerGroup({
|
|
254
|
-
point,
|
|
255
|
-
maxCount,
|
|
256
|
-
markerColor,
|
|
257
|
-
markerRadiusRange,
|
|
258
|
-
onMarkerClick,
|
|
259
|
-
}: {
|
|
260
|
-
point: WorldHeatMapPoint;
|
|
261
|
-
maxCount: number;
|
|
262
|
-
markerColor: string;
|
|
263
|
-
markerRadiusRange: [number, number];
|
|
264
|
-
onMarkerClick?: (point: WorldHeatMapPoint) => void;
|
|
265
|
-
}) {
|
|
266
|
-
const layers = deriveMarker(point.count, maxCount, markerRadiusRange);
|
|
267
|
-
const handlers = React.useMemo(
|
|
268
|
-
() => (onMarkerClick ? { click: () => onMarkerClick(point) } : undefined),
|
|
269
|
-
[onMarkerClick, point],
|
|
270
|
-
);
|
|
271
|
-
|
|
272
|
-
return (
|
|
273
|
-
<>
|
|
274
|
-
<CircleMarker
|
|
275
|
-
center={[point.lat, point.lng]}
|
|
276
|
-
radius={layers.glowRadius}
|
|
277
|
-
pathOptions={{
|
|
278
|
-
fillColor: markerColor,
|
|
279
|
-
fillOpacity: layers.glowOpacity,
|
|
280
|
-
stroke: false,
|
|
281
|
-
interactive: false,
|
|
282
|
-
}}
|
|
283
|
-
/>
|
|
284
|
-
<CircleMarker
|
|
285
|
-
center={[point.lat, point.lng]}
|
|
286
|
-
radius={layers.mainRadius}
|
|
287
|
-
pathOptions={{
|
|
288
|
-
fillColor: markerColor,
|
|
289
|
-
fillOpacity: layers.mainOpacity,
|
|
290
|
-
color: markerColor,
|
|
291
|
-
weight: 1,
|
|
292
|
-
opacity: 0.8,
|
|
293
|
-
}}
|
|
294
|
-
eventHandlers={handlers}
|
|
295
|
-
>
|
|
296
|
-
<Tooltip direction="top" offset={[0, -layers.mainRadius]} className="whm-tooltip-popup">
|
|
297
|
-
<div className="whm-tooltip-label">{point.label}</div>
|
|
298
|
-
<div className="whm-tooltip-count">
|
|
299
|
-
<span className="whm-tooltip-dot" style={{ background: markerColor }} />
|
|
300
|
-
{point.count.toLocaleString()} session
|
|
301
|
-
{point.count !== 1 ? "s" : ""}
|
|
302
|
-
</div>
|
|
303
|
-
</Tooltip>
|
|
304
|
-
</CircleMarker>
|
|
305
|
-
<CircleMarker
|
|
306
|
-
center={[point.lat, point.lng]}
|
|
307
|
-
radius={layers.centreRadius}
|
|
308
|
-
pathOptions={{
|
|
309
|
-
fillColor: markerColor,
|
|
310
|
-
fillOpacity: 0.95,
|
|
311
|
-
stroke: false,
|
|
312
|
-
interactive: false,
|
|
313
|
-
}}
|
|
314
|
-
/>
|
|
315
|
-
</>
|
|
316
|
-
);
|
|
317
|
-
}
|