Skip to content

Commit ef7d75f

Browse files
committed
feat(dashboard): five share posters, a Share that posts, and the S key
The share button's poster is no longer only the overview: sources, countries, the people on the site right now and the top pages each have one, picked at the top of the preview, on the same card and in the same two themes. Each card publishes the rows it is showing to the poster store, so the picture never disagrees with the screen. The site's name and its icon are separate switches; a list can drop its figures, its summary line or, on pages, its rank numbers. The chart is drawn on a smooth curve with the dashboard's own glow, and the two brands no longer share one line: the site is the headline, the period sits in the corner, and Open Analytics signs on a small tab at the bottom edge. The dark poster is the tab bar's charcoal rather than black. Share copies the image and turns into X, Bluesky and LinkedIn, each opening a composer with a sentence about the poster; on a phone the system share sheet carries the image itself. S opens the dialog from the overview. Also: the add-site dialog measures its panel with offsetHeight and applies the first measurement instantly, so it no longer grows a beat after opening.
1 parent 673be05 commit ef7d75f

14 files changed

Lines changed: 2171 additions & 305 deletions

‎CHANGELOG.md‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,33 @@ taking.
88
Releases before 0.6.0 have their notes on the
99
[GitHub releases page](https://github.com/OpenLabs-so/openanalytics/releases).
1010

11+
## [Unreleased]
12+
13+
### Added
14+
15+
- **Five share posters.** The share button's poster is no longer only the
16+
overview: sources, countries, the people on the site right now and the top
17+
pages each have one, picked at the top of the preview, on the same card and
18+
in the same two themes. Each card hands the poster the rows it is showing,
19+
so the picture never disagrees with the screen. The site's name and its icon
20+
are separate switches; a list can drop its figures, its summary line or, on
21+
pages, its rank numbers.
22+
- **Sharing from the poster.** Share copies the image and turns into X,
23+
Bluesky and LinkedIn, each opening a composer with a sentence about the
24+
poster; on a phone the system share sheet carries the image itself. `S`
25+
opens the share dialog from the overview.
26+
27+
### Changed
28+
29+
- The poster's chart is drawn on a smooth curve with the dashboard's own glow,
30+
and the two brands no longer share one line: the site is the headline, the
31+
period sits in the corner, and Open Analytics signs on a small tab at the
32+
bottom edge. The dark poster is the tab bar's charcoal rather than black.
33+
34+
### Fixed
35+
36+
- The add-site dialog no longer grows a beat after opening.
37+
1138
## [0.6.0] - 2026-09-05
1239

1340
**Upgrade notes: none.** `./upgrade.sh` and nothing else. The two ClickHouse

‎apps/web/components/dashboard/add-site-dialog.tsx‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,15 +116,31 @@ export function AddSiteFlow({ onClose }: { onClose: () => void }) {
116116
// instead of holding one fixed frame — onboarding's measured-height
117117
// spring. Border box, not `contentRect`: the step carries padding, and
118118
// the content box alone would clip the last control away.
119+
// `offsetHeight`, the `FlowDialog` fix carried here: the card enters
120+
// through a `scale: 0.94 → 1` spring and a bounding rect measures the
121+
// transformed box, so the first reading came in short and the panel
122+
// visibly grew as the entrance played out.
119123
const [panelHeight, setPanelHeight] = React.useState<number | null>(null);
120124
const measureStep = React.useCallback((node: HTMLDivElement | null) => {
121125
if (!node) return;
122126
const observer = new ResizeObserver(() => {
123-
setPanelHeight(node.getBoundingClientRect().height);
127+
setPanelHeight(node.offsetHeight);
124128
});
125129
observer.observe(node);
126130
return () => observer.disconnect();
127131
}, []);
132+
// The first measurement applies instantly; only step changes ride the
133+
// spring. The observer's first value lands a frame after the modal is on
134+
// screen at `height: auto`, and springing from that to the measured pixel
135+
// value played out as the panel growing a beat after opening. `settled`
136+
// flips one timer-hop after that first value, so the render carrying it
137+
// still has the transition off.
138+
const [settled, setSettled] = React.useState(false);
139+
React.useEffect(() => {
140+
if (panelHeight === null || settled) return;
141+
const raise = setTimeout(() => setSettled(true), 0);
142+
return () => clearTimeout(raise);
143+
}, [panelHeight, settled]);
128144

129145
React.useEffect(() => () => clearTimeout(timeout.current), []);
130146

@@ -298,7 +314,7 @@ export function AddSiteFlow({ onClose }: { onClose: () => void }) {
298314
animate={{ height: panelHeight ?? "auto" }}
299315
className="overflow-hidden"
300316
initial={false}
301-
transition={SPRING}
317+
transition={settled ? SPRING : { duration: 0 }}
302318
>
303319
<AnimatePresence mode="popLayout" initial={false} custom={dir}>
304320
{step === 1 && (

‎apps/web/components/dashboard/locations-card.tsx‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from "@/components/dashboard/analytics-card";
1212
import { ImportedGapNote } from "@/components/dashboard/data-state";
1313
import { useAnalyticsFilters } from "@/components/dashboard/filter-context";
14+
import { usePublishPosterCountries } from "@/components/dashboard/poster-publishers";
1415
import { HoverList } from "@/components/dashboard/hover-list";
1516
import {
1617
SeeAllModal,
@@ -122,6 +123,22 @@ export function LocationsCard() {
122123
filters: filtersParam,
123124
});
124125
const [view, setView] = React.useState<LocationView>("countries");
126+
127+
// The country fold for the share poster, whichever cut the header shows.
128+
const posterRows = React.useMemo(
129+
() =>
130+
resource.status === "ready"
131+
? foldCountries(resource.data.items).map((place) => ({
132+
code: place.country,
133+
visitors: place.visitors,
134+
}))
135+
: null,
136+
[resource]
137+
);
138+
usePublishPosterCountries(
139+
posterRows,
140+
resource.status === "ready" && resource.data.meta.truncated
141+
);
125142
const [open, setOpen] = React.useState(false);
126143
const current = VIEWS.find((entry) => entry.id === view) ?? VIEWS[0];
127144

0 commit comments

Comments
 (0)