@alpic-ai/ui 0.0.0-dev.g05467b7 → 0.0.0-dev.g05c89ce

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 (47) hide show
  1. package/dist/components/area-chart.d.mts +2 -0
  2. package/dist/components/area-chart.mjs +9 -3
  3. package/dist/components/bar-chart.d.mts +2 -0
  4. package/dist/components/bar-chart.mjs +9 -3
  5. package/dist/components/bar-list.d.mts +3 -0
  6. package/dist/components/bar-list.mjs +25 -12
  7. package/dist/components/chart-card.d.mts +1 -1
  8. package/dist/components/chart-card.mjs +1 -1
  9. package/dist/components/chart-container.d.mts +1 -1
  10. package/dist/components/chart-legend.d.mts +5 -0
  11. package/dist/components/chart-legend.mjs +11 -2
  12. package/dist/components/donut-chart.mjs +9 -5
  13. package/dist/components/form.mjs +1 -1
  14. package/dist/components/heatmap-chart.d.mts +8 -0
  15. package/dist/components/heatmap-chart.mjs +39 -8
  16. package/dist/components/line-chart.d.mts +2 -0
  17. package/dist/components/line-chart.mjs +10 -3
  18. package/dist/components/stat.d.mts +3 -1
  19. package/dist/components/stat.mjs +14 -4
  20. package/dist/components/wizard.d.mts +1 -19
  21. package/dist/components/wizard.mjs +1 -19
  22. package/dist/lib/chart.mjs +16 -1
  23. package/package.json +23 -23
  24. package/src/components/area-chart.tsx +12 -4
  25. package/src/components/bar-chart.tsx +12 -4
  26. package/src/components/bar-list.tsx +26 -10
  27. package/src/components/chart-card.tsx +8 -6
  28. package/src/components/chart-container.tsx +2 -0
  29. package/src/components/chart-legend.tsx +10 -2
  30. package/src/components/donut-chart.tsx +6 -6
  31. package/src/components/form.tsx +1 -1
  32. package/src/components/heatmap-chart.tsx +62 -18
  33. package/src/components/line-chart.tsx +18 -5
  34. package/src/components/stat.tsx +10 -6
  35. package/src/components/wizard.tsx +1 -35
  36. package/src/lib/chart.ts +34 -0
  37. package/src/stories/area-chart.stories.tsx +1 -3
  38. package/src/stories/bar-chart.stories.tsx +1 -3
  39. package/src/stories/bar-list.stories.tsx +1 -3
  40. package/src/stories/donut-chart.stories.tsx +1 -3
  41. package/src/stories/heatmap-chart.stories.tsx +1 -3
  42. package/src/stories/line-chart.stories.tsx +1 -3
  43. package/src/stories/wizard.stories.tsx +23 -5
  44. package/src/styles/tokens.css +0 -45
  45. package/dist/components/grid-fx.d.mts +0 -13
  46. package/dist/components/grid-fx.mjs +0 -188
  47. package/src/components/grid-fx.tsx +0 -238
@@ -16,7 +16,7 @@ import {
16
16
  } from "recharts";
17
17
 
18
18
  import { useReducedMotion } from "../hooks/use-reduced-motion";
19
- import { type ChartSeries, resolveSeries } from "../lib/chart";
19
+ import { type ChartSeries, makeXAxisTick, resolveSeries } from "../lib/chart";
20
20
  import type { ChartPaletteName } from "../lib/chart-palette";
21
21
  import { cn } from "../lib/cn";
22
22
  import type { ChartMarker } from "./area-chart";
