@spatika/react 1.0.0 → 1.2.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/AGENTS.md +71 -0
- package/README.md +4 -3
- package/dist/composites/AccentRule.d.ts +6 -0
- package/dist/composites/AccentRule.d.ts.map +1 -0
- package/dist/composites/AccentRule.js +6 -0
- package/dist/composites/AvailabilityBadge.d.ts +9 -0
- package/dist/composites/AvailabilityBadge.d.ts.map +1 -0
- package/dist/composites/AvailabilityBadge.js +9 -0
- package/dist/composites/Card.d.ts +7 -1
- package/dist/composites/Card.d.ts.map +1 -1
- package/dist/composites/Card.js +10 -0
- package/dist/composites/CareerCard.d.ts +20 -0
- package/dist/composites/CareerCard.d.ts.map +1 -0
- package/dist/composites/CareerCard.js +17 -0
- package/dist/composites/ChartDataGrid.d.ts +33 -0
- package/dist/composites/ChartDataGrid.d.ts.map +1 -0
- package/dist/composites/ChartDataGrid.js +77 -0
- package/dist/composites/ContactLink.d.ts +15 -0
- package/dist/composites/ContactLink.d.ts.map +1 -0
- package/dist/composites/ContactLink.js +8 -0
- package/dist/composites/CoverHero.d.ts +1 -1
- package/dist/composites/CoverHero.d.ts.map +1 -1
- package/dist/composites/CoverHero.js +2 -2
- package/dist/composites/DisplayHeading.d.ts +9 -0
- package/dist/composites/DisplayHeading.d.ts.map +1 -0
- package/dist/composites/DisplayHeading.js +6 -0
- package/dist/composites/EntityMediaCard.d.ts +1 -1
- package/dist/composites/EntityMediaCard.d.ts.map +1 -1
- package/dist/composites/EntityMediaCard.js +12 -4
- package/dist/composites/EventCalendar.d.ts +35 -0
- package/dist/composites/EventCalendar.d.ts.map +1 -0
- package/dist/composites/EventCalendar.js +339 -0
- package/dist/composites/EventEditor.d.ts +22 -0
- package/dist/composites/EventEditor.d.ts.map +1 -0
- package/dist/composites/EventEditor.js +86 -0
- package/dist/composites/EventTimeline.d.ts +24 -0
- package/dist/composites/EventTimeline.d.ts.map +1 -0
- package/dist/composites/EventTimeline.js +184 -0
- package/dist/composites/FloatChip.d.ts +15 -0
- package/dist/composites/FloatChip.d.ts.map +1 -0
- package/dist/composites/FloatChip.js +7 -0
- package/dist/composites/GradientText.d.ts +11 -0
- package/dist/composites/GradientText.d.ts.map +1 -0
- package/dist/composites/GradientText.js +6 -0
- package/dist/composites/IconTile.d.ts +10 -0
- package/dist/composites/IconTile.d.ts.map +1 -0
- package/dist/composites/IconTile.js +7 -0
- package/dist/composites/MapChart.d.ts +24 -0
- package/dist/composites/MapChart.d.ts.map +1 -0
- package/dist/composites/MapChart.js +58 -0
- package/dist/composites/MetaChip.d.ts +9 -0
- package/dist/composites/MetaChip.d.ts.map +1 -0
- package/dist/composites/MetaChip.js +6 -0
- package/dist/composites/PresenceDot.d.ts +10 -0
- package/dist/composites/PresenceDot.d.ts.map +1 -0
- package/dist/composites/PresenceDot.js +10 -0
- package/dist/composites/ProfileHero.d.ts +6 -4
- package/dist/composites/ProfileHero.d.ts.map +1 -1
- package/dist/composites/ProfileHero.js +8 -8
- package/dist/composites/ProjectCard.d.ts +12 -0
- package/dist/composites/ProjectCard.d.ts.map +1 -0
- package/dist/composites/ProjectCard.js +9 -0
- package/dist/composites/ScatterWebGL.d.ts +22 -0
- package/dist/composites/ScatterWebGL.d.ts.map +1 -0
- package/dist/composites/ScatterWebGL.js +45 -0
- package/dist/composites/SectionHeading.d.ts +16 -0
- package/dist/composites/SectionHeading.d.ts.map +1 -0
- package/dist/composites/SectionHeading.js +12 -0
- package/dist/composites/SiteFooter.d.ts +18 -0
- package/dist/composites/SiteFooter.d.ts.map +1 -0
- package/dist/composites/SiteFooter.js +15 -0
- package/dist/composites/SiteNav.d.ts +25 -0
- package/dist/composites/SiteNav.d.ts.map +1 -0
- package/dist/composites/SiteNav.js +44 -0
- package/dist/composites/StatCard.d.ts +7 -1
- package/dist/composites/StatCard.d.ts.map +1 -1
- package/dist/composites/StatCard.js +5 -2
- package/dist/composites/StatusDot.d.ts +1 -1
- package/dist/composites/Tag.d.ts +11 -0
- package/dist/composites/Tag.d.ts.map +1 -0
- package/dist/composites/Tag.js +33 -0
- package/dist/composites/Wordmark.d.ts +19 -0
- package/dist/composites/Wordmark.d.ts.map +1 -0
- package/dist/composites/Wordmark.js +15 -0
- package/dist/composites/cartesian-charts.d.ts +138 -0
- package/dist/composites/cartesian-charts.d.ts.map +1 -0
- package/dist/composites/cartesian-charts.js +401 -0
- package/dist/composites/chart-composition.d.ts +139 -0
- package/dist/composites/chart-composition.d.ts.map +1 -0
- package/dist/composites/chart-composition.js +486 -0
- package/dist/composites/chart-ui.d.ts +83 -0
- package/dist/composites/chart-ui.d.ts.map +1 -0
- package/dist/composites/chart-ui.js +108 -0
- package/dist/composites/charts-3d.d.ts +37 -0
- package/dist/composites/charts-3d.d.ts.map +1 -0
- package/dist/composites/charts-3d.js +121 -0
- package/dist/composites/flow-charts.d.ts +89 -0
- package/dist/composites/flow-charts.d.ts.map +1 -0
- package/dist/composites/flow-charts.js +186 -0
- package/dist/composites/radial-charts.d.ts +109 -0
- package/dist/composites/radial-charts.d.ts.map +1 -0
- package/dist/composites/radial-charts.js +176 -0
- package/dist/composites/scheduler-ui.d.ts +43 -0
- package/dist/composites/scheduler-ui.d.ts.map +1 -0
- package/dist/composites/scheduler-ui.js +71 -0
- package/dist/index.d.ts +83 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +74 -7
- package/dist/lib/chart-export.d.ts +10 -0
- package/dist/lib/chart-export.d.ts.map +1 -0
- package/dist/lib/chart-export.js +102 -0
- package/dist/lib/charts.d.ts +235 -0
- package/dist/lib/charts.d.ts.map +1 -0
- package/dist/lib/charts.js +744 -0
- package/dist/lib/geo.d.ts +39 -0
- package/dist/lib/geo.d.ts.map +1 -0
- package/dist/lib/geo.js +101 -0
- package/dist/lib/iso.d.ts +21 -0
- package/dist/lib/iso.d.ts.map +1 -0
- package/dist/lib/iso.js +56 -0
- package/dist/lib/rrule.d.ts +20 -0
- package/dist/lib/rrule.d.ts.map +1 -0
- package/dist/lib/rrule.js +236 -0
- package/dist/lib/scheduler.d.ts +152 -0
- package/dist/lib/scheduler.d.ts.map +1 -0
- package/dist/lib/scheduler.js +501 -0
- package/dist/lib/webgl-scatter.d.ts +44 -0
- package/dist/lib/webgl-scatter.d.ts.map +1 -0
- package/dist/lib/webgl-scatter.js +185 -0
- package/dist/primitives/Accordion.d.ts +44 -20
- package/dist/primitives/Accordion.d.ts.map +1 -1
- package/dist/primitives/Accordion.js +82 -22
- package/dist/primitives/AppBar.d.ts +13 -0
- package/dist/primitives/AppBar.d.ts.map +1 -0
- package/dist/primitives/AppBar.js +27 -0
- package/dist/primitives/Autocomplete.d.ts +28 -0
- package/dist/primitives/Autocomplete.d.ts.map +1 -0
- package/dist/primitives/Autocomplete.js +118 -0
- package/dist/primitives/Avatar.d.ts +13 -1
- package/dist/primitives/Avatar.d.ts.map +1 -1
- package/dist/primitives/Avatar.js +17 -2
- package/dist/primitives/Backdrop.d.ts +17 -0
- package/dist/primitives/Backdrop.d.ts.map +1 -0
- package/dist/primitives/Backdrop.js +17 -0
- package/dist/primitives/BottomNavigation.d.ts +23 -0
- package/dist/primitives/BottomNavigation.d.ts.map +1 -0
- package/dist/primitives/BottomNavigation.js +30 -0
- package/dist/primitives/Button.d.ts +4 -4
- package/dist/primitives/Button.d.ts.map +1 -1
- package/dist/primitives/Button.js +2 -0
- package/dist/primitives/ButtonBase.d.ts +14 -0
- package/dist/primitives/ButtonBase.d.ts.map +1 -0
- package/dist/primitives/ButtonBase.js +13 -0
- package/dist/primitives/ButtonGroup.d.ts +25 -0
- package/dist/primitives/ButtonGroup.d.ts.map +1 -0
- package/dist/primitives/ButtonGroup.js +44 -0
- package/dist/primitives/CircularProgress.d.ts +16 -0
- package/dist/primitives/CircularProgress.d.ts.map +1 -0
- package/dist/primitives/CircularProgress.js +17 -0
- package/dist/primitives/ClickAwayListener.d.ts +16 -0
- package/dist/primitives/ClickAwayListener.d.ts.map +1 -0
- package/dist/primitives/ClickAwayListener.js +33 -0
- package/dist/primitives/Collapse.d.ts +19 -0
- package/dist/primitives/Collapse.d.ts.map +1 -0
- package/dist/primitives/Collapse.js +14 -0
- package/dist/primitives/Container.d.ts +13 -0
- package/dist/primitives/Container.d.ts.map +1 -0
- package/dist/primitives/Container.js +26 -0
- package/dist/primitives/Fab.d.ts +22 -0
- package/dist/primitives/Fab.d.ts.map +1 -0
- package/dist/primitives/Fab.js +42 -0
- package/dist/primitives/FormControl.d.ts +27 -0
- package/dist/primitives/FormControl.d.ts.map +1 -0
- package/dist/primitives/FormControl.js +24 -0
- package/dist/primitives/FormControlLabel.d.ts +18 -0
- package/dist/primitives/FormControlLabel.d.ts.map +1 -0
- package/dist/primitives/FormControlLabel.js +19 -0
- package/dist/primitives/Grid.d.ts +32 -0
- package/dist/primitives/Grid.d.ts.map +1 -0
- package/dist/primitives/Grid.js +50 -0
- package/dist/primitives/IconButton.d.ts +22 -0
- package/dist/primitives/IconButton.d.ts.map +1 -0
- package/dist/primitives/IconButton.js +39 -0
- package/dist/primitives/ImageList.d.ts +31 -0
- package/dist/primitives/ImageList.d.ts.map +1 -0
- package/dist/primitives/ImageList.js +33 -0
- package/dist/primitives/InputAdornment.d.ts +28 -0
- package/dist/primitives/InputAdornment.d.ts.map +1 -0
- package/dist/primitives/InputAdornment.js +13 -0
- package/dist/primitives/Link.d.ts +17 -0
- package/dist/primitives/Link.d.ts.map +1 -0
- package/dist/primitives/Link.js +29 -0
- package/dist/primitives/List.d.ts +41 -0
- package/dist/primitives/List.d.ts.map +1 -0
- package/dist/primitives/List.js +22 -0
- package/dist/primitives/Masonry.d.ts +14 -0
- package/dist/primitives/Masonry.d.ts.map +1 -0
- package/dist/primitives/Masonry.js +15 -0
- package/dist/primitives/Menu.d.ts +28 -0
- package/dist/primitives/Menu.d.ts.map +1 -0
- package/dist/primitives/Menu.js +54 -0
- package/dist/primitives/MobileStepper.d.ts +15 -0
- package/dist/primitives/MobileStepper.d.ts.map +1 -0
- package/dist/primitives/MobileStepper.js +8 -0
- package/dist/primitives/Modal.d.ts +18 -0
- package/dist/primitives/Modal.d.ts.map +1 -0
- package/dist/primitives/Modal.js +29 -0
- package/dist/primitives/NoSsr.d.ts +15 -0
- package/dist/primitives/NoSsr.d.ts.map +1 -0
- package/dist/primitives/NoSsr.js +19 -0
- package/dist/primitives/Paper.d.ts +22 -0
- package/dist/primitives/Paper.d.ts.map +1 -0
- package/dist/primitives/Paper.js +33 -0
- package/dist/primitives/Progress.d.ts +6 -2
- package/dist/primitives/Progress.d.ts.map +1 -1
- package/dist/primitives/Progress.js +8 -3
- package/dist/primitives/Snackbar.d.ts +23 -0
- package/dist/primitives/Snackbar.d.ts.map +1 -0
- package/dist/primitives/Snackbar.js +33 -0
- package/dist/primitives/SpeedDial.d.ts +26 -0
- package/dist/primitives/SpeedDial.d.ts.map +1 -0
- package/dist/primitives/SpeedDial.js +26 -0
- package/dist/primitives/Stack.d.ts +15 -0
- package/dist/primitives/Stack.d.ts.map +1 -0
- package/dist/primitives/Stack.js +45 -0
- package/dist/primitives/StarRating.d.ts +19 -1
- package/dist/primitives/StarRating.d.ts.map +1 -1
- package/dist/primitives/StarRating.js +36 -6
- package/dist/primitives/Stepper.d.ts +36 -0
- package/dist/primitives/Stepper.d.ts.map +1 -0
- package/dist/primitives/Stepper.js +53 -0
- package/dist/primitives/TablePagination.d.ts +16 -0
- package/dist/primitives/TablePagination.d.ts.map +1 -0
- package/dist/primitives/TablePagination.js +12 -0
- package/dist/primitives/TextField.d.ts +24 -0
- package/dist/primitives/TextField.d.ts.map +1 -0
- package/dist/primitives/TextField.js +19 -0
- package/dist/primitives/TextareaAutosize.d.ts +11 -0
- package/dist/primitives/TextareaAutosize.d.ts.map +1 -0
- package/dist/primitives/TextareaAutosize.js +26 -0
- package/dist/primitives/ToggleButton.d.ts +28 -0
- package/dist/primitives/ToggleButton.d.ts.map +1 -0
- package/dist/primitives/ToggleButton.js +56 -0
- package/dist/primitives/TransferList.d.ts +21 -0
- package/dist/primitives/TransferList.d.ts.map +1 -0
- package/dist/primitives/TransferList.js +40 -0
- package/dist/primitives/Typography.d.ts +23 -0
- package/dist/primitives/Typography.d.ts.map +1 -0
- package/dist/primitives/Typography.js +69 -0
- package/dist/primitives/transitions.d.ts +29 -0
- package/dist/primitives/transitions.d.ts.map +1 -0
- package/dist/primitives/transitions.js +52 -0
- package/package.json +30 -5
- package/skills/spatika-ui/SKILL.md +96 -0
- package/src/composites/AccentRule.tsx +12 -0
- package/src/composites/AvailabilityBadge.tsx +32 -0
- package/src/composites/Card.tsx +51 -1
- package/src/composites/CareerCard.tsx +111 -0
- package/src/composites/ChartDataGrid.test.tsx +57 -0
- package/src/composites/ChartDataGrid.tsx +196 -0
- package/src/composites/ContactLink.tsx +66 -0
- package/src/composites/CoverHero.test.tsx +38 -0
- package/src/composites/CoverHero.tsx +29 -22
- package/src/composites/DisplayHeading.tsx +27 -0
- package/src/composites/EntityMediaCard.test.tsx +58 -0
- package/src/composites/EntityMediaCard.tsx +31 -13
- package/src/composites/EventCalendar.tsx +928 -0
- package/src/composites/EventEditor.tsx +268 -0
- package/src/composites/EventTimeline.tsx +357 -0
- package/src/composites/FloatChip.tsx +63 -0
- package/src/composites/GradientText.tsx +31 -0
- package/src/composites/IconTile.tsx +33 -0
- package/src/composites/MapChart.test.tsx +39 -0
- package/src/composites/MapChart.tsx +130 -0
- package/src/composites/MetaChip.tsx +24 -0
- package/src/composites/PresenceDot.tsx +42 -0
- package/src/composites/ProfileHero.test.tsx +33 -0
- package/src/composites/ProfileHero.tsx +96 -79
- package/src/composites/ProjectCard.tsx +65 -0
- package/src/composites/ScatterWebGL.test.tsx +25 -0
- package/src/composites/ScatterWebGL.tsx +79 -0
- package/src/composites/SectionHeading.tsx +61 -0
- package/src/composites/SiteFooter.test.tsx +37 -0
- package/src/composites/SiteFooter.tsx +79 -0
- package/src/composites/SiteNav.test.tsx +46 -0
- package/src/composites/SiteNav.tsx +150 -0
- package/src/composites/StatCard.test.tsx +30 -0
- package/src/composites/StatCard.tsx +36 -1
- package/src/composites/Tag.tsx +50 -0
- package/src/composites/Wordmark.test.tsx +23 -0
- package/src/composites/Wordmark.tsx +46 -0
- package/src/composites/cartesian-charts.test.tsx +52 -0
- package/src/composites/cartesian-charts.tsx +1287 -0
- package/src/composites/chart-composition.test.tsx +53 -0
- package/src/composites/chart-composition.tsx +898 -0
- package/src/composites/chart-ui.tsx +328 -0
- package/src/composites/charts-3d.test.tsx +25 -0
- package/src/composites/charts-3d.tsx +318 -0
- package/src/composites/charts.test.tsx +254 -0
- package/src/composites/chrome.test.tsx +29 -0
- package/src/composites/flow-charts.test.tsx +79 -0
- package/src/composites/flow-charts.tsx +607 -0
- package/src/composites/marketing.test.tsx +192 -0
- package/src/composites/radial-charts.test.tsx +74 -0
- package/src/composites/radial-charts.tsx +617 -0
- package/src/composites/scheduler-ui.tsx +304 -0
- package/src/composites/scheduler.test.tsx +194 -0
- package/src/index.test.ts +22 -0
- package/src/index.ts +300 -5
- package/src/lib/chart-export.test.ts +20 -0
- package/src/lib/chart-export.ts +99 -0
- package/src/lib/charts-platform.test.ts +93 -0
- package/src/lib/charts.test.ts +132 -0
- package/src/lib/charts.ts +1021 -0
- package/src/lib/geo.test.ts +42 -0
- package/src/lib/geo.ts +138 -0
- package/src/lib/iso.test.ts +22 -0
- package/src/lib/iso.ts +102 -0
- package/src/lib/rrule.test.ts +53 -0
- package/src/lib/rrule.ts +281 -0
- package/src/lib/scheduler.test.ts +186 -0
- package/src/lib/scheduler.ts +688 -0
- package/src/lib/tokens.test.ts +2 -2
- package/src/lib/webgl-scatter.test.ts +36 -0
- package/src/lib/webgl-scatter.ts +230 -0
- package/src/primitives/Accordion.tsx +218 -63
- package/src/primitives/AppBar.tsx +39 -0
- package/src/primitives/Autocomplete.tsx +236 -0
- package/src/primitives/Avatar.tsx +52 -1
- package/src/primitives/Backdrop.tsx +48 -0
- package/src/primitives/BottomNavigation.tsx +102 -0
- package/src/primitives/Button.tsx +3 -0
- package/src/primitives/ButtonBase.tsx +34 -0
- package/src/primitives/ButtonGroup.tsx +90 -0
- package/src/primitives/CircularProgress.tsx +104 -0
- package/src/primitives/ClickAwayListener.tsx +46 -0
- package/src/primitives/Collapse.tsx +39 -0
- package/src/primitives/Container.tsx +40 -0
- package/src/primitives/Fab.tsx +62 -0
- package/src/primitives/FormControl.tsx +90 -0
- package/src/primitives/FormControlLabel.tsx +51 -0
- package/src/primitives/Grid.tsx +101 -0
- package/src/primitives/IconButton.tsx +59 -0
- package/src/primitives/ImageList.test.tsx +48 -0
- package/src/primitives/ImageList.tsx +112 -0
- package/src/primitives/InputAdornment.tsx +86 -0
- package/src/primitives/Link.tsx +48 -0
- package/src/primitives/List.tsx +121 -0
- package/src/primitives/Masonry.tsx +32 -0
- package/src/primitives/Menu.tsx +112 -0
- package/src/primitives/MobileStepper.tsx +64 -0
- package/src/primitives/Modal.tsx +70 -0
- package/src/primitives/NoSsr.tsx +28 -0
- package/src/primitives/Paper.tsx +55 -0
- package/src/primitives/Progress.tsx +25 -7
- package/src/primitives/Snackbar.tsx +83 -0
- package/src/primitives/SpeedDial.tsx +110 -0
- package/src/primitives/Stack.tsx +88 -0
- package/src/primitives/StarRating.tsx +87 -28
- package/src/primitives/Stepper.tsx +184 -0
- package/src/primitives/TablePagination.tsx +58 -0
- package/src/primitives/TextField.tsx +105 -0
- package/src/primitives/TextareaAutosize.tsx +49 -0
- package/src/primitives/ToggleButton.tsx +148 -0
- package/src/primitives/TransferList.tsx +110 -0
- package/src/primitives/Typography.tsx +90 -0
- package/src/primitives/mui-more.test.tsx +76 -0
- package/src/primitives/mui.test.tsx +134 -0
- package/src/primitives/transitions.tsx +120 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { cn } from "../lib/cn";
|
|
3
|
+
|
|
4
|
+
export type GradientTextProps = {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
/** `static` clips a 3-stop theme gradient; `flow` animates it. */
|
|
7
|
+
variant?: "static" | "flow";
|
|
8
|
+
className?: string;
|
|
9
|
+
as?: "span" | "em" | "strong";
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** Theme-colored gradient type for wordmarks and display names. */
|
|
13
|
+
export function GradientText({
|
|
14
|
+
children,
|
|
15
|
+
variant = "static",
|
|
16
|
+
className,
|
|
17
|
+
as: Tag = "span",
|
|
18
|
+
}: GradientTextProps) {
|
|
19
|
+
return (
|
|
20
|
+
<Tag
|
|
21
|
+
data-slot="gradient-text"
|
|
22
|
+
data-variant={variant}
|
|
23
|
+
className={cn(
|
|
24
|
+
variant === "flow" ? "spk-gradient-text" : "spk-gradient-text-static",
|
|
25
|
+
className,
|
|
26
|
+
)}
|
|
27
|
+
>
|
|
28
|
+
{children}
|
|
29
|
+
</Tag>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { cn } from "../lib/cn";
|
|
3
|
+
import { Card } from "./Card";
|
|
4
|
+
|
|
5
|
+
export type IconTileProps = {
|
|
6
|
+
icon: ReactNode;
|
|
7
|
+
title: ReactNode;
|
|
8
|
+
description?: ReactNode;
|
|
9
|
+
className?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** Horizontal domain / feature tile: icon well + italic title + caption. */
|
|
13
|
+
export function IconTile({ icon, title, description, className }: IconTileProps) {
|
|
14
|
+
return (
|
|
15
|
+
<div data-slot="icon-tile">
|
|
16
|
+
<Card
|
|
17
|
+
variant="panel"
|
|
18
|
+
padding="md"
|
|
19
|
+
className={cn("group flex items-center gap-5 p-5", className)}
|
|
20
|
+
>
|
|
21
|
+
<div className="flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl border border-border bg-secondary text-3xl">
|
|
22
|
+
{icon}
|
|
23
|
+
</div>
|
|
24
|
+
<div className="min-w-0">
|
|
25
|
+
<p className="text-[16px] font-[900] italic text-foreground">{title}</p>
|
|
26
|
+
{description ? (
|
|
27
|
+
<p className="mt-0.5 text-[12px] font-bold text-muted-foreground">{description}</p>
|
|
28
|
+
) : null}
|
|
29
|
+
</div>
|
|
30
|
+
</Card>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { render } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { MapChart } from "./MapChart";
|
|
4
|
+
|
|
5
|
+
describe("MapChart", () => {
|
|
6
|
+
it("draws choropleth paths from GeoJSON", () => {
|
|
7
|
+
const { container } = render(
|
|
8
|
+
<MapChart
|
|
9
|
+
width={240}
|
|
10
|
+
height={160}
|
|
11
|
+
geoData={{
|
|
12
|
+
type: "FeatureCollection",
|
|
13
|
+
features: [
|
|
14
|
+
{
|
|
15
|
+
type: "Feature",
|
|
16
|
+
id: "a",
|
|
17
|
+
properties: { name: "A" },
|
|
18
|
+
geometry: {
|
|
19
|
+
type: "Polygon",
|
|
20
|
+
coordinates: [
|
|
21
|
+
[
|
|
22
|
+
[0, 0],
|
|
23
|
+
[4, 0],
|
|
24
|
+
[4, 3],
|
|
25
|
+
[0, 3],
|
|
26
|
+
[0, 0],
|
|
27
|
+
],
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
}}
|
|
33
|
+
series={[{ data: [{ id: "a", value: 12, label: "A" }] }]}
|
|
34
|
+
/>,
|
|
35
|
+
);
|
|
36
|
+
expect(container.querySelector('[data-slot="map-chart"]')).toBeTruthy();
|
|
37
|
+
expect(container.querySelector('[data-slot="map-chart"] path')).toBeTruthy();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { chartColor, formatChartNumber, type ChartMargin } from "../lib/charts";
|
|
2
|
+
import {
|
|
3
|
+
featureId,
|
|
4
|
+
fitFeatures,
|
|
5
|
+
geoPath,
|
|
6
|
+
type GeoJsonFeatureCollection,
|
|
7
|
+
type GeoProjectionKind,
|
|
8
|
+
} from "../lib/geo";
|
|
9
|
+
import { ChartFrame, useChartHover } from "./chart-ui";
|
|
10
|
+
|
|
11
|
+
export type MapShapeDatum = {
|
|
12
|
+
id: string | number;
|
|
13
|
+
value: number;
|
|
14
|
+
label?: string;
|
|
15
|
+
color?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type MapChartProps = {
|
|
19
|
+
geoData: GeoJsonFeatureCollection;
|
|
20
|
+
series?: Array<{ data: MapShapeDatum[] }>;
|
|
21
|
+
idProperty?: string;
|
|
22
|
+
projection?: GeoProjectionKind;
|
|
23
|
+
height?: number;
|
|
24
|
+
width?: number;
|
|
25
|
+
colors?: string[];
|
|
26
|
+
hideLegend?: boolean;
|
|
27
|
+
margin?: ChartMargin;
|
|
28
|
+
className?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function MapChart({
|
|
32
|
+
geoData,
|
|
33
|
+
series,
|
|
34
|
+
idProperty = "id",
|
|
35
|
+
projection = "mercator",
|
|
36
|
+
height = 280,
|
|
37
|
+
width,
|
|
38
|
+
colors,
|
|
39
|
+
hideLegend,
|
|
40
|
+
margin,
|
|
41
|
+
className,
|
|
42
|
+
}: MapChartProps) {
|
|
43
|
+
const features = geoData.features ?? [];
|
|
44
|
+
const data = series?.[0]?.data ?? [];
|
|
45
|
+
const byId = new Map(data.map((item) => [String(item.id), item]));
|
|
46
|
+
const values = data.map((item) => item.value);
|
|
47
|
+
const min = values.length ? Math.min(...values) : 0;
|
|
48
|
+
const max = values.length ? Math.max(...values) : 1;
|
|
49
|
+
const { hover, setHover, clear } = useChartHover();
|
|
50
|
+
const legend = hideLegend
|
|
51
|
+
? undefined
|
|
52
|
+
: data.slice(0, 8).map((item, i) => ({
|
|
53
|
+
id: String(item.id),
|
|
54
|
+
label: item.label ?? String(item.id),
|
|
55
|
+
color: item.color ?? choroplethFill(item.value, min, max, i, colors),
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<ChartFrame
|
|
60
|
+
slot="map-chart"
|
|
61
|
+
className={className}
|
|
62
|
+
width={width}
|
|
63
|
+
height={height}
|
|
64
|
+
margin={margin ?? { top: 8, right: 8, bottom: 8, left: 8 }}
|
|
65
|
+
legend={legend}
|
|
66
|
+
hover={hover}
|
|
67
|
+
aria-label="Map chart"
|
|
68
|
+
>
|
|
69
|
+
{({ width: w, height: h, m }) => {
|
|
70
|
+
const pad = Math.min(m.left, m.right, m.top, m.bottom);
|
|
71
|
+
const project = fitFeatures(features, w, h, pad, projection);
|
|
72
|
+
return (
|
|
73
|
+
<svg width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
|
|
74
|
+
{features.map((feature, i) => {
|
|
75
|
+
if (!feature.geometry) return null;
|
|
76
|
+
const id = featureId(feature, idProperty) || String(i);
|
|
77
|
+
const datum = byId.get(id);
|
|
78
|
+
const label =
|
|
79
|
+
datum?.label ??
|
|
80
|
+
(feature.properties?.name != null ? String(feature.properties.name) : id);
|
|
81
|
+
const fill = datum
|
|
82
|
+
? (datum.color ?? choroplethFill(datum.value, min, max, i, colors))
|
|
83
|
+
: "color-mix(in srgb, var(--muted) 55%, transparent)";
|
|
84
|
+
const d = geoPath(feature.geometry, project);
|
|
85
|
+
if (!d) return null;
|
|
86
|
+
return (
|
|
87
|
+
<path
|
|
88
|
+
key={id}
|
|
89
|
+
d={d}
|
|
90
|
+
fill={fill}
|
|
91
|
+
className="spk-chart-map-region spk-chart-mark"
|
|
92
|
+
onMouseEnter={(event) =>
|
|
93
|
+
setHover({
|
|
94
|
+
x: event.nativeEvent.offsetX,
|
|
95
|
+
y: event.nativeEvent.offsetY,
|
|
96
|
+
title: label,
|
|
97
|
+
items: datum
|
|
98
|
+
? [
|
|
99
|
+
{
|
|
100
|
+
color: fill,
|
|
101
|
+
label: label,
|
|
102
|
+
value: formatChartNumber(datum.value),
|
|
103
|
+
},
|
|
104
|
+
]
|
|
105
|
+
: [{ color: fill, label: label, value: "—" }],
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
onMouseLeave={clear}
|
|
109
|
+
/>
|
|
110
|
+
);
|
|
111
|
+
})}
|
|
112
|
+
</svg>
|
|
113
|
+
);
|
|
114
|
+
}}
|
|
115
|
+
</ChartFrame>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function choroplethFill(
|
|
120
|
+
value: number,
|
|
121
|
+
min: number,
|
|
122
|
+
max: number,
|
|
123
|
+
index: number,
|
|
124
|
+
colors?: string[],
|
|
125
|
+
): string {
|
|
126
|
+
const t = max === min ? 0.6 : (value - min) / (max - min);
|
|
127
|
+
const base = chartColor(index, colors);
|
|
128
|
+
const mix = Math.round(28 + t * 72);
|
|
129
|
+
return `color-mix(in srgb, ${base} ${mix}%, var(--muted))`;
|
|
130
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { cn } from "../lib/cn";
|
|
3
|
+
|
|
4
|
+
export type MetaChipProps = {
|
|
5
|
+
icon?: ReactNode;
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
className?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** Compact “powered by” chip: icon + label with a quiet ring. */
|
|
11
|
+
export function MetaChip({ icon, children, className }: MetaChipProps) {
|
|
12
|
+
return (
|
|
13
|
+
<span
|
|
14
|
+
data-slot="meta-chip"
|
|
15
|
+
className={cn(
|
|
16
|
+
"inline-flex items-center gap-1.5 rounded-lg bg-secondary px-2.5 py-1.5 text-[11px] font-medium text-muted-foreground ring-1 ring-border/80",
|
|
17
|
+
className,
|
|
18
|
+
)}
|
|
19
|
+
>
|
|
20
|
+
{icon ? <span className="inline-flex h-4 w-4 shrink-0 items-center justify-center opacity-90">{icon}</span> : null}
|
|
21
|
+
{children}
|
|
22
|
+
</span>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { cn } from "../lib/cn";
|
|
2
|
+
|
|
3
|
+
export type PresenceDotProps = {
|
|
4
|
+
tone?: "primary" | "success";
|
|
5
|
+
/** Dual-span scale ping. Defaults to true. */
|
|
6
|
+
ping?: boolean;
|
|
7
|
+
className?: string;
|
|
8
|
+
label?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const toneClass = {
|
|
12
|
+
primary: "bg-primary",
|
|
13
|
+
success: "bg-[var(--accent-info)]",
|
|
14
|
+
} as const;
|
|
15
|
+
|
|
16
|
+
/** Live status pip with optional ping halo (availability badges, hero chips). */
|
|
17
|
+
export function PresenceDot({
|
|
18
|
+
tone = "primary",
|
|
19
|
+
ping = true,
|
|
20
|
+
className,
|
|
21
|
+
label,
|
|
22
|
+
}: PresenceDotProps) {
|
|
23
|
+
return (
|
|
24
|
+
<span
|
|
25
|
+
data-slot="presence-dot"
|
|
26
|
+
role={label ? "img" : undefined}
|
|
27
|
+
aria-label={label}
|
|
28
|
+
className={cn("relative flex h-2 w-2", className)}
|
|
29
|
+
>
|
|
30
|
+
{ping ? (
|
|
31
|
+
<span
|
|
32
|
+
className={cn(
|
|
33
|
+
"absolute inline-flex h-full w-full animate-ping rounded-full opacity-75",
|
|
34
|
+
toneClass[tone],
|
|
35
|
+
)}
|
|
36
|
+
aria-hidden
|
|
37
|
+
/>
|
|
38
|
+
) : null}
|
|
39
|
+
<span className={cn("relative inline-flex h-2 w-2 rounded-full", toneClass[tone])} />
|
|
40
|
+
</span>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { ProfileHero } from "./ProfileHero";
|
|
5
|
+
|
|
6
|
+
describe("ProfileHero", () => {
|
|
7
|
+
it("renders name, alias, and a notched identity plate", () => {
|
|
8
|
+
const { container } = render(
|
|
9
|
+
<ProfileHero name="Alex Chen" alias="Product designer" coverSeed="alex" bleed={false} />,
|
|
10
|
+
);
|
|
11
|
+
expect(screen.getByRole("heading", { name: "Alex Chen" })).toBeInTheDocument();
|
|
12
|
+
expect(screen.getByText("Product designer")).toBeInTheDocument();
|
|
13
|
+
expect(container.querySelector(".profile-identity")).toBeTruthy();
|
|
14
|
+
expect(container.querySelector(".profile-identity__plate")).toBeTruthy();
|
|
15
|
+
expect(container.querySelector("[data-slot='profile-hero']")).not.toHaveAttribute(
|
|
16
|
+
"data-cover-bleed",
|
|
17
|
+
);
|
|
18
|
+
expect(container.querySelector("[data-slot='profile-hero']")).toHaveClass("w-full");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("keeps the avatar inert until onAvatarClick is provided", async () => {
|
|
22
|
+
const user = userEvent.setup();
|
|
23
|
+
const onAvatarClick = vi.fn();
|
|
24
|
+
const { rerender } = render(<ProfileHero name="Alex Chen" bleed={false} />);
|
|
25
|
+
expect(screen.getByRole("button", { name: "Alex Chen" })).toBeDisabled();
|
|
26
|
+
|
|
27
|
+
rerender(<ProfileHero name="Alex Chen" onAvatarClick={onAvatarClick} bleed={false} />);
|
|
28
|
+
const avatar = screen.getByRole("button", { name: "Alex Chen" });
|
|
29
|
+
expect(avatar).toBeEnabled();
|
|
30
|
+
await user.click(avatar);
|
|
31
|
+
expect(onAvatarClick).toHaveBeenCalledTimes(1);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -33,12 +33,14 @@ export type ProfileHeroProps = {
|
|
|
33
33
|
coverKind?: CoverPatternKind | null;
|
|
34
34
|
dark?: boolean;
|
|
35
35
|
warm?: boolean;
|
|
36
|
+
/** When false, keep the cover inside this frame (catalog previews). */
|
|
37
|
+
bleed?: boolean;
|
|
36
38
|
};
|
|
37
39
|
|
|
38
40
|
/**
|
|
39
|
-
* Profile / entity hero: cover
|
|
40
|
-
*
|
|
41
|
-
*
|
|
41
|
+
* Profile / entity hero: cover fills the frame, overlapping avatar notches
|
|
42
|
+
* the frosted identity plate. Stacked in a narrow column; left-notched once
|
|
43
|
+
* the plate is wide enough (container query, not viewport).
|
|
42
44
|
*/
|
|
43
45
|
export function ProfileHero({
|
|
44
46
|
name,
|
|
@@ -63,6 +65,7 @@ export function ProfileHero({
|
|
|
63
65
|
coverKind,
|
|
64
66
|
dark,
|
|
65
67
|
warm,
|
|
68
|
+
bleed = true,
|
|
66
69
|
}: ProfileHeroProps) {
|
|
67
70
|
const initials = getInitials(name);
|
|
68
71
|
const seed = coverSeed ?? name;
|
|
@@ -70,92 +73,106 @@ export function ProfileHero({
|
|
|
70
73
|
const ink = scene.headerInk;
|
|
71
74
|
|
|
72
75
|
return (
|
|
73
|
-
<div
|
|
76
|
+
<div
|
|
77
|
+
data-slot="profile-hero"
|
|
78
|
+
data-cover-bleed={bleed ? "true" : undefined}
|
|
79
|
+
className={cn(
|
|
80
|
+
"relative w-full",
|
|
81
|
+
bleed ? "overflow-visible" : "overflow-hidden rounded-[1.35rem] sm:rounded-[1.75rem]",
|
|
82
|
+
className,
|
|
83
|
+
)}
|
|
84
|
+
>
|
|
74
85
|
<div
|
|
75
|
-
className={cn(
|
|
76
|
-
|
|
77
|
-
coverClassName,
|
|
78
|
-
)}
|
|
86
|
+
className={cn("cover-media cover-media--profile", bleed && "sm:rounded-b-[2rem]")}
|
|
87
|
+
aria-hidden
|
|
79
88
|
>
|
|
80
|
-
|
|
81
|
-
{
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
89
|
+
{!hideImages && coverUrl ? (
|
|
90
|
+
<img src={coverUrl} alt="" className="absolute inset-0 size-full object-cover" />
|
|
91
|
+
) : (
|
|
92
|
+
<CoverPattern seed={seed} hue={coverHue} dark={dark} warm={warm} kind={coverKind} />
|
|
93
|
+
)}
|
|
94
|
+
<div className="absolute inset-x-0 top-0 h-8 bg-gradient-to-b from-black/[0.04] to-transparent dark:from-black/12" />
|
|
95
|
+
<div className="absolute inset-x-0 bottom-0 h-[22%] bg-gradient-to-t from-black/[0.03] via-transparent to-transparent dark:from-black/10" />
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div className="relative z-10 flex flex-col">
|
|
99
|
+
<div
|
|
100
|
+
className={cn(
|
|
101
|
+
"relative min-h-[8.5rem] sm:min-h-[11rem] lg:min-h-[12rem]",
|
|
102
|
+
coverClassName,
|
|
85
103
|
)}
|
|
86
|
-
|
|
87
|
-
<div className="absolute inset-x-0
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
) : null}
|
|
104
|
+
>
|
|
105
|
+
<div className="absolute inset-x-0 top-0 z-30 flex items-start justify-between gap-2 p-3 sm:p-4">
|
|
106
|
+
<div>{topLeading}</div>
|
|
107
|
+
{topActions ? (
|
|
108
|
+
<div
|
|
109
|
+
data-cover-ink={ink}
|
|
110
|
+
className="cover-floating-shell flex items-center gap-1 p-1"
|
|
111
|
+
>
|
|
112
|
+
{topActions}
|
|
113
|
+
</div>
|
|
114
|
+
) : null}
|
|
115
|
+
</div>
|
|
99
116
|
</div>
|
|
100
|
-
</div>
|
|
101
117
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
>
|
|
119
|
-
<span
|
|
120
|
-
className="flex items-center justify-center overflow-hidden rounded-full border-[3px] border-white bg-muted text-lg font-black leading-none text-primary shadow-[0_16px_40px_rgba(0,0,0,0.38)] dark:border-white/30"
|
|
121
|
-
style={{
|
|
122
|
-
width: "var(--profile-avatar-size)",
|
|
123
|
-
height: "var(--profile-avatar-size)",
|
|
124
|
-
}}
|
|
118
|
+
<div className="relative z-20 px-3 pb-4 sm:px-4 sm:pb-5 lg:px-6">
|
|
119
|
+
<div className={cn("profile-identity", identityClassName)}>
|
|
120
|
+
<div
|
|
121
|
+
aria-hidden
|
|
122
|
+
className={cn(
|
|
123
|
+
"pointer-events-none absolute inset-0 rounded-[1.35rem] profile-identity__plate",
|
|
124
|
+
coverPlateClass(ink),
|
|
125
|
+
)}
|
|
126
|
+
/>
|
|
127
|
+
<div className="profile-identity__body">
|
|
128
|
+
<button
|
|
129
|
+
type="button"
|
|
130
|
+
disabled={!onAvatarClick}
|
|
131
|
+
onClick={onAvatarClick}
|
|
132
|
+
aria-label={name}
|
|
133
|
+
className="profile-identity__avatar"
|
|
125
134
|
>
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
{avatarAccessory}
|
|
133
|
-
</button>
|
|
134
|
-
<div className="profile-identity__copy">
|
|
135
|
-
<div className="profile-identity__heading">
|
|
136
|
-
<h1
|
|
137
|
-
className={cn(
|
|
138
|
-
"truncate text-[1.15rem] font-black leading-tight tracking-tight sm:text-2xl",
|
|
139
|
-
coverTitleClass(ink),
|
|
140
|
-
)}
|
|
135
|
+
<span
|
|
136
|
+
className="flex items-center justify-center overflow-hidden rounded-full border-[3px] border-white bg-muted text-lg font-black leading-none text-primary shadow-[0_16px_40px_rgba(0,0,0,0.38)] dark:border-white/30"
|
|
137
|
+
style={{
|
|
138
|
+
width: "var(--profile-avatar-size)",
|
|
139
|
+
height: "var(--profile-avatar-size)",
|
|
140
|
+
}}
|
|
141
141
|
>
|
|
142
|
-
{
|
|
143
|
-
|
|
144
|
-
|
|
142
|
+
{!hideImages && avatarUrl ? (
|
|
143
|
+
<img src={avatarUrl} alt="" className="size-full object-cover" />
|
|
144
|
+
) : (
|
|
145
|
+
initials
|
|
146
|
+
)}
|
|
147
|
+
</span>
|
|
148
|
+
{avatarAccessory}
|
|
149
|
+
</button>
|
|
150
|
+
<div className="profile-identity__copy">
|
|
151
|
+
<div className="profile-identity__heading">
|
|
152
|
+
<h1
|
|
153
|
+
className={cn(
|
|
154
|
+
"truncate text-[1.15rem] font-black leading-tight tracking-tight sm:text-2xl",
|
|
155
|
+
coverTitleClass(ink),
|
|
156
|
+
)}
|
|
157
|
+
>
|
|
158
|
+
{name}
|
|
159
|
+
</h1>
|
|
160
|
+
{statusBadges}
|
|
161
|
+
</div>
|
|
162
|
+
{alias ? (
|
|
163
|
+
<p className={cn("mt-0.5 truncate text-sm font-medium", coverMutedClass(ink))}>
|
|
164
|
+
{alias}
|
|
165
|
+
</p>
|
|
166
|
+
) : null}
|
|
167
|
+
{metaLines ? (
|
|
168
|
+
<div className={cn("mt-2 space-y-1 text-sm", coverMutedClass(ink))}>{metaLines}</div>
|
|
169
|
+
) : null}
|
|
170
|
+
{stats ? <div className="mt-3">{stats}</div> : null}
|
|
171
|
+
{tags ? <div className="profile-identity__tags">{tags}</div> : null}
|
|
145
172
|
</div>
|
|
146
|
-
{alias ? (
|
|
147
|
-
<p className={cn("mt-0.5 truncate text-sm font-medium", coverMutedClass(ink))}>
|
|
148
|
-
{alias}
|
|
149
|
-
</p>
|
|
150
|
-
) : null}
|
|
151
|
-
{metaLines ? (
|
|
152
|
-
<div className={cn("mt-2 space-y-1 text-sm", coverMutedClass(ink))}>{metaLines}</div>
|
|
153
|
-
) : null}
|
|
154
|
-
{stats ? <div className="mt-3">{stats}</div> : null}
|
|
155
|
-
{tags ? <div className="profile-identity__tags">{tags}</div> : null}
|
|
156
173
|
</div>
|
|
174
|
+
{children ? <div className="relative z-10 px-3 pb-3 sm:px-6">{children}</div> : null}
|
|
157
175
|
</div>
|
|
158
|
-
{children ? <div className="relative z-10 px-3 pb-3 sm:px-6">{children}</div> : null}
|
|
159
176
|
</div>
|
|
160
177
|
</div>
|
|
161
178
|
</div>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { cn } from "../lib/cn";
|
|
3
|
+
import { Badge } from "../primitives/Badge";
|
|
4
|
+
import { Card, CardContent, CardHeader, CardTitle } from "./Card";
|
|
5
|
+
import { Tag } from "./Tag";
|
|
6
|
+
|
|
7
|
+
export type ProjectCardProps = {
|
|
8
|
+
title: ReactNode;
|
|
9
|
+
company?: ReactNode;
|
|
10
|
+
outcome?: ReactNode;
|
|
11
|
+
description?: ReactNode;
|
|
12
|
+
tags?: string[];
|
|
13
|
+
className?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Marketing project tile: title, company, outcome pill, body, tech tags. */
|
|
17
|
+
export function ProjectCard({
|
|
18
|
+
title,
|
|
19
|
+
company,
|
|
20
|
+
outcome,
|
|
21
|
+
description,
|
|
22
|
+
tags = [],
|
|
23
|
+
className,
|
|
24
|
+
}: ProjectCardProps) {
|
|
25
|
+
return (
|
|
26
|
+
<Card
|
|
27
|
+
variant="panel"
|
|
28
|
+
padding="md"
|
|
29
|
+
className={cn(
|
|
30
|
+
"group flex h-full flex-col hover:border-primary/30 hover:shadow-[0_12px_32px_color-mix(in_srgb,var(--primary)_7%,transparent)]",
|
|
31
|
+
className,
|
|
32
|
+
)}
|
|
33
|
+
>
|
|
34
|
+
<CardHeader className="mb-2">
|
|
35
|
+
<div className="min-w-0">
|
|
36
|
+
<CardTitle className="text-[15px] leading-snug font-[900] group-hover:text-primary sm:text-[16px]">
|
|
37
|
+
{title}
|
|
38
|
+
</CardTitle>
|
|
39
|
+
{company ? (
|
|
40
|
+
<p className="mt-0.5 text-[11px] font-bold tracking-wider text-muted-foreground uppercase">
|
|
41
|
+
{company}
|
|
42
|
+
</p>
|
|
43
|
+
) : null}
|
|
44
|
+
</div>
|
|
45
|
+
{outcome ? (
|
|
46
|
+
<Badge variant="warm" className="rounded-full px-2.5 py-1 text-[10px] font-black">
|
|
47
|
+
{outcome}
|
|
48
|
+
</Badge>
|
|
49
|
+
) : null}
|
|
50
|
+
</CardHeader>
|
|
51
|
+
<CardContent className="flex flex-1 flex-col">
|
|
52
|
+
{description ? (
|
|
53
|
+
<p className="mb-3 flex-1 text-[13px] leading-relaxed text-muted-foreground">{description}</p>
|
|
54
|
+
) : null}
|
|
55
|
+
{tags.length > 0 ? (
|
|
56
|
+
<div className="flex flex-wrap gap-1.5">
|
|
57
|
+
{tags.map((tag) => (
|
|
58
|
+
<Tag key={tag}>{tag}</Tag>
|
|
59
|
+
))}
|
|
60
|
+
</div>
|
|
61
|
+
) : null}
|
|
62
|
+
</CardContent>
|
|
63
|
+
</Card>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { render } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { ScatterWebGLLayer } from "./ScatterWebGL";
|
|
4
|
+
|
|
5
|
+
describe("ScatterWebGLLayer", () => {
|
|
6
|
+
it("renders a canvas layer and reports when WebGL is unavailable", () => {
|
|
7
|
+
const onUnavailable = vi.fn();
|
|
8
|
+
const { container } = render(
|
|
9
|
+
<ScatterWebGLLayer
|
|
10
|
+
width={120}
|
|
11
|
+
height={80}
|
|
12
|
+
series={[{ data: [{ x: 1, y: 2 }] }]}
|
|
13
|
+
meta={[{ id: "a", label: "A", color: "#2563eb" }]}
|
|
14
|
+
hidden={new Set()}
|
|
15
|
+
xAt={(value) => value}
|
|
16
|
+
yAt={(value) => value}
|
|
17
|
+
sizeAt={(z) => z}
|
|
18
|
+
bubble={false}
|
|
19
|
+
onUnavailable={onUnavailable}
|
|
20
|
+
/>,
|
|
21
|
+
);
|
|
22
|
+
expect(container.querySelector('[data-slot="scatter-webgl"]')).toBeTruthy();
|
|
23
|
+
expect(onUnavailable).toHaveBeenCalled();
|
|
24
|
+
});
|
|
25
|
+
});
|