@cs2dak/react 0.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.
@@ -0,0 +1,35 @@
1
+ import type { TimelineEvent } from "@cs2dak/contract";
2
+ import { useState } from "react";
3
+
4
+ export interface RoundTimelineProps {
5
+ events: TimelineEvent[];
6
+ initialLimit?: number;
7
+ }
8
+
9
+ export function RoundTimeline({ events, initialLimit = 120 }: RoundTimelineProps) {
10
+ const [expanded, setExpanded] = useState(false);
11
+ const visibleEvents = expanded ? events : events.slice(0, initialLimit);
12
+ const hiddenCount = events.length - visibleEvents.length;
13
+
14
+ return (
15
+ <div className="dak-timeline">
16
+ {visibleEvents.map((event) => (
17
+ <div className="dak-timeline-row" key={event.id}>
18
+ <span className="dak-badge">R{event.roundNumber}</span>
19
+ <span className="dak-mono dak-muted">{event.clockLabel}</span>
20
+ <span>{event.label}</span>
21
+ </div>
22
+ ))}
23
+ {hiddenCount > 0 && (
24
+ <button className="dak-timeline-more" type="button" onClick={() => setExpanded(true)}>
25
+ 展开剩余 {hiddenCount} 条事件
26
+ </button>
27
+ )}
28
+ {expanded && events.length > initialLimit && (
29
+ <button className="dak-timeline-more" type="button" onClick={() => setExpanded(false)}>
30
+ 收起到前 {initialLimit} 条
31
+ </button>
32
+ )}
33
+ </div>
34
+ );
35
+ }
@@ -0,0 +1,47 @@
1
+ import React from "react";
2
+ import { describe, expect, it } from "vitest";
3
+ import { renderToStaticMarkup } from "react-dom/server";
4
+ import type { PlayerScoreboardRow } from "@cs2dak/contract";
5
+ import { ScoreboardTable } from "./ScoreboardTable";
6
+
7
+ // The component only reads a handful of fields; cast a minimal fixture rather
8
+ // than reconstruct the full (large) scoreboard schema.
9
+ const rows = [
10
+ {
11
+ steamId64: "76561198000000001",
12
+ name: "Alice",
13
+ teamKey: "teamA",
14
+ accountRR: 1.234,
15
+ rr: 1.1,
16
+ kills: 20,
17
+ deaths: 14,
18
+ assists: 5,
19
+ adr: 88.5,
20
+ kast: 72,
21
+ headshotPercent: 55,
22
+ entryKills: 4,
23
+ tradeKills: 3,
24
+ awpKills: 2,
25
+ utilityDamage: 120,
26
+ },
27
+ ] as unknown as PlayerScoreboardRow[];
28
+
29
+ describe("ScoreboardTable", () => {
30
+ it("renders plain rows when no onPlayerClick is given", () => {
31
+ const html = renderToStaticMarkup(React.createElement(ScoreboardTable, { rows }));
32
+ expect(html).toContain("Alice");
33
+ expect(html).not.toContain("dak-row-clickable");
34
+ expect(html).not.toContain('role="button"');
35
+ expect(html).not.toContain("tabindex");
36
+ });
37
+
38
+ it("marks rows interactive when onPlayerClick is provided", () => {
39
+ const html = renderToStaticMarkup(
40
+ React.createElement(ScoreboardTable, { rows, onPlayerClick: () => {} }),
41
+ );
42
+ expect(html).toContain("dak-row-clickable");
43
+ expect(html).toContain('role="button"');
44
+ expect(html).toContain('tabindex="0"');
45
+ expect(html).toContain("查看 Alice 详情");
46
+ });
47
+ });
@@ -0,0 +1,80 @@
1
+ import type { PlayerScoreboardRow, TeamKey } from "@cs2dak/contract";
2
+
3
+ export interface ScoreboardTableProps {
4
+ rows: PlayerScoreboardRow[];
5
+ /**
6
+ * When provided, each player row becomes clickable (mouse + keyboard) and
7
+ * reports the clicked player's steamId64. Lets an embedding app (RivalHub,
8
+ * CS2-insight-agent) wire up navigation without forking this component.
9
+ */
10
+ onPlayerClick?: (steamId64: string) => void;
11
+ }
12
+
13
+ function teamColor(teamKey: TeamKey): string {
14
+ return teamKey === "teamA" ? "var(--dak-accent)" : "var(--dak-accent-b)";
15
+ }
16
+
17
+ export function ScoreboardTable({ rows, onPlayerClick }: ScoreboardTableProps) {
18
+ return (
19
+ <table className="dak-table">
20
+ <thead>
21
+ <tr>
22
+ <th>选手</th>
23
+ <th>V2 RR</th>
24
+ <th>RR</th>
25
+ <th>K</th>
26
+ <th>D</th>
27
+ <th>A</th>
28
+ <th>ADR</th>
29
+ <th>KAST</th>
30
+ <th>HS</th>
31
+ <th>首杀</th>
32
+ <th>补枪</th>
33
+ <th>AWP</th>
34
+ <th>道具伤害</th>
35
+ </tr>
36
+ </thead>
37
+ <tbody>
38
+ {rows.map((row) => (
39
+ <tr
40
+ key={row.steamId64}
41
+ className={onPlayerClick ? "dak-row-clickable" : undefined}
42
+ onClick={onPlayerClick ? () => onPlayerClick(row.steamId64) : undefined}
43
+ onKeyDown={
44
+ onPlayerClick
45
+ ? (e) => {
46
+ if (e.key === "Enter" || e.key === " ") {
47
+ e.preventDefault();
48
+ onPlayerClick(row.steamId64);
49
+ }
50
+ }
51
+ : undefined
52
+ }
53
+ tabIndex={onPlayerClick ? 0 : undefined}
54
+ role={onPlayerClick ? "button" : undefined}
55
+ aria-label={onPlayerClick ? `查看 ${row.name} 详情` : undefined}
56
+ >
57
+ <td>
58
+ <span className="dak-team-chip">
59
+ <span className="dak-team-dot" style={{ background: teamColor(row.teamKey) }} />
60
+ <span>{row.name}</span>
61
+ </span>
62
+ </td>
63
+ <td className="dak-mono dak-rr">{row.accountRR.toFixed(3)}</td>
64
+ <td className="dak-mono dak-rr">{row.rr.toFixed(2)}</td>
65
+ <td className="dak-mono">{row.kills}</td>
66
+ <td className="dak-mono">{row.deaths}</td>
67
+ <td className="dak-mono">{row.assists}</td>
68
+ <td className="dak-mono">{row.adr.toFixed(1)}</td>
69
+ <td className="dak-mono">{row.kast.toFixed(1)}%</td>
70
+ <td className="dak-mono">{row.headshotPercent.toFixed(0)}%</td>
71
+ <td className="dak-mono">{row.entryKills}</td>
72
+ <td className="dak-mono">{row.tradeKills}</td>
73
+ <td className="dak-mono">{row.awpKills}</td>
74
+ <td className="dak-mono">{row.utilityDamage}</td>
75
+ </tr>
76
+ ))}
77
+ </tbody>
78
+ </table>
79
+ );
80
+ }
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ export { MatchWorkspace, AdminQaWorkspace } from "./components/MatchWorkspace";
2
+ export { DemoAnalysisDashboard } from "./components/DemoAnalysisDashboard";
3
+ export { ScoreboardTable } from "./components/ScoreboardTable";
4
+ export { RoundTimeline } from "./components/RoundTimeline";
5
+ export { EconomyPanel } from "./components/EconomyPanel";
6
+ export { EconomyConversionPanel } from "./components/EconomyConversionPanel";
7
+ export { HeatmapCanvas } from "./components/HeatmapCanvas";
8
+ export { KillFeed } from "./components/KillFeed";
9
+ export { QaReportPanel } from "./components/QaReportPanel";