@@ -32,6 +32,7 @@ export interface LineChartProps {
32
32
  series: ChartSeries[];
33
33
  curve?: keyof typeof CURVE_TYPE;
34
34
  legend?: boolean;
35
+ legendAlign?: "left" | "center" | "right";
35
36
  valueFlags?: boolean;
36
37
  dots?: boolean;
37
38
  height?: number;
@@ -52,6 +53,7 @@ function LineChart({
52
53
  series,
53
54
  curve = "monotone",
54
55
  legend = false,
56
+ legendAlign = "left",
55
57
  valueFlags = false,
56
58
  dots = false,
57
59
  height = 200,
@@ -170,11 +172,22 @@ function LineChart({
170
172
  no data in range
171
173
  </div>
172
174
  ) : (
173
- <ResponsiveContainer width="100%" height="100%">
175
+ <ResponsiveContainer width="100%" height="100%" initialDimension={{ width: 0, height }}>
174
176
  <RechartsLineChart data={data as Record<string, string | number>[]} margin={margin}>
175
177
  <CartesianGrid vertical={false} stroke={theme.grid} strokeDasharray="2 4" />
176
- <XAxis dataKey={index} {...axis} interval="preserveStartEnd" minTickGap={44} />
177
- <YAxis {...axis} width={yAxisWidth} tickFormatter={(value: number) => valueFormatter(value)} />
178
+ <XAxis
179
+ dataKey={index}
180
+ {...axis}
181
+ tick={makeXAxisTick(theme)}
182
+ interval="preserveStartEnd"
183
+ minTickGap={44}
184
+ />
185
+ <YAxis
186
+ {...axis}
187
+ width={yAxisWidth}
188
+ domain={["auto", "auto"]}
189
+ tickFormatter={(value: number) => valueFormatter(value)}
190
+ />
178
191
  <Tooltip
179
192
  offset={12}
180
193
  allowEscapeViewBox={{ x: false, y: false }}
@@ -256,7 +269,7 @@ function LineChart({
256
269
  )}
257
270
  </div>
258
271
 
259
- {legend && !isEmpty && <ChartLegend items={legendItems} style={{ paddingLeft: yAxisWidth }} />}
272
+ {legend && !isEmpty && <ChartLegend items={legendItems} align={legendAlign} insetLeft={yAxisWidth} />}
260
273
  </div>
261
274
  );
262
275
  }
@@ -31,8 +31,9 @@ export interface StatDelta {
31
31
  export interface StatProps extends React.ComponentProps<"div"> {
32
32
  value: React.ReactNode;
33
33
  unit?: string;
34
- delta?: StatDelta;
34
+ delta?: StatDelta | null;
35
35
  sparkline?: number[] | Array<{ value: number }>;
36
+ semantic?: "error" | "warning" | "success";
36
37
  }
37
38
 
38
39
  const toSparkData = (sparkline: StatProps["sparkline"]) =>
@@ -40,12 +41,15 @@ const toSparkData = (sparkline: StatProps["sparkline"]) =>
40
41
  typeof point === "number" ? { index, value: point } : { index, value: point.value },
41
42
  );
42
43
 
43
- function Stat({ value, unit, delta, sparkline, className, ...props }: StatProps) {
44
- const { palette } = useChartContext();
44
+ const SEMANTIC_KEY = { error: "destructive", warning: "warning", success: "success" } as const;
45
+
46
+ function Stat({ value, unit, delta, sparkline, semantic, className, ...props }: StatProps) {
47
+ const { palette, theme } = useChartContext();
45
48
  const gradientId = React.useId().replace(/:/g, "");
46
49
  const sparkData = React.useMemo(() => toSparkData(sparkline), [sparkline]);
50
+ const hasSpark = sparkData.some((point) => point.value > 0);
47
51
  // biome-ignore lint/style/noNonNullAssertion: palettes are never empty
48
- const sparkColor = palette[0]!;
52
+ const sparkColor = semantic ? theme[SEMANTIC_KEY[semantic]] : palette[0]!;
49
53
 
50
54
  const sentiment =
51
55
  delta && (delta.invert ? delta.direction === "down" : delta.direction === "up") ? "positive" : "negative";
@@ -64,9 +68,9 @@ function Stat({ value, unit, delta, sparkline, className, ...props }: StatProps)
64
68
  </DeltaPill>
65
69
  )}
66
70
  </div>
67
- {sparkData.length > 0 && (
71
+ {hasSpark && (
68
72
  <div className="h-9 w-full">
69
- <ResponsiveContainer width="100%" height="100%">
73
+ <ResponsiveContainer width="100%" height="100%" initialDimension={{ width: 0, height: 36 }}>
70
74
  <AreaChart data={sparkData} margin={{ top: 4, right: 2, bottom: 0, left: 2 }}>
71
75
  <defs>
72
76
  <linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
@@ -3,45 +3,12 @@
3
3
  /*
4
4
  * Wizard family — primitives for multi-step flows.
5
5
  *
6
- * - WizardSteps — vertical step rail (controlled by activeIdx + onSelect)
7
6
  * - WizardProgress — step counter + progress bar
8
- *
9
- * Consumers compose these inside whatever container they need (sticky aside, modal, etc.).
10
7
  */
11
8
 
12
9
  import type * as React from "react";
13
10
 
14
11
  import { cn } from "../lib/cn";
15
- import { TabsNav, TabsNavList, TabsNavTrigger } from "./tabs";
16
-
17
- interface WizardStep {
18
- id: string;
19
- label: string;
20
- }
21
-
22
- interface WizardStepsProps {
23
- steps: readonly WizardStep[];
24
- activeIdx: number;
25
- onSelect: (idx: number) => void;
26
- ariaLabel?: string;
27
- className?: string;
28
- }
29
-
30
- function WizardSteps({ steps, activeIdx, onSelect, ariaLabel = "Wizard steps", className }: WizardStepsProps) {
31
- return (
32
- <TabsNav orientation="vertical" aria-label={ariaLabel} className={className}>
33
- <TabsNavList>
34
- {steps.map((step, idx) => (
35
- <TabsNavTrigger key={step.id} active={idx === activeIdx} asChild>
36
- <button type="button" onClick={() => onSelect(idx)} className="w-full justify-start text-left">
37
- {step.label}
38
- </button>
39
- </TabsNavTrigger>
40
- ))}
41
- </TabsNavList>
42
- </TabsNav>
43
- );
44
- }
45
12
 
46
13
  interface WizardProgressProps extends React.ComponentProps<"div"> {
47
14
  current: number;
@@ -65,5 +32,4 @@ function WizardProgress({ current, total, className, ...props }: WizardProgressP
65
32
  );
66
33
  }
67
34
 
68
- export type { WizardStep };
69
- export { WizardProgress, WizardSteps };
35
+ export { WizardProgress };
package/src/lib/chart.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import * as React from "react";
1
2
  import type { ChartTheme } from "../hooks/use-chart-theme";
2
3
  import { luminance, paletteColor } from "./chart-palette";
3
4
 
@@ -54,3 +55,36 @@ export const resolveSeries = (
54
55
  */
55
56
  export const orderByLuminance = (series: ResolvedSeries[]) =>
56
57
  [...series].sort((lower, upper) => luminance(lower.color) - luminance(upper.color));
58
+
59
+ export const makeXAxisTick =
60
+ (theme: ChartTheme) =>
61
+ ({
62
+ x,
63
+ y,
64
+ payload,
65
+ index,
66
+ visibleTicksCount,
67
+ }: {
68
+ x?: string | number;
69
+ y?: string | number;
70
+ payload?: { value?: string | number };
71
+ index?: number;
72
+ visibleTicksCount?: number;
73
+ }) => {
74
+ const isFirst = index === 0;
75
+ const isLast = visibleTicksCount != null && index === visibleTicksCount - 1;
76
+ const anchor = isFirst ? "start" : isLast ? "end" : "middle";
77
+ return React.createElement(
78
+ "text",
79
+ {
80
+ x: Number(x ?? 0),
81
+ y: Number(y ?? 0),
82
+ dy: 12,
83
+ textAnchor: anchor,
84
+ fill: theme.axisForeground,
85
+ fontFamily: theme.fontMono,
86
+ fontSize: 10,
87
+ },
88
+ String(payload?.value ?? ""),
89
+ );
90
+ };
@@ -2,7 +2,6 @@ import type { Story } from "@ladle/react";
2
2
 
3
3
  import { AreaChart } from "../components/area-chart";
4
4
  import { ChartCard } from "../components/chart-card";
5
- import { GridFx } from "../components/grid-fx";
6
5
  import { Stat } from "../components/stat";
7
6
 
8
7
  export default { title: "Charts/Area Chart" };
@@ -90,8 +89,7 @@ const latencyPeak = latency.reduce((best, row) => (row.p95 > best.p95 ? row : be
90
89
  const errorsPeak = errors.reduce((best, row) => (row.mcp + row.tool > best.mcp + best.tool ? row : best));
91
90
 
92
91
  export const AllVariants: Story = () => (
93
- <div className="chart-canvas mx-auto max-w-[1600px] p-8">
94
- <GridFx />
92
+ <div className="mx-auto max-w-[1600px] p-8">
95
93
  <div className="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3">
96
94
  <ChartCard
97
95
  palette="magenta"
@@ -2,7 +2,6 @@ import type { Story } from "@ladle/react";
2
2
 
3
3
  import { BarChart } from "../components/bar-chart";
4
4
  import { ChartCard } from "../components/chart-card";
5
- import { GridFx } from "../components/grid-fx";
6
5
  import { Stat } from "../components/stat";
7
6
 
8
7
  export default { title: "Charts/Bar Chart" };
@@ -77,8 +76,7 @@ const sessionsSpark = stacked.map((row) => CLIENTS.reduce((acc, client) => acc +
77
76
  const errorsPeak = errors.reduce((best, row) => (row.mcp + row.tool > best.mcp + best.tool ? row : best));
78
77
 
79
78
  export const AllVariants: Story = () => (
80
- <div className="chart-canvas mx-auto max-w-[1600px] p-8">
81
- <GridFx />
79
+ <div className="mx-auto max-w-[1600px] p-8">
82
80
  <div className="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3">
83
81
  <ChartCard
84
82
  palette="magenta"
@@ -2,7 +2,6 @@ import type { Story } from "@ladle/react";
2
2
 
3
3
  import { BarList } from "../components/bar-list";
4
4
  import { ChartCard } from "../components/chart-card";
5
- import { GridFx } from "../components/grid-fx";
6
5
  import { Stat } from "../components/stat";
7
6
 
8
7
  export default { title: "Charts/Bar List" };
@@ -53,8 +52,7 @@ const fmtK = (value: number) => {
53
52
  const toolCallsTotal = TOP_TOOLS.reduce((sum, row) => sum + row.calls, 0);
54
53
 
55
54
  export const AllVariants: Story = () => (
56
- <div className="chart-canvas mx-auto max-w-[1600px] p-8">
57
- <GridFx />
55
+ <div className="mx-auto max-w-[1600px] p-8">
58
56
  <div className="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3">
59
57
  <ChartCard palette="magenta" kicker="Last 7d" title="Top tools" description="Ranked · magenta ramp">
60
58
  <Stat value={fmtK(toolCallsTotal)} unit="calls" delta={{ value: 12.4, direction: "up" }} />
@@ -2,7 +2,6 @@ import type { Story } from "@ladle/react";
2
2
 
3
3
  import { ChartCard } from "../components/chart-card";
4
4
  import { DonutChart } from "../components/donut-chart";
5
- import { GridFx } from "../components/grid-fx";
6
5
  import { Stat } from "../components/stat";
7
6
 
8
7
  export default { title: "Charts/Donut Chart" };
@@ -73,8 +72,7 @@ const fmtK = (value: number) => {
73
72
  const clientsTotal = clients.reduce((sum, row) => sum + row.sessions, 0);
74
73
 
75
74
  export const AllVariants: Story = () => (
76
- <div className="chart-canvas mx-auto max-w-[1600px] p-8">
77
- <GridFx />
75
+ <div className="mx-auto max-w-[1600px] p-8">
78
76
  <div className="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3">
79
77
  <ChartCard palette="magenta" kicker="Last 7d" title="Sessions by client" description="Donut · share readout">
80
78
  <Stat value={fmtK(clientsTotal)} unit="sessions" delta={{ value: 6.2, direction: "up" }} />
@@ -1,7 +1,6 @@
1
1
  import type { Story } from "@ladle/react";
2
2
 
3
3
  import { ChartCard } from "../components/chart-card";
4
- import { GridFx } from "../components/grid-fx";
5
4
  import { HeatmapChart } from "../components/heatmap-chart";
6
5
 
7
6
  export default { title: "Charts/Heatmap" };
@@ -40,8 +39,7 @@ const HOURS = Array.from({ length: 24 }, (_, hour) => String(hour).padStart(2, "
40
39
  const nf = (value: number) => value.toLocaleString("en-US");
41
40
 
42
41
  export const AllVariants: Story = () => (
43
- <div className="chart-canvas mx-auto max-w-[1600px] p-8">
44
- <GridFx />
42
+ <div className="mx-auto max-w-[1600px] p-8">
45
43
  <div className="grid grid-cols-1 gap-6 xl:grid-cols-2">
46
44
  <ChartCard palette="magenta" accent="left" kicker="Last 7d" title="Activity" description="Square · hour × day">
47
45
  <HeatmapChart
@@ -1,7 +1,6 @@
1
1
  import type { Story } from "@ladle/react";
2
2
 
3
3
  import { ChartCard } from "../components/chart-card";
4
- import { GridFx } from "../components/grid-fx";
5
4
  import { LineChart } from "../components/line-chart";
6
5
  import { Stat } from "../components/stat";
7
6
 
@@ -64,8 +63,7 @@ const tokensSpark = tokens.map((row) => row.v);
64
63
  const latencyPeak = latency.reduce((best, row) => (row.p95 > best.p95 ? row : best));
65
64
 
66
65
  export const AllVariants: Story = () => (
67
- <div className="chart-canvas mx-auto max-w-[1600px] p-8">
68
- <GridFx />
66
+ <div className="mx-auto max-w-[1600px] p-8">
69
67
  <div className="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3">
70
68
  <ChartCard
71
69
  palette="cyan"
@@ -1,18 +1,36 @@
1
1
  import type { Story } from "@ladle/react";
2
2
  import { useState } from "react";
3
3
 
4
- import { WizardProgress, type WizardStep, WizardSteps } from "../components/wizard";
4
+ import { TabsNav, TabsNavList, TabsNavTrigger } from "../components/tabs";
5
+ import { WizardProgress } from "../components/wizard";
5
6
 
6
7
  const SECTION_HEADER = "type-text-xs font-medium text-muted-foreground uppercase tracking-wide pt-4";
7
8
 
8
- const steps: WizardStep[] = [
9
+ const steps = [
9
10
  { id: "overview", label: "Overview" },
10
11
  { id: "branding", label: "Branding & metadata" },
11
12
  { id: "auth", label: "Authentication" },
12
13
  { id: "tools", label: "Tools & test cases" },
13
- { id: "review", label: "Review & submit" },
14
+ { id: "review", label: "Review & export" },
14
15
  ];
15
16
 
17
+ /** The step rail is a vertical `TabsNav` composed with the consumer's selection state. */
18
+ function StepRail({ activeIdx, onSelect }: { activeIdx: number; onSelect: (idx: number) => void }) {
19
+ return (
20
+ <TabsNav orientation="vertical" aria-label="Submission steps">
21
+ <TabsNavList>
22
+ {steps.map((step, idx) => (
23
+ <TabsNavTrigger key={step.id} active={idx === activeIdx} asChild>
24
+ <button type="button" onClick={() => onSelect(idx)} className="w-full justify-start text-left">
25
+ {step.label}
26
+ </button>
27
+ </TabsNavTrigger>
28
+ ))}
29
+ </TabsNavList>
30
+ </TabsNav>
31
+ );
32
+ }
33
+
16
34
  export const AllVariants: Story = () => {
17
35
  const [activeIdx, setActiveIdx] = useState(1);
18
36
 
@@ -23,7 +41,7 @@ export const AllVariants: Story = () => {
23
41
  <p className={SECTION_HEADER}>Full rail — steps + progress</p>
24
42
  <div className="mt-4 flex gap-6">
25
43
  <aside className="basis-56 shrink-0 flex flex-col gap-4 self-start">
26
- <WizardSteps steps={steps} activeIdx={activeIdx} onSelect={setActiveIdx} ariaLabel="Submission steps" />
44
+ <StepRail activeIdx={activeIdx} onSelect={setActiveIdx} />
27
45
  <WizardProgress current={activeIdx + 1} total={steps.length} />
28
46
  </aside>
29
47
  <div className="flex-1 rounded-md border p-4">
@@ -36,7 +54,7 @@ export const AllVariants: Story = () => {
36
54
  <div>
37
55
  <p className={SECTION_HEADER}>Steps only</p>
38
56
  <div className="mt-4 max-w-56">
39
- <WizardSteps steps={steps} activeIdx={activeIdx} onSelect={setActiveIdx} />
57
+ <StepRail activeIdx={activeIdx} onSelect={setActiveIdx} />
40
58
  </div>
41
59
  </div>
42
60
 
@@ -447,51 +447,6 @@
447
447
  }
448
448
  }
449
449
 
450
- /* ─── Chart canvas — control-room atmosphere for analytics surfaces ───────────
451
- Apply `.chart-canvas` to a dashboard/page container holding chart cards: a
452
- dotted grid faded from the top, plus a dual brand glow in dark mode. The grid
453
- is masked away so it never bleeds into the cards stacked above it. */
454
-
455
- .chart-canvas {
456
- position: relative;
457
- isolation: isolate;
458
- }
459
-
460
- .chart-canvas::before {
461
- content: "";
462
- position: absolute;
463
- inset: 0;
464
- z-index: -1;
465
- pointer-events: none;
466
- background-image:
467
- linear-gradient(var(--color-sidebar-border) 1px, transparent 1px),
468
- linear-gradient(90deg, var(--color-sidebar-border) 1px, transparent 1px);
469
- background-size:
470
- 46px 46px,
471
- 46px 46px;
472
- opacity: 0.65;
473
- -webkit-mask-image: radial-gradient(120% 90% at 50% 0%, #000 35%, transparent 100%);
474
- mask-image: radial-gradient(120% 90% at 50% 0%, #000 35%, transparent 100%);
475
- }
476
-
477
- .dark .chart-canvas::before {
478
- background-image:
479
- radial-gradient(
480
- 900px 480px at 78% -8%,
481
- color-mix(in oklab, var(--color-cta-accent) 12%, transparent),
482
- transparent 60%
483
- ),
484
- radial-gradient(820px 520px at 8% 4%, color-mix(in oklab, var(--color-primary) 12%, transparent), transparent 60%),
485
- linear-gradient(var(--color-sidebar-border) 1px, transparent 1px),
486
- linear-gradient(90deg, var(--color-sidebar-border) 1px, transparent 1px);
487
- background-size:
488
- 100% 100%,
489
- 100% 100%,
490
- 46px 46px,
491
- 46px 46px;
492
- opacity: 0.6;
493
- }
494
-
495
450
  @media (prefers-reduced-motion: no-preference) {
496
451
  .chart-rise {
497
452
  animation: chart-rise 0.65s cubic-bezier(0.2, 0.7, 0.2, 1) both;
@@ -1,13 +0,0 @@
1
- import * as React$1 from "react";
2
-
3
- //#region src/components/grid-fx.d.ts
4
- declare function GridFx({
5
- className,
6
- cellSize,
7
- style,
8
- ...props
9
- }: React$1.ComponentProps<"canvas"> & {
10
- cellSize?: number;
11
- }): React$1.JSX.Element | null;
12
- //#endregion
13
- export { GridFx };
@@ -1,188 +0,0 @@
1
- "use client";
2
- import { cn } from "../lib/cn.mjs";
3
- import { useReducedMotion } from "../hooks/use-reduced-motion.mjs";
4
- import { jsx } from "react/jsx-runtime";
5
- import * as React$1 from "react";
6
- //#region src/components/grid-fx.tsx
7
- const CELL_SIZE = 46;
8
- const TTL_MIN = 42;
9
- const TTL_MAX = 78;
10
- const SPAWN_MIN = 180;
11
- const SPAWN_MAX = 480;
12
- const rand = (min, max) => min + Math.random() * (max - min);
13
- function resolveColors(element) {
14
- const styles = getComputedStyle(element);
15
- return {
16
- color: styles.getPropertyValue("--color-primary").trim() || "#e90060",
17
- colorHi: styles.getPropertyValue("--color-primary-hover").trim() || "#f22b79"
18
- };
19
- }
20
- function strokeFull(ctx, horiz, at, width, height) {
21
- ctx.beginPath();
22
- if (horiz) {
23
- ctx.moveTo(0, at);
24
- ctx.lineTo(width, at);
25
- } else {
26
- ctx.moveTo(at, 0);
27
- ctx.lineTo(at, height);
28
- }
29
- ctx.stroke();
30
- }
31
- function drawGlitchLine(ctx, line, width, height) {
32
- const { horiz, at, color, colorHi } = line;
33
- const span = horiz ? width : height;
34
- const progress = line.life / line.ttl;
35
- const envelope = progress < .08 ? progress / .08 : 1 - (progress - .08) / .92;
36
- const base = Math.max(0, envelope);
37
- ctx.lineCap = "round";
38
- const ghosts = [
39
- {
40
- offset: 0,
41
- alpha: .85,
42
- blur: 12
43
- },
44
- {
45
- offset: rand(-3, 3),
46
- alpha: .35,
47
- blur: 0
48
- },
49
- {
50
- offset: rand(-7, 7),
51
- alpha: .2,
52
- blur: 0
53
- }
54
- ];
55
- for (const ghost of ghosts) {
56
- ctx.globalAlpha = base * ghost.alpha * (Math.random() < .1 ? .3 : 1);
57
- ctx.strokeStyle = color;
58
- ctx.lineWidth = 1.5;
59
- ctx.shadowBlur = ghost.blur;
60
- ctx.shadowColor = color;
61
- strokeFull(ctx, horiz, at + ghost.offset, width, height);
62
- }
63
- if (Math.random() < .5) {
64
- ctx.globalAlpha = base * .6;
65
- ctx.lineWidth = 2;
66
- ctx.shadowBlur = 0;
67
- ctx.strokeStyle = colorHi;
68
- for (let segment = 0; segment < 3; segment++) {
69
- const start = rand(0, span * .85);
70
- const end = start + rand(20, 80);
71
- const jitter = rand(-4, 4);
72
- ctx.beginPath();
73
- if (horiz) {
74
- ctx.moveTo(start, at + jitter);
75
- ctx.lineTo(end, at + jitter);
76
- } else {
77
- ctx.moveTo(at + jitter, start);
78
- ctx.lineTo(at + jitter, end);
79
- }
80
- ctx.stroke();
81
- }
82
- }
83
- }
84
- function GridFx({ className, cellSize = CELL_SIZE, style, ...props }) {
85
- const reduced = useReducedMotion();
86
- const canvasRef = React$1.useRef(null);
87
- React$1.useEffect(() => {
88
- if (reduced) return;
89
- const canvas = canvasRef.current;
90
- const parent = canvas?.parentElement;
91
- const ctx = canvas?.getContext("2d");
92
- if (!canvas || !parent || !ctx) return;
93
- let width = 0;
94
- let height = 0;
95
- let dpr = 1;
96
- let frame = 0;
97
- let nextIn = rand(SPAWN_MIN, SPAWN_MAX);
98
- let onScreen = true;
99
- const lines = [];
100
- const resize = () => {
101
- dpr = Math.min(window.devicePixelRatio || 1, 2);
102
- width = parent.clientWidth;
103
- height = parent.clientHeight;
104
- canvas.width = width * dpr;
105
- canvas.height = height * dpr;
106
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
107
- };
108
- const spawn = () => {
109
- const horiz = Math.random() < .5;
110
- const tracks = Math.ceil((horiz ? height : width) / cellSize);
111
- const { color, colorHi } = resolveColors(canvas);
112
- lines.push({
113
- horiz,
114
- at: Math.floor(Math.random() * tracks) * cellSize + .5,
115
- life: 0,
116
- ttl: rand(TTL_MIN, TTL_MAX),
117
- color,
118
- colorHi
119
- });
120
- };
121
- const tick = () => {
122
- ctx.globalAlpha = 1;
123
- ctx.shadowBlur = 0;
124
- ctx.clearRect(0, 0, width, height);
125
- if (--nextIn <= 0) {
126
- spawn();
127
- nextIn = rand(SPAWN_MIN, SPAWN_MAX);
128
- }
129
- for (let index = lines.length - 1; index >= 0; index--) {
130
- const line = lines[index];
131
- if (!line) continue;
132
- line.life++;
133
- drawGlitchLine(ctx, line, width, height);
134
- if (line.life >= line.ttl) lines.splice(index, 1);
135
- }
136
- ctx.globalAlpha = 1;
137
- ctx.shadowBlur = 0;
138
- frame = requestAnimationFrame(tick);
139
- };
140
- const running = () => onScreen && document.visibilityState === "visible";
141
- const start = () => {
142
- if (!frame && running()) frame = requestAnimationFrame(tick);
143
- };
144
- const stop = () => {
145
- if (frame) cancelAnimationFrame(frame);
146
- frame = 0;
147
- ctx.clearRect(0, 0, width, height);
148
- };
149
- resize();
150
- start();
151
- const resizeObserver = new ResizeObserver(() => resize());
152
- resizeObserver.observe(parent);
153
- const intersectionObserver = new IntersectionObserver(([entry]) => {
154
- if (!entry) return;
155
- onScreen = entry.isIntersecting;
156
- if (onScreen) start();
157
- else stop();
158
- });
159
- intersectionObserver.observe(canvas);
160
- const onVisibility = () => {
161
- if (running()) start();
162
- else stop();
163
- };
164
- document.addEventListener("visibilitychange", onVisibility);
165
- return () => {
166
- stop();
167
- resizeObserver.disconnect();
168
- intersectionObserver.disconnect();
169
- document.removeEventListener("visibilitychange", onVisibility);
170
- };
171
- }, [reduced, cellSize]);
172
- if (reduced) return null;
173
- return /* @__PURE__ */ jsx("canvas", {
174
- ref: canvasRef,
175
- "aria-hidden": true,
176
- "data-slot": "grid-fx",
177
- className: cn("pointer-events-none absolute inset-0 h-full w-full", className),
178
- style: {
179
- zIndex: -1,
180
- WebkitMaskImage: "radial-gradient(120% 95% at 50% 0%, #000 30%, transparent 100%)",
181
- maskImage: "radial-gradient(120% 95% at 50% 0%, #000 30%, transparent 100%)",
182
- ...style
183
- },
184
- ...props
185
- });
186
- }
187
- //#endregion
188
- export { GridFx };