Skip to content

Commit 3be4ccc

Browse files
feat(embed): handle heights of 400px
1 parent e4fded9 commit 3be4ccc

6 files changed

Lines changed: 46 additions & 37 deletions

File tree

docs/embed.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ To discover the current set of valid hwKeys, visit `/inference` on the live site
8888
## Embed mode behavior
8989

9090
- Site header, footer, background decorations, and navigation are hidden on all `/embed/*` routes.
91-
- A small "SemiAnalysis InferenceX →" attribution link is shown at the bottom of each embed, deep-linking to the equivalent canonical dashboard URL.
91+
- A "SemiAnalysis InferenceX →" link appears in the chart caption (`Source: …`), deep-linking to the equivalent canonical dashboard URL.
9292
- `robots: noindex, nofollow` is set on all embed routes — they won't appear in search results.
9393
- An `embed_view` PostHog event is fired once on mount, capturing `referrer`, `embed_host`, `embed_chart`, `model`, `sequence`, `gpus`, `y_metric`, and `precisions`. This makes external embed traffic attributable in analytics.
9494

@@ -118,3 +118,5 @@ All other routes set `frame-ancestors 'self'` and `X-Frame-Options: SAMEORIGIN`,
118118
`allow="clipboard-write"` is optional but needed if you want clipboard actions inside the embedded chart to work from the parent page.
119119

120120
You can copy the same ready-made iframe snippet from the dashboard: open the chart's **Export** menu and choose **Copy embed**.
121+
122+
For very short iframes (around 300–400 px tall), prefer `width` ≥ 1024 if you want the legend as a side column; below that width the legend uses a collapsible row at the bottom.

packages/app/cypress/e2e/embed-scatter.cy.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,21 @@ describe('Embed — Scatter Chart', () => {
104104
cy.get('[data-testid="embed-legend-panel"]').should('be.visible');
105105
});
106106

