@olympusoss/canvas 2.20.1 → 4.0.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.
Files changed (208) hide show
  1. package/package.json +41 -177
  2. package/src/cn.ts +3 -0
  3. package/src/index.ts +12 -603
  4. package/src/theme.ts +41 -0
  5. package/src/tokens.ts +11 -0
  6. package/styles/base.css +17 -0
  7. package/styles/canvas.css +69 -52
  8. package/styles/components/alert.css +66 -0
  9. package/styles/components/app-shell.css +46 -0
  10. package/styles/components/avatar.css +15 -0
  11. package/styles/components/badge.css +83 -0
  12. package/styles/components/breadcrumb.css +35 -0
  13. package/styles/components/button-group.css +23 -0
  14. package/styles/components/button.css +107 -0
  15. package/styles/components/calendar.css +73 -0
  16. package/styles/components/card.css +58 -0
  17. package/styles/components/checkbox.css +55 -0
  18. package/styles/components/code-block.css +18 -0
  19. package/styles/components/combobox.css +75 -0
  20. package/styles/components/command.css +94 -0
  21. package/styles/components/data-table.css +142 -0
  22. package/styles/components/dialog.css +72 -0
  23. package/styles/components/dropdown.css +54 -0
  24. package/styles/components/empty-state.css +17 -0
  25. package/styles/components/field.css +27 -0
  26. package/styles/components/filter-panel.css +58 -0
  27. package/styles/components/form.css +27 -0
  28. package/styles/components/icon.css +8 -0
  29. package/styles/components/input-group.css +45 -0
  30. package/styles/components/input.css +56 -0
  31. package/styles/components/kbd.css +15 -0
  32. package/styles/components/page-header.css +52 -0
  33. package/styles/components/pagination.css +48 -0
  34. package/styles/components/popover.css +14 -0
  35. package/styles/components/radio.css +28 -0
  36. package/styles/components/row-menu.css +69 -0
  37. package/styles/components/section-card.css +49 -0
  38. package/styles/components/select.css +57 -0
  39. package/styles/components/separator.css +32 -0
  40. package/styles/components/sheet.css +70 -0
  41. package/styles/components/sidebar.css +146 -0
  42. package/styles/components/skeleton.css +32 -0
  43. package/styles/components/spinner.css +26 -0
  44. package/styles/components/stat-card.css +71 -0
  45. package/styles/components/stepper.css +63 -0
  46. package/styles/components/switch.css +45 -0
  47. package/styles/components/tabs.css +40 -0
  48. package/styles/components/textarea.css +31 -0
  49. package/styles/components/toast.css +95 -0
  50. package/styles/components/tooltip.css +53 -0
  51. package/styles/components/topbar.css +24 -0
  52. package/styles/components/typography.css +105 -0
  53. package/styles/patterns/backdrops.css +35 -0
  54. package/styles/patterns/density.css +66 -0
  55. package/styles/patterns/focus.css +38 -0
  56. package/styles/patterns/glass.css +85 -0
  57. package/styles/patterns/high-contrast.css +70 -0
  58. package/styles/patterns/reduced-motion.css +12 -0
  59. package/styles/patterns/scrollbar.css +10 -0
  60. package/styles/reset.css +89 -0
  61. package/styles/tokens/colors.css +106 -0
  62. package/styles/tokens/motion.css +33 -0
  63. package/styles/tokens/radius.css +10 -0
  64. package/styles/tokens/shadows.css +35 -0
  65. package/styles/tokens/spacing.css +19 -0
  66. package/styles/tokens/typography.css +6 -0
  67. package/styles/tokens/z-index.css +12 -0
  68. package/tsconfig.json +20 -21
  69. package/README.md +0 -60
  70. package/src/components/atoms/README.md +0 -11
  71. package/src/components/atoms/aspect-ratio.tsx +0 -32
  72. package/src/components/atoms/avatar.tsx +0 -98
  73. package/src/components/atoms/badge.tsx +0 -44
  74. package/src/components/atoms/brand-mark.tsx +0 -74
  75. package/src/components/atoms/button.tsx +0 -105
  76. package/src/components/atoms/checkbox.tsx +0 -63
  77. package/src/components/atoms/flex-box.tsx +0 -105
  78. package/src/components/atoms/icon.tsx +0 -34
  79. package/src/components/atoms/input.tsx +0 -92
  80. package/src/components/atoms/label.tsx +0 -41
  81. package/src/components/atoms/logo.tsx +0 -89
  82. package/src/components/atoms/progress.tsx +0 -55
  83. package/src/components/atoms/radio-group.tsx +0 -122
  84. package/src/components/atoms/scroll-area.tsx +0 -106
  85. package/src/components/atoms/section.tsx +0 -48
  86. package/src/components/atoms/separator.tsx +0 -45
  87. package/src/components/atoms/skeleton.tsx +0 -17
  88. package/src/components/atoms/slider.tsx +0 -93
  89. package/src/components/atoms/spinner.tsx +0 -47
  90. package/src/components/atoms/switch.tsx +0 -60
  91. package/src/components/atoms/textarea.tsx +0 -78
  92. package/src/components/atoms/toggle.tsx +0 -80
  93. package/src/components/charts/activity-heatmap.tsx +0 -186
  94. package/src/components/charts/axes.tsx +0 -21
  95. package/src/components/charts/chart-container.tsx +0 -254
  96. package/src/components/charts/chart-legend.tsx +0 -67
  97. package/src/components/charts/chart-tooltip.tsx +0 -161
  98. package/src/components/charts/chart-types.tsx +0 -49
  99. package/src/components/charts/containers.tsx +0 -11
  100. package/src/components/charts/data.tsx +0 -16
  101. package/src/components/charts/details.tsx +0 -25
  102. package/src/components/charts/dot-pulse.tsx +0 -61
  103. package/src/components/charts/gauge.tsx +0 -106
  104. package/src/components/charts/grids.tsx +0 -8
  105. package/src/components/charts/index.ts +0 -62
  106. package/src/components/charts/labeled-bar-list.tsx +0 -85
  107. package/src/components/charts/metric-breakdown.tsx +0 -316
  108. package/src/components/charts/references.tsx +0 -8
  109. package/src/components/charts/service-health-list.tsx +0 -85
  110. package/src/components/charts/sparkline-area.tsx +0 -80
  111. package/src/components/charts/sparkline.tsx +0 -52
  112. package/src/components/charts/stacked-bar.tsx +0 -104
  113. package/src/components/charts/text.tsx +0 -10
  114. package/src/components/charts/world-heat-map-inner.tsx +0 -317
  115. package/src/components/charts/world-heat-map.tsx +0 -184
  116. package/src/components/molecules/README.md +0 -12
  117. package/src/components/molecules/action-bar.tsx +0 -73
  118. package/src/components/molecules/activity-item.tsx +0 -74
  119. package/src/components/molecules/alert.tsx +0 -86
  120. package/src/components/molecules/animated-background.tsx +0 -92
  121. package/src/components/molecules/auth-shell.tsx +0 -95
  122. package/src/components/molecules/brand-lockup.tsx +0 -48
  123. package/src/components/molecules/breadcrumb.tsx +0 -157
  124. package/src/components/molecules/button-group.tsx +0 -104
  125. package/src/components/molecules/calendar.tsx +0 -217
  126. package/src/components/molecules/card.tsx +0 -102
  127. package/src/components/molecules/client-brand.tsx +0 -95
  128. package/src/components/molecules/code-block.tsx +0 -86
  129. package/src/components/molecules/countdown-button.tsx +0 -92
  130. package/src/components/molecules/empty-state.tsx +0 -56
  131. package/src/components/molecules/error-state.tsx +0 -42
  132. package/src/components/molecules/field-display.tsx +0 -35
  133. package/src/components/molecules/input-otp.tsx +0 -74
  134. package/src/components/molecules/launcher-card.tsx +0 -152
  135. package/src/components/molecules/loading-state.tsx +0 -36
  136. package/src/components/molecules/notification-item.tsx +0 -67
  137. package/src/components/molecules/notification-list.tsx +0 -45
  138. package/src/components/molecules/number-badge.tsx +0 -53
  139. package/src/components/molecules/or-separator.tsx +0 -38
  140. package/src/components/molecules/page-header.tsx +0 -88
  141. package/src/components/molecules/page-tabs.tsx +0 -94
  142. package/src/components/molecules/pagination.tsx +0 -150
  143. package/src/components/molecules/password-input.tsx +0 -83
  144. package/src/components/molecules/password-strength-meter.tsx +0 -104
  145. package/src/components/molecules/phone-input.tsx +0 -200
  146. package/src/components/molecules/search-bar.tsx +0 -64
  147. package/src/components/molecules/secret-field.tsx +0 -158
  148. package/src/components/molecules/section-card.tsx +0 -91
  149. package/src/components/molecules/social-buttons.tsx +0 -165
  150. package/src/components/molecules/stat-card.tsx +0 -100
  151. package/src/components/molecules/status-badge.tsx +0 -42
  152. package/src/components/molecules/stepper.tsx +0 -96
  153. package/src/components/molecules/table.tsx +0 -157
  154. package/src/components/molecules/terminal.tsx +0 -74
  155. package/src/components/molecules/toggle-group.tsx +0 -145
  156. package/src/components/molecules/tooltip.tsx +0 -155
  157. package/src/components/molecules/user-avatar-chip.tsx +0 -71
  158. package/src/components/organisms/README.md +0 -14
  159. package/src/components/organisms/accordion.tsx +0 -154
  160. package/src/components/organisms/alert-dialog.tsx +0 -277
  161. package/src/components/organisms/carousel.tsx +0 -244
  162. package/src/components/organisms/collapsible.tsx +0 -69
  163. package/src/components/organisms/command.tsx +0 -144
  164. package/src/components/organisms/context-menu.tsx +0 -339
  165. package/src/components/organisms/dashboard-grid.tsx +0 -369
  166. package/src/components/organisms/data-table.tsx +0 -330
  167. package/src/components/organisms/dialog.tsx +0 -312
  168. package/src/components/organisms/drawer.tsx +0 -123
  169. package/src/components/organisms/dropdown-menu.tsx +0 -440
  170. package/src/components/organisms/editors/code-editor.tsx +0 -144
  171. package/src/components/organisms/editors/index.ts +0 -4
  172. package/src/components/organisms/editors/markdown-editor.tsx +0 -153
  173. package/src/components/organisms/editors/markdown-renderer.ts +0 -27
  174. package/src/components/organisms/editors/prose-canvas-classes.ts +0 -45
  175. package/src/components/organisms/editors/rich-text-editor.tsx +0 -126
  176. package/src/components/organisms/editors/toolbar/md-toolbar.tsx +0 -129
  177. package/src/components/organisms/editors/toolbar/rte-toolbar.tsx +0 -211
  178. package/src/components/organisms/editors/toolbar/toolbar-shell.tsx +0 -45
  179. package/src/components/organisms/editors/use-codemirror-theme.ts +0 -61
  180. package/src/components/organisms/error-boundary.tsx +0 -61
  181. package/src/components/organisms/form.tsx +0 -174
  182. package/src/components/organisms/hover-card.tsx +0 -115
  183. package/src/components/organisms/menubar.tsx +0 -498
  184. package/src/components/organisms/navbar.tsx +0 -104
  185. package/src/components/organisms/navigation-menu.tsx +0 -235
  186. package/src/components/organisms/popover.tsx +0 -149
  187. package/src/components/organisms/resizable.tsx +0 -58
  188. package/src/components/organisms/schema-form.tsx +0 -232
  189. package/src/components/organisms/select.tsx +0 -309
  190. package/src/components/organisms/sheet.tsx +0 -265
  191. package/src/components/organisms/sidebar.tsx +0 -1040
  192. package/src/components/organisms/sonner.tsx +0 -96
  193. package/src/components/organisms/tabs.tsx +0 -133
  194. package/src/components/organisms/theme-provider.tsx +0 -101
  195. package/src/hooks/use-mobile.tsx +0 -19
  196. package/src/lib/portal-container.tsx +0 -35
  197. package/src/lib/utils.ts +0 -6
  198. package/src/native.ts +0 -23
  199. package/src/tokens/colors.ts +0 -91
  200. package/src/tokens/index.ts +0 -3
  201. package/src/tokens/spacing.ts +0 -55
  202. package/src/tokens/typography.ts +0 -27
  203. package/styles/dashboard-grid.css +0 -47
  204. package/styles/fonts/Roboto-VariableFont_wdth_wght.ttf +0 -0
  205. package/styles/glass.css +0 -171
  206. package/styles/leaflet.css +0 -13
  207. package/styles/tokens.css +0 -317
  208. package/tailwind.config.ts +0 -70
