From cbcb0d45946581c9954051dd6519e80b83cb4173 Mon Sep 17 00:00:00 2001
From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com>
Date: Tue, 5 May 2026 04:29:29 +0000
Subject: [PATCH] feat(share): redesign share button as popover with
consolidated actions
Promotes the chart-header share affordance from a small outlined
icon-button (easy to miss) to a filled brand-color trigger that opens a
popover containing: a one-line explanation, the current share URL
auto-selected for Cmd/Ctrl+C, a dedicated Copy button with feedback,
and the X / LinkedIn share buttons consolidated inside (previously
hidden on mobile behind the sm: breakpoint).
Adds share_popover_opened analytics event so popover discoverability
can be measured separately from copy / social conversion. Refactors
the four direct ShareButton + social-button consumers to use the
shared ChartShareActions wrapper.
Wires .test.tsx files into vitest's include pattern so the existing
chart-display-helpers.test.tsx and the new share-button.test.tsx run
in CI (the coverage exclude already listed *.test.tsx, but the
include pattern was matching only .test.ts).
Closes #268
Co-authored-by: functionstackx
---
.../ThroughputCalculatorDisplay.tsx | 11 +-
.../components/gpu-power/GpuPowerDisplay.tsx | 9 +-
.../gpu-specs/gpu-specs-content.tsx | 11 +-
.../submissions/SubmissionsDisplay.tsx | 9 +-
.../ui/chart-display-helpers.test.tsx | 8 +-
.../components/ui/chart-display-helpers.tsx | 11 +-
.../src/components/ui/share-button.test.tsx | 62 +++++++++
.../app/src/components/ui/share-button.tsx | 127 +++++++++++++-----
packages/app/vitest.config.ts | 2 +-
9 files changed, 171 insertions(+), 79 deletions(-)
create mode 100644 packages/app/src/components/ui/share-button.test.tsx
diff --git a/packages/app/src/components/calculator/ThroughputCalculatorDisplay.tsx b/packages/app/src/components/calculator/ThroughputCalculatorDisplay.tsx
index fe84bb3b..0e26093a 100644
--- a/packages/app/src/components/calculator/ThroughputCalculatorDisplay.tsx
+++ b/packages/app/src/components/calculator/ThroughputCalculatorDisplay.tsx
@@ -19,9 +19,8 @@ import {
import { ExternalLinkIcon } from '@/components/ui/external-link-icon';
import { Input } from '@/components/ui/input';
import { LabelWithTooltip } from '@/components/ui/label-with-tooltip';
-import { ShareButton } from '@/components/ui/share-button';
+import { ChartShareActions } from '@/components/ui/chart-display-helpers';
import { UnofficialDomainNotice } from '@/components/ui/unofficial-domain-notice';
-import { ShareTwitterButton, ShareLinkedInButton } from '@/components/share-buttons';
import { TooltipProvider } from '@/components/ui/tooltip';
import { MultiSelect } from '@/components/ui/multi-select';
import { SegmentedToggle, type SegmentedToggleOption } from '@/components/ui/segmented-toggle';
@@ -420,13 +419,7 @@ export default function ThroughputCalculatorDisplay() {
across all GPUs. Values are interpolated from real benchmark data.
-
-
-
-
-
-
-
+
{/* Controls — grid layout matching inference chart controls */}
diff --git a/packages/app/src/components/gpu-power/GpuPowerDisplay.tsx b/packages/app/src/components/gpu-power/GpuPowerDisplay.tsx
index 8b71d6ed..29d59e04 100644
--- a/packages/app/src/components/gpu-power/GpuPowerDisplay.tsx
+++ b/packages/app/src/components/gpu-power/GpuPowerDisplay.tsx
@@ -12,9 +12,8 @@ import ChartLegend from '@/components/ui/chart-legend';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { SegmentedToggle, type SegmentedToggleOption } from '@/components/ui/segmented-toggle';
-import { ShareButton } from '@/components/ui/share-button';
+import { ChartShareActions } from '@/components/ui/chart-display-helpers';
import { UnofficialDomainNotice } from '@/components/ui/unofficial-domain-notice';
-import { ShareTwitterButton, ShareLinkedInButton } from '@/components/share-buttons';
import {
Select,
SelectContent,
@@ -259,11 +258,7 @@ export default function GpuMetricsDisplay() {
Re-lock feature gate
-
-
-
-
-
+
diff --git a/packages/app/src/components/gpu-specs/gpu-specs-content.tsx b/packages/app/src/components/gpu-specs/gpu-specs-content.tsx
index a6b0f370..42dd4ea4 100644
--- a/packages/app/src/components/gpu-specs/gpu-specs-content.tsx
+++ b/packages/app/src/components/gpu-specs/gpu-specs-content.tsx
@@ -6,9 +6,8 @@ import { BarChart3, Radar, Table2 } from 'lucide-react';
import { Card } from '@/components/ui/card';
import { SegmentedToggle, type SegmentedToggleOption } from '@/components/ui/segmented-toggle';
-import { ShareButton } from '@/components/ui/share-button';
+import { ChartShareActions } from '@/components/ui/chart-display-helpers';
import { UnofficialDomainNotice } from '@/components/ui/unofficial-domain-notice';
-import { ShareTwitterButton, ShareLinkedInButton } from '@/components/share-buttons';
import {
formatTflops,
getScaleUpDomainMemory,
@@ -321,13 +320,7 @@ export function GpuSpecsContent() {
compute performance, memory bandwidth, and interconnect details.
-
-
-
-
-
-
-
+
diff --git a/packages/app/src/components/submissions/SubmissionsDisplay.tsx b/packages/app/src/components/submissions/SubmissionsDisplay.tsx
index 09656817..738defe7 100644
--- a/packages/app/src/components/submissions/SubmissionsDisplay.tsx
+++ b/packages/app/src/components/submissions/SubmissionsDisplay.tsx
@@ -9,8 +9,7 @@ import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { ChartButtons } from '@/components/ui/chart-buttons';
import { SegmentedToggle, type SegmentedToggleOption } from '@/components/ui/segmented-toggle';
-import { ShareButton } from '@/components/ui/share-button';
-import { ShareTwitterButton, ShareLinkedInButton } from '@/components/share-buttons';
+import { ChartShareActions } from '@/components/ui/chart-display-helpers';
import { exportToCsv } from '@/lib/csv-export';
import { submissionsVolumeToCsv } from '@/lib/csv-export-helpers';
import { useSubmissions } from '@/hooks/api/use-submissions';
@@ -90,11 +89,7 @@ export default function SubmissionsDisplay() {
Re-lock feature gate
-
-
-
-
-
+
diff --git a/packages/app/src/components/ui/chart-display-helpers.test.tsx b/packages/app/src/components/ui/chart-display-helpers.test.tsx
index 76369f70..f78b570a 100644
--- a/packages/app/src/components/ui/chart-display-helpers.test.tsx
+++ b/packages/app/src/components/ui/chart-display-helpers.test.tsx
@@ -34,12 +34,12 @@ afterEach(() => {
});
describe('ChartShareActions', () => {
- it('renders the shared copy-link, X, and LinkedIn buttons with stable test ids', () => {
+ it('renders the share popover trigger', () => {
renderUi();
- expect(container.querySelector('[data-testid="share-button"]')).not.toBeNull();
- expect(container.querySelector('[data-testid="share-twitter"]')).not.toBeNull();
- expect(container.querySelector('[data-testid="share-linkedin"]')).not.toBeNull();
+ const trigger = container.querySelector('[data-testid="share-button"]');
+ expect(trigger).not.toBeNull();
+ expect(trigger?.textContent).toContain('Share');
});
});
diff --git a/packages/app/src/components/ui/chart-display-helpers.tsx b/packages/app/src/components/ui/chart-display-helpers.tsx
index ee8c7f16..724eb152 100644
--- a/packages/app/src/components/ui/chart-display-helpers.tsx
+++ b/packages/app/src/components/ui/chart-display-helpers.tsx
@@ -1,7 +1,6 @@
import Link from 'next/link';
import type { ReactNode } from 'react';
-import { ShareTwitterButton, ShareLinkedInButton } from '@/components/share-buttons';
import { Badge } from '@/components/ui/badge';
import { ExternalLinkIcon } from '@/components/ui/external-link-icon';
import { ShareButton } from '@/components/ui/share-button';
@@ -93,15 +92,7 @@ function getCostValues(selectedYAxisMetric: string) {
}
export function ChartShareActions() {
- return (
-
-
-
-
-
-
-
- );
+ return ;
}
export function MetricAssumptionNotes({
diff --git a/packages/app/src/components/ui/share-button.test.tsx b/packages/app/src/components/ui/share-button.test.tsx
new file mode 100644
index 00000000..f581818b
--- /dev/null
+++ b/packages/app/src/components/ui/share-button.test.tsx
@@ -0,0 +1,62 @@
+// @vitest-environment jsdom
+import React, { act } from 'react';
+import { createRoot, type Root } from 'react-dom/client';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+
+vi.mock('@/lib/url-state', () => ({
+ buildShareUrl: () => 'https://inferencex.semianalysis.com/?g_model=dsr1#inference',
+}));
+
+vi.mock('@/lib/analytics', () => ({
+ track: vi.fn(),
+}));
+
+import { ShareButton } from '@/components/ui/share-button';
+
+let container: HTMLDivElement;
+let root: Root;
+
+function renderUi(ui: React.ReactNode) {
+ act(() => root.render(ui));
+}
+
+beforeEach(() => {
+ container = document.createElement('div');
+ document.body.append(container);
+ root = createRoot(container);
+});
+
+afterEach(() => {
+ act(() => root.unmount());
+ container.remove();
+});
+
+describe('ShareButton', () => {
+ it('renders a closed popover trigger by default', () => {
+ renderUi();
+
+ const trigger = container.querySelector('[data-testid="share-button"]');
+ expect(trigger).not.toBeNull();
+ expect(trigger?.textContent).toContain('Share');
+ // Popover content lives in a portal and is not in the DOM until opened.
+ expect(document.querySelector('[data-testid="share-popover"]')).toBeNull();
+ });
+
+ it('opens the popover with the share URL pre-filled when the trigger is clicked', () => {
+ renderUi();
+
+ const trigger = container.querySelector('[data-testid="share-button"]');
+ expect(trigger).not.toBeNull();
+
+ act(() => trigger?.click());
+
+ const input = document.querySelector('[data-testid="share-url-input"]');
+ expect(input).not.toBeNull();
+ expect(input?.value).toBe('https://inferencex.semianalysis.com/?g_model=dsr1#inference');
+
+ // Copy + social buttons live inside the popover content.
+ expect(document.querySelector('[data-testid="share-copy-button"]')).not.toBeNull();
+ expect(document.querySelector('[data-testid="share-twitter"]')).not.toBeNull();
+ expect(document.querySelector('[data-testid="share-linkedin"]')).not.toBeNull();
+ });
+});
diff --git a/packages/app/src/components/ui/share-button.tsx b/packages/app/src/components/ui/share-button.tsx
index cfd72033..908a8828 100644
--- a/packages/app/src/components/ui/share-button.tsx
+++ b/packages/app/src/components/ui/share-button.tsx
@@ -1,60 +1,123 @@
'use client';
-import { track } from '@/lib/analytics';
-import { Check, Link as LinkIcon } from 'lucide-react';
-import { useCallback, useState } from 'react';
+import { Check, Copy, Share2 } from 'lucide-react';
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { ShareLinkedInButton, ShareTwitterButton } from '@/components/share-buttons';
+import { track } from '@/lib/analytics';
import { buildShareUrl } from '@/lib/url-state';
import { Button } from './button';
+import { Popover, PopoverContent, PopoverTrigger } from './popover';
export function ShareButton() {
+ const [open, setOpen] = useState(false);
const [copied, setCopied] = useState(false);
+ const [url, setUrl] = useState('');
+ const inputRef = useRef(null);
+
+ useEffect(() => {
+ if (!open) return;
+ const next = buildShareUrl();
+ setUrl(next);
+ setCopied(false);
+ track('share_popover_opened');
+ // Auto-select the URL so Cmd/Ctrl+C just works.
+ queueMicrotask(() => {
+ inputRef.current?.focus();
+ inputRef.current?.select();
+ });
+ }, [open]);
const handleCopy = useCallback(async () => {
- const url = buildShareUrl();
+ const target = url || buildShareUrl();
track('share_link_copied');
try {
- await navigator.clipboard.writeText(url);
- setCopied(true);
- setTimeout(() => setCopied(false), 2000);
+ await navigator.clipboard.writeText(target);
} catch {
- // fallback for older browsers
const textArea = document.createElement('textarea');
- textArea.value = url;
+ textArea.value = target;
document.body.append(textArea);
textArea.select();
document.execCommand('copy');
textArea.remove();
- setCopied(true);
- setTimeout(() => setCopied(false), 2000);
}
- // Dispatch action event for post-action star prompt
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
window.dispatchEvent(new CustomEvent('inferencex:action'));
- }, []);
+ }, [url]);
return (
-
+
+ {
+ // Keep focus on the URL input rather than the first focusable child.
+ event.preventDefault();
+ inputRef.current?.focus();
+ inputRef.current?.select();
+ }}
+ >
+
+
+
Share this view
+
+ Anyone with this link will see your current selections and filters.
+