107+
it('legend dropdown stays inside the card at short height (800×400)', () => {
108+
cy.viewport(800, 400);
109+
cy.visit('/embed/scatter');
110+
cy.get('[data-testid="embed-scatter-figure"]', { timeout: 15000 }).should('exist');
111+
cy.get('[data-testid="embed-legend-panel"] summary').click();
112+
cy.get('[data-testid="embed-legend-dropdown"]').should('be.visible');
113+
cy.get('[data-testid="embed-scatter-figure"] [data-slot="card"]').then(($card) => {
114+
cy.get('[data-testid="embed-legend-dropdown"]').then(($dd) => {
115+
const cardTop = $card[0].getBoundingClientRect().top;
116+
const ddTop = $dd[0].getBoundingClientRect().top;
117+
expect(ddTop).to.be.at.least(cardTop);
118+
});
119+
});
120+
});
121+
107122
it('renders chart at 800×600', () => {
108123
cy.viewport(800, 600);
109124
cy.visit('/embed/scatter');

packages/app/src/app/embed/scatter/embed-scatter-client.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import { useEffect, useRef } from 'react';
44

55
import EmbedScatterDisplay from '@/components/embed/embed-scatter-display';
6-
import { EmbedAttribution } from '@/components/embed/embed-attribution';
76
import { GlobalFilterProvider } from '@/components/GlobalFilterContext';
87
import { InferenceProvider } from '@/components/inference/InferenceContext';
98
import { UnofficialRunProvider } from '@/components/unofficial-run-provider';
@@ -66,12 +65,9 @@ export default function EmbedScatterClient({ params, canonicalHref }: Props) {
6665
<UnofficialRunProvider>
6766
<GlobalFilterProvider>
6867
<InferenceProvider activeTab="inference">
69-
<div className="flex h-screen min-h-0 flex-col gap-2 p-2 sm:p-4">
68+
<div className="flex h-screen min-h-0 flex-col p-1 sm:p-2">
7069
<div className="min-h-0 flex-1">
71-
<EmbedScatterDisplay chartType={params.chart} />
72-
</div>
73-
<div className="flex justify-end pt-1">
74-
<EmbedAttribution canonicalHref={canonicalHref} />
70+
<EmbedScatterDisplay chartType={params.chart} canonicalHref={canonicalHref} />
7571
</div>
7672
</div>
7773
</InferenceProvider>

packages/app/src/components/embed/embed-attribution.tsx

Lines changed: 0 additions & 22 deletions
This file was deleted.

packages/app/src/components/embed/embed-legend-wrapper.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ function getLgServerSnapshot() {
2727
* Below `lg`: a native `<details>` collapsible with a styled summary button.
2828
* Closed by default. When open, content is absolutely positioned above the
2929
* summary (`bottom-full`) so it overlays the chart instead of growing layout
30-
* height in fill-height embeds. Content scrolls inside `max-h-96`.
30+
* height in fill-height embeds. Content scrolls inside a viewport-clamped max
31+
* height so the panel never escapes a short iframe.
3132
*
3233
* Above `lg`: the `<details>` UI is skipped and the legend renders in a flex
3334
* column sized by the parent height container.
@@ -58,7 +59,10 @@ export function EmbedLegendWrapper({ children }: { children: React.ReactNode })
5859
className="shrink-0 text-muted-foreground transition-transform duration-200 group-open:rotate-180"
5960
/>
6061
</summary>
61-
<div className="absolute bottom-full left-0 right-0 z-40 mb-1 flex max-h-96 min-h-0 flex-col overflow-y-auto overflow-x-hidden rounded-md border border-border bg-background shadow-lg">
62+
<div
63+
data-testid="embed-legend-dropdown"
64+
className="absolute bottom-full left-0 right-0 z-40 mb-1 flex max-h-[min(24rem,calc(100dvh-6rem))] min-h-0 flex-col overflow-y-auto overflow-x-hidden rounded-md border border-border bg-background shadow-lg"
65+
>
6266
{children}
6367
</div>
6468
</details>

packages/app/src/components/embed/embed-scatter-display.tsx

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22

33
import { useCallback, useMemo } from 'react';
44

5+
import { EmbedLegendWrapper } from '@/components/embed/embed-legend-wrapper';
56
import { useInference } from '@/components/inference/InferenceContext';
67
import type { InferenceData, OverlayData } from '@/components/inference/types';
7-
import { processOverlayChartData } from '@/components/inference/utils';
88
import ScatterGraph from '@/components/inference/ui/ScatterGraph';
9-
import { EmbedLegendWrapper } from '@/components/embed/embed-legend-wrapper';
9+
import { processOverlayChartData } from '@/components/inference/utils';
1010
import { Card } from '@/components/ui/card';
1111
import { Skeleton } from '@/components/ui/skeleton';
1212
import { useUnofficialRun } from '@/components/unofficial-run-provider';
13+
import { track } from '@/lib/analytics';
1314
import {
1415
type Model,
1516
type Precision,
@@ -22,6 +23,8 @@ import {
2223
interface Props {
2324
/** Which chart to render — `e2e` or `interactivity`. Defaults to `e2e`. */
2425
chartType: 'e2e' | 'interactivity';
26+
/** Canonical dashboard URL for the attribution link in the caption. */
27+
canonicalHref: string;
2528
}
2629

2730
/**
@@ -31,7 +34,7 @@ interface Props {
3134
* Reads from the same `InferenceContext` as the dashboard so chart behavior
3235
* (legend, zoom, overlay rendering) stays consistent.
3336
*/
34-
export default function EmbedScatterDisplay({ chartType }: Props) {
37+
export default function EmbedScatterDisplay({ chartType, canonicalHref }: Props) {
3538
const {
3639
graphs,
3740
loading,
@@ -120,7 +123,7 @@ export default function EmbedScatterDisplay({ chartType }: Props) {
120123

121124
if (isFirstLoad || !targetGraph) {
122125
return (
123-
<Card data-testid="embed-scatter-skeleton">
126+
<Card data-testid="embed-scatter-skeleton" className="border-0 p-2 md:p-3">
124127
<Skeleton className="h-7 w-2/4 mb-1" />
125128
<Skeleton className="h-5 w-3/4 mb-2" />
126129
<Skeleton className="h-[420px] w-full" />
@@ -149,7 +152,18 @@ export default function EmbedScatterDisplay({ chartType }: Props) {
149152
<p className="text-sm text-muted-foreground mb-2">
150153
{getModelLabel(targetGraph.model as Model)}{' '}
151154
{selectedPrecisions.map((prec) => getPrecisionLabel(prec as Precision)).join(', ')}{' '}
152-
{getSequenceLabel(targetGraph.sequence as Sequence)} • Source: SemiAnalysis InferenceX™
155+
{getSequenceLabel(targetGraph.sequence as Sequence)} • Source:{' '}
156+
<a
157+
data-testid="embed-attribution"
158+
href={canonicalHref}
159+
target="_blank"
160+
rel="noopener"
161+
onClick={() => track('embed_attribution_clicked', { href: canonicalHref })}
162+
className="inline-flex items-center gap-1 text-foreground underline-offset-2 hover:underline"
163+
>
164+
SemiAnalysis InferenceX
165+
<span aria-hidden></span>
166+
</a>
153167
</p>
154168
</>
155169
);
@@ -164,7 +178,7 @@ export default function EmbedScatterDisplay({ chartType }: Props) {
164178
data-testid="embed-scatter-figure"
165179
className="relative flex h-full min-h-0 flex-col rounded-lg"
166180
>
167-
<Card className="flex h-full min-h-0 flex-col">
181+
<Card className="flex h-full min-h-0 flex-col border-0 p-2 md:p-3">
168182
<ScatterGraph
169183
chartId="embed-chart"
170184
modelLabel={targetGraph.model}

0 commit comments

Comments
 (0)