@@ -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
- '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> &copy; <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
- }
@@ -1,184 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
-
5
- import { cn } from "../../lib/utils";
6
-
7
- /**
8
- * `<WorldHeatMap>` — Leaflet-based geographic scatter / heat-map.
9
- *
10
- * Public surface lives here; the actual react-leaflet tree is in
11
- * `world-heat-map-inner.tsx` and is loaded via `React.lazy()` so neither
12
- * `leaflet` nor `react-leaflet` lands in canvas's main bundle. Both are
13
- * declared as **peer-optional** in `package.json` — consumers who use the map
14
- * install them explicitly; consumers who don't, don't pay.
15
- *
16
- * Consumers must also import the Leaflet base stylesheet at app entry:
17
- *
18
- * import "@olympusoss/canvas/styles/leaflet.css";
19
- *
20
- * Visual identity matches the deployed admin dashboard reference (CartoDB
21
- * tiles, hidden controls, triple-layered CircleMarker per point).
22
- */
23
-
24
- // ── Public types ─────────────────────────────────────────────
25
-
26
- export interface WorldHeatMapPoint {
27
- /** Latitude. */
28
- lat: number;
29
- /** Longitude. */
30
- lng: number;
31
- /** Tooltip label (e.g. "Munich, DE"). */
32
- label: string;
33
- /** Drives marker radius. */
34
- count: number;
35
- }
36
-
37
- export interface WorldHeatMapProps {
38
- /** Geo-located points to render. Empty array shows the empty-state overlay. */
39
- points: WorldHeatMapPoint[];
40
- /** Container height. Number → px, string passes through. Default `"100%"`. */
41
- height?: number | string;
42
- /** Initial map center `[lat, lng]`. Default `[20, 0]`. */
43
- center?: [number, number];
44
- /** Initial zoom. Default `3`. */
45
- zoom?: number;
46
- /** Tile basemap. `"auto"` follows canvas's `useTheme()`. Default `"auto"`. */
47
- tileTheme?: "auto" | "dark" | "light";
48
- /** Show Leaflet zoom + attribution controls. Default `false`. */
49
- showControls?: boolean;
50
- /** Marker fill (any CSS colour, including `hsl(var(--chart-1))`). Default `"hsl(var(--chart-1))"`. */
51
- markerColor?: string;
52
- /** `[min, max]` marker radius in pixels (log-scaled on `count`). Default `[4, 20]`. */
53
- markerRadiusRange?: [number, number];
54
- /** Per-marker click handler. */
55
- onMarkerClick?: (point: WorldHeatMapPoint) => void;
56
- /** Show "Fewer / More" legend overlay (bottom-left). Default `true`. */
57
- showLegend?: boolean;
58
- /** Optional title overlaid top-left. */
59
- title?: string;
60
- /** Override the empty-state node. Default: a small "No geographic data available" card. */
61
- emptyState?: React.ReactNode;
62
- /** Pass-through className on the outer `.world-heat-map` wrapper. */
63
- className?: string;
64
- }
65
-
66
- // ── Lazy-loaded inner ───────────────────────────────────────
67
-
68
- const WorldHeatMapInner = React.lazy(() => import("./world-heat-map-inner"));
69
-
70
- // ── Error boundary (peer-missing or runtime-load failure) ───
71
-
72
- interface WorldHeatMapErrorBoundaryProps {
73
- fallback: React.ReactNode;
74
- children: React.ReactNode;
75
- }
76
- interface WorldHeatMapErrorBoundaryState {
77
- hasError: boolean;
78
- }
79
-
80
- class WorldHeatMapErrorBoundary extends React.Component<
81
- WorldHeatMapErrorBoundaryProps,
82
- WorldHeatMapErrorBoundaryState
83
- > {
84
- state: WorldHeatMapErrorBoundaryState = { hasError: false };
85
-
86
- /* c8 ignore start -- only triggers when the lazy-loaded Leaflet inner throws (peer missing or runtime load failure); both are jsdom-incompatible to simulate */
87
- static getDerivedStateFromError(): WorldHeatMapErrorBoundaryState {
88
- return { hasError: true };
89
- }
90
-
91
- render(): React.ReactNode {
92
- return this.state.hasError ? this.props.fallback : this.props.children;
93
- }
94
- /* c8 ignore stop */
95
- }
96
-
97
- // ── Shell (SSR / loading / error) ───────────────────────────
98
-
99
- type ShellState = "loading" | "error";
100
-
101
- interface ShellProps {
102
- height?: number | string;
103
- className?: string;
104
- state: ShellState;
105
- }
106
-
107
- function resolveHeight(height: number | string | undefined): string {
108
- if (height === undefined) return "100%";
109
- return typeof height === "number" ? `${height}px` : height;
110
- }
111
-
112
- function Shell({ height, className, state }: ShellProps) {
113
- const heightStr = resolveHeight(height);
114
- return (
115
- <div
116
- className={cn(
117
- "relative flex items-center justify-center overflow-hidden rounded-xl bg-muted/40 text-xs text-muted-foreground world-heat-map",
118
- className,
119
- )}
120
- style={{ height: heightStr }}
121
- >
122
- {state === "loading" ? (
123
- <>
124
- <svg
125
- width="20"
126
- height="20"
127
- viewBox="0 0 20 20"
128
- aria-hidden="true"
129
- className="mr-2 animate-spin opacity-50"
130
- >
131
- <title>Loading</title>
132
- <circle
133
- cx="10"
134
- cy="10"
135
- r="8"
136
- fill="none"
137
- stroke="currentColor"
138
- strokeWidth="2"
139
- strokeDasharray="40"
140
- strokeLinecap="round"
141
- />
142
- </svg>
143
- Loading map…
144
- </>
145
- ) : (
146
- <div className="px-4 py-2 text-center">
147
- Map dependencies missing — install <code>leaflet</code> and <code>react-leaflet</code>.
148
- </div>
149
- )}
150
- </div>
151
- );
152
- }
153
-
154
- // ── Public component ────────────────────────────────────────
155
-
156
- export function WorldHeatMap(props: WorldHeatMapProps) {
157
- const [mounted, setMounted] = React.useState(false);
158
-
159
- React.useEffect(() => {
160
- setMounted(true);
161
- }, []);
162
-
163
- // SSR / hydration guard — prevents `window`/`document` access during the
164
- // server pass and the first client render before hydration completes.
165
- if (!mounted) {
166
- return <Shell height={props.height} className={props.className} state="loading" />;
167
- }
168
-
169
- /* c8 ignore start -- post-mount tree mounts the lazy Leaflet inner; jsdom can't run Leaflet so the Suspense + ErrorBoundary tree is verified visually in the docs */
170
- return (
171
- <WorldHeatMapErrorBoundary
172
- fallback={<Shell height={props.height} className={props.className} state="error" />}
173
- >
174
- <React.Suspense
175
- fallback={<Shell height={props.height} className={props.className} state="loading" />}
176
- >
177
- <WorldHeatMapInner {...props} />
178
- </React.Suspense>
179
- </WorldHeatMapErrorBoundary>
180
- );
181
- /* c8 ignore stop */
182
- }
183
-
184
- WorldHeatMap.displayName = "WorldHeatMap";
@@ -1,12 +0,0 @@
1
- # Molecules
2
-
3
- Small compositions of atoms. Meaningful UI semantics. No app-state model.
4
-
5
- **Can import**: `tokens/`, `lib/utils`, `atoms/`, React.
6
-
7
- **Cannot import**: anything from `organisms/` or `templates/`.
8
-
9
- Molecules don't own interactive state more complex than a local toggle. If it
10
- has open/close state, selection, or form integration, it's an organism.
11
-
12
- See [CONTRIBUTING.md](../../../CONTRIBUTING.md) for the full atomic-design rules.
@@ -1,73 +0,0 @@
1
- "use client";
2
-
3
- import type * as React from "react";
4
-
5
- import { cn } from "../../lib/utils";
6
- import { Button } from "../atoms/button";
7
- import { Icon } from "../atoms/icon";
8
-
9
- export interface ActionBarAction {
10
- label: string;
11
- onClick: () => void;
12
- icon?: React.ReactNode;
13
- loading?: boolean;
14
- disabled?: boolean;
15
- }
16
-
17
- export interface ActionBarSecondary extends ActionBarAction {
18
- variant?: "outline" | "ghost";
19
- }
20
-
21
- export interface ActionBarProps {
22
- primaryAction?: ActionBarAction;
23
- secondaryActions?: ActionBarSecondary[];
24
- align?: "left" | "right" | "center" | "space-between";
25
- className?: string;
26
- }
27
-
28
- const ALIGN: Record<NonNullable<ActionBarProps["align"]>, string> = {
29
- left: "justify-start",
30
- right: "justify-end",
31
- center: "justify-center",
32
- "space-between": "justify-between",
33
- };
34
-
35
- export function ActionBar({
36
- primaryAction,
37
- secondaryActions = [],
38
- align = "right",
39
- className,
40
- }: ActionBarProps) {
41
- return (
42
- <div className={cn("flex items-center gap-2", ALIGN[align], className)}>
43
- {secondaryActions.map((a) => (
44
- <Button
45
- key={a.label}
46
- onClick={a.onClick}
47
- disabled={a.disabled || a.loading}
48
- variant={a.variant ?? "outline"}
49
- size="sm"
50
- >
51
- {a.icon && <span className="mr-1">{a.icon}</span>}
52
- {a.label}
53
- </Button>
54
- ))}
55
- {primaryAction && (
56
- <Button
57
- onClick={primaryAction.onClick}
58
- disabled={primaryAction.disabled || primaryAction.loading}
59
- size="sm"
60
- >
61
- {primaryAction.loading ? (
62
- <Icon name="LoaderCircle" className="mr-1 h-4 w-4 animate-spin" />
63
- ) : (
64
- primaryAction.icon && <span className="mr-1">{primaryAction.icon}</span>
65
- )}
66
- {primaryAction.label}
67
- </Button>
68
- )}
69
- </div>
70
- );
71
- }
72
-
73
- ActionBar.displayName = "ActionBar";
@@ -1,74 +0,0 @@
1
- import * as React from "react";
2
-
3
- import { cn } from "../../lib/utils";
4
-
5
- export interface ActivityItemProps extends React.HTMLAttributes<HTMLLIElement> {
6
- /** Bold subject (the actor). */
7
- subject: React.ReactNode;
8
- /** Muted action verb + object. */
9
- action: React.ReactNode;
10
- /** Right-aligned timestamp. */
11
- timestamp?: React.ReactNode;
12
- /** Leading icon or avatar slot. */
13
- leading?: React.ReactNode;
14
- /** When provided, the row becomes clickable. */
15
- onClick?: () => void;
16
- /**
17
- * Index in a list — when 0, the top border is suppressed so consecutive
18
- * items only render dividers between rows. Defaults to 1 (border on).
19
- */
20
- index?: number;
21
- }
22
-
23
- export const ActivityItem = React.forwardRef<HTMLLIElement, ActivityItemProps>(
24
- ({ subject, action, timestamp, leading, onClick, index = 1, className, ...props }, ref) => {
25
- const Row = onClick ? "button" : "div";
26
- return (
27
- <li
28
- ref={ref}
29
- className={cn(
30
- "flex items-start gap-3 py-3",
31
- index > 0 && "border-t border-border",
32
- className,
33
- )}
34
- {...props}
35
- >
36
- {leading && <div className="shrink-0">{leading}</div>}
37
- <Row
38
- type={onClick ? "button" : undefined}
39
- onClick={onClick}
40
- className={cn(
41
- "flex w-full items-start justify-between gap-4 text-left",
42
- onClick && "transition-colors hover:text-foreground",
43
- )}
44
- >
45
- <p className="m-0 min-w-0 text-[13.5px]">
46
- <span className="font-medium text-foreground">{subject}</span>{" "}
47
- <span className="text-muted-foreground">{action}</span>
48
- </p>
49
- {timestamp != null && (
50
- <span className="shrink-0 font-mono text-[11px] text-muted-foreground">
51
- {timestamp}
52
- </span>
53
- )}
54
- </Row>
55
- </li>
56
- );
57
- },
58
- );
59
- ActivityItem.displayName = "ActivityItem";
60
-
61
- export interface ActivityFeedProps extends React.HTMLAttributes<HTMLUListElement> {
62
- children?: React.ReactNode;
63
- }
64
-
65
- export const ActivityFeed = React.forwardRef<HTMLUListElement, ActivityFeedProps>(
66
- ({ children, className, ...props }, ref) => {
67
- return (
68
- <ul ref={ref} className={cn("m-0 list-none p-0", className)} {...props}>
69
- {children}
70
- </ul>
71
- );
72
- },
73
- );
74
- ActivityFeed.displayName = "ActivityFeed";