Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
beb869f
Improve reports layout
sanne-san Dec 18, 2025
848fafd
Update map colors
sanne-san Dec 18, 2025
01b2258
Remove unused `sectionTitle` function and ensure Goals tab label upda…
sanne-san Dec 18, 2025
949dd50
Fix formatting issues
sanne-san Dec 18, 2025
b1a9cf8
Fix another formatting issue
sanne-san Dec 18, 2025
1fab2d7
Refactor referrer list to use new ReportLayout structure
sanne-san Dec 18, 2025
0a86781
Show 'Conversions' instead of 'Current visitors' when both conversion…
sanne-san Dec 18, 2025
65e1d96
Improve imported query unsupported warning icon and tooltip
sanne-san Dec 18, 2025
7f31c9e
Make active tab bold to improve contrast
sanne-san Dec 18, 2025
b804a76
Various small fixes
sanne-san Dec 18, 2025
0b79b17
Merge branch 'master' into sanne-report-layout-improvements
sanne-san Dec 18, 2025
f3d6c56
Remove group/report class from report layout
sanne-san Dec 18, 2025
b430b6c
Improve tab button styles
sanne-san Dec 18, 2025
973e73d
Change font weight for report header
sanne-san Dec 18, 2025
add6ac6
Apply feedback on MoreLink state handling
sanne-san Dec 23, 2025
10e90d8
Switch to MoreLinkState enum for better state management
sanne-san Dec 23, 2025
2a8bbdc
Linting
sanne-san Dec 23, 2025
c26feb3
Fix formatting
sanne-san Dec 23, 2025
2f09395
Vertically center-align CTA notice
sanne-san Dec 23, 2025
1c011fd
Update assets/js/dashboard/nav-menu/segments/searchable-segments-sect…
sanne-san Dec 24, 2025
92b76fd
Fix tooltip z-index issue
sanne-san Dec 24, 2025
644cd12
get rid of onListUpdate
RobertJoonas Dec 29, 2025
2c49d14
get rid of detailsLinkProps
RobertJoonas Dec 29, 2025
5eb3b6a
remove onDataUpdate from map.tsx as well
RobertJoonas Dec 29, 2025
ed02e35
Merge pull request #5974 from plausible/report-layout-improvements-patch
sanne-san Jan 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions assets/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
*:focus-visible {
@apply ring-2 ring-indigo-500 ring-offset-2 dark:ring-offset-gray-900 outline-none;
}

:focus:not(:focus-visible) {
@apply outline-none;
}
}

@layer components {
Expand Down
2 changes: 1 addition & 1 deletion assets/css/modal.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
position: fixed;
inset: 0;
background: rgb(0 0 0 / 60%);
z-index: 99;
z-index: 999;
overflow: auto;
}

Expand Down
8 changes: 4 additions & 4 deletions assets/js/dashboard/components/notice.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,13 @@ export function FeatureSetupNotice({
}

return (
<div className="sm:mx-32 mt-6 mb-3">
<div className="py-3">
<div className="text-center text-pretty mt-2 text-gray-800 dark:text-gray-200 font-medium">
<div className="size-full flex items-center justify-center">
<div className="py-3 max-w-2xl">
<div className="text-center text-pretty mt-2 text-gray-800 dark:text-gray-200 font-medium text-pretty">
{title}
</div>

<div className="text-center text-pretty mt-4 font-small text-sm text-gray-500 dark:text-gray-200">
<div className="text-center text-pretty mt-4 font-small text-sm text-gray-500 dark:text-gray-200 text-pretty">
{info}
</div>

Expand Down
20 changes: 20 additions & 0 deletions assets/js/dashboard/components/pill.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React, { ReactNode } from 'react'
import classNames from 'classnames'

export type PillProps = {
className?: string
children: ReactNode
}

export function Pill({ className, children }: PillProps) {
return (
<div
className={classNames(
'flex items-center shrink-0 h-fit rounded-md bg-green-50 dark:bg-green-900/60 text-green-700 dark:text-green-300 text-xs font-medium px-2.5 py-1',
className
)}
>
{children}
</div>
)
}
3 changes: 1 addition & 2 deletions assets/js/dashboard/components/popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,7 @@ const items = {
'data-[selected=true]:bg-gray-100',
'data-[selected=true]:dark:bg-gray-700',
'data-[selected=true]:text-gray-900',
'data-[selected=true]:dark:text-gray-100',
'data-[selected=true]:font-semibold'
'data-[selected=true]:dark:text-gray-100'
),
hoverLink: classNames(
'hover:bg-gray-100',
Expand Down
102 changes: 61 additions & 41 deletions assets/js/dashboard/components/tabs.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Popover, Transition } from '@headlessui/react'
import classNames from 'classnames'
import React, { ReactNode, useRef } from 'react'
import React, { ReactNode, useRef, useEffect } from 'react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import { popover, BlurMenuButtonOnEscape } from './popover'
import { useSearchableItems } from '../hooks/use-searchable-items'
Expand All @@ -16,7 +16,7 @@ export const TabWrapper = ({
}) => (
<div
className={classNames(
'flex text-xs font-medium text-gray-500 dark:text-gray-400 space-x-2 items-baseline',
'flex items-baseline gap-x-3.5 text-xs font-medium text-gray-500 dark:text-gray-400',
className
)}
>
Expand All @@ -32,11 +32,10 @@ const TabButtonText = ({
active: boolean
}) => (
<span
className={classNames('truncate text-left transition-colors duration-150', {
'hover:text-indigo-700 dark:hover:text-indigo-400 cursor-pointer':
className={classNames('truncate text-left text-xs uppercase', {
'text-gray-500 dark:text-gray-400 group-hover/tab:text-gray-800 dark:group-hover/tab:text-gray-200 font-semibold cursor-pointer':
!active,
'text-indigo-600 dark:text-indigo-500 font-bold underline decoration-2 decoration-indigo-600 dark:decoration-indigo-500':
active
'text-gray-900 dark:text-gray-100 font-bold tracking-[-.01em]': active
})}
>
{children}
Expand All @@ -54,9 +53,18 @@ export const TabButton = ({
onClick: () => void
active: boolean
}) => (
<button className={classNames('rounded-sm', className)} onClick={onClick}>
<TabButtonText active={active}>{children}</TabButtonText>
</button>
<div
className={classNames('-mb-px pb-4', {
'border-b-2 border-gray-900 dark:border-gray-100': active
})}
>
<button
className={classNames('group/tab flex rounded-sm', className)}
onClick={onClick}
>
<TabButtonText active={active}>{children}</TabButtonText>
</button>
</div>
)

export const DropdownTabButton = ({
Expand All @@ -78,25 +86,34 @@ export const DropdownTabButton = ({
{({ close: closeDropdown }) => (
<>
<BlurMenuButtonOnEscape targetRef={dropdownButtonRef} />
<Popover.Button
className="inline-flex justify-between rounded-xs"
ref={dropdownButtonRef}
<div
className={classNames('-mb-px pb-4', {
'border-b-2 border-gray-900 dark:border-gray-100': active
})}
>
<TabButtonText active={active}>{children}</TabButtonText>

<div
className="flex self-stretch -mr-1 ml-1 items-center"
aria-hidden="true"
<Popover.Button
className="group/tab inline-flex justify-between rounded-xs"
ref={dropdownButtonRef}
>
<ChevronDownIcon className="h-4 w-4" />
</div>
</Popover.Button>
<TabButtonText active={active}>{children}</TabButtonText>

<div className="ml-0.5 -mr-1" aria-hidden="true">
<ChevronDownIcon
className={classNames('size-4', {
'text-gray-500 dark:text-gray-400 group-hover/tab:text-gray-800 dark:group-hover/tab:text-gray-200':
!active,
'text-gray-900 dark:text-gray-100': active
})}
/>
</div>
</Popover.Button>
</div>

<Transition
as="div"
{...popover.transition.props}
className={classNames(
popover.transition.classNames.fullwidth,
popover.transition.classNames.left,
'mt-2',
transitionClassName
)}
Expand All @@ -115,15 +132,9 @@ type ItemsProps = {
closeDropdown: () => void
options: Array<{ selected: boolean; onClick: () => void; label: string }>
searchable?: boolean
collectionTitle?: string
}

const Items = ({
options,
searchable,
collectionTitle,
closeDropdown
}: ItemsProps) => {
const Items = ({ options, searchable, closeDropdown }: ItemsProps) => {
const {
filteredData,
showableData,
Expand All @@ -136,7 +147,7 @@ const Items = ({
countOfMoreToShow
} = useSearchableItems({
data: options,
maxItemsInitially: searchable ? 5 : options.length,
maxItemsInitially: searchable ? 10 : options.length,
itemMatchesSearchValue: (option, trimmedSearchString) =>
option.label.toLowerCase().includes(trimmedSearchString.toLowerCase())
})
Expand All @@ -148,24 +159,30 @@ const Items = ({
popover.items.classNames.hoverLink
)

useEffect(() => {
if (searchable && showSearch && searchRef.current) {
const timeoutId = setTimeout(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: Why are we focusing the search in 100ms and not instantly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The delay is to account for the animation of the popover.

searchRef.current?.focus()
}, 100)
return () => clearTimeout(timeoutId)
}
}, [searchable, showSearch, searchRef])

return (
<>
{searchable && showSearch && (
<div className="flex items-center py-2 px-4">
{collectionTitle && (
<div className="text-sm font-bold uppercase text-indigo-500 dark:text-indigo-400 mr-4">
{collectionTitle}
</div>
)}
<div className="flex items-center p-1">
<SearchInput
searchRef={searchRef}
placeholderUnfocused="Press / to search"
className="ml-auto w-full py-1"
className="w-full !max-w-none"
onSearch={handleSearchInput}
/>
</div>
)}
<div className={'max-h-[210px] overflow-y-scroll'}>
<div
className={'max-h-[224px] overflow-y-auto flex flex-col gap-y-0.5 p-1'}
>
{showableData.map(({ selected, label, onClick }, index) => {
return (
<button
Expand All @@ -177,7 +194,7 @@ const Items = ({
data-selected={selected}
className={itemClassName}
>
{label}
<span className="line-clamp-1">{label}</span>
</button>
)
})}
Expand All @@ -186,7 +203,7 @@ const Items = ({
onClick={handleShowAll}
className={classNames(
itemClassName,
'w-full text-left font-bold hover:text-indigo-700 dark:hover:text-indigo-500'
'w-full text-left text-gray-500 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200'
)}
>
{`Show ${countOfMoreToShow} more`}
Expand All @@ -197,11 +214,14 @@ const Items = ({
<button
className={classNames(
itemClassName,
'w-full text-left font-bold hover:text-indigo-700 dark:hover:text-indigo-500'
'w-full text-left !justify-start'
)}
onClick={handleClearSearch}
>
No items found. Clear search to show all.
No items found.{' '}
<span className="ml-1 text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-500">
Click to clear search.
</span>
</button>
)}
</div>
Expand Down
6 changes: 4 additions & 2 deletions assets/js/dashboard/extra/funnel.js
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,9 @@ export default function Funnel({ funnelName, tabs }) {
const header = () => {
return (
<div className="flex justify-between w-full">
<h4 className="mt-2 text-sm dark:text-gray-100">{funnelName}</h4>
<h4 className="mt-2 text-base font-semibold dark:text-gray-100">
{funnelName}
</h4>
{tabs}
</div>
)
Expand Down Expand Up @@ -346,7 +348,7 @@ export default function Funnel({ funnelName, tabs }) {
return (
<div className="mb-8">
{header()}
<p className="mt-1 text-gray-500 text-sm">
<p className="mt-0.5 text-gray-500 text-sm">
{funnel.steps.length}-step funnel • {conversionRate}% conversion
rate
</p>
Expand Down
40 changes: 12 additions & 28 deletions assets/js/dashboard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,37 +50,21 @@ function DashboardStats({
}
}, [onLiveNavigate])

const statsBoxClass =
'relative min-h-[436px] w-full mt-5 p-4 flex flex-col bg-white dark:bg-gray-900 shadow-sm rounded-md md:min-h-initial md:h-27.25rem md:w-[calc(50%-10px)] md:ml-[10px] md:mr-[10px] first:ml-0 last:mr-0'

return (
<>
<VisitorGraph updateImportedDataInView={updateImportedDataInView} />
<div className="w-full md:flex">
<div className={statsBoxClass}>
<Sources />
</div>
<div className={statsBoxClass}>
{site.flags.live_dashboard ? (
<LiveViewPortal
id="pages-breakdown-live"
className="w-full h-full border-0 overflow-hidden"
/>
) : (
<Pages />
)}
</div>
</div>

<div className="w-full md:flex">
<div className={statsBoxClass}>
<Locations />
</div>
<div className={statsBoxClass}>
<Devices />
</div>
</div>
<Sources />
{site.flags.live_dashboard ? (
<LiveViewPortal
id="pages-breakdown-live"
className="w-full h-full border-0 overflow-hidden"
/>
) : (
<Pages />
)}

<Locations />
<Devices />
<Behaviours importedDataInView={importedDataInView} />
</>
)
Expand All @@ -98,7 +82,7 @@ function Dashboard() {
const [importedDataInView, setImportedDataInView] = useState(false)

return (
<div className="mb-16">
<div className="mb-16 grid grid-cols-1 md:grid-cols-2 gap-5">
<TopBar showCurrentVisitors={!isRealTimeDashboard} />
<DashboardStats
importedDataInView={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export const SearchableSegmentsSection = ({
)}
</div>

<div className="max-h-[210px] overflow-y-scroll">
<div className="max-h-[228px] overflow-y-auto">
{showableData.map((segment) => {
return (
<Tooltip
Expand Down
6 changes: 3 additions & 3 deletions assets/js/dashboard/nav-menu/top-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ function TopBarStickyWrapper({ children }: { children: ReactNode }) {

return (
<>
<div id="stats-container-top" ref={ref} />
<div id="stats-container-top" className="col-span-full" ref={ref} />
<div
className={classNames(
'relative top-0 py-2 sm:py-3 z-10',
'col-span-full relative top-0 py-2 sm:py-3 -my-3 sm:-my-4 z-10',
!site.embedded &&
!inView &&
'sticky fullwidth-shadow bg-gray-50 dark:bg-gray-950'
Expand All @@ -47,7 +47,7 @@ function TopBarInner({ showCurrentVisitors }: TopBarProps) {

return (
<div className="flex items-center w-full">
<div className="flex items-center gap-x-4 shrink-0" ref={leftActionsRef}>
<div className="flex items-center gap-x-5 shrink-0" ref={leftActionsRef}>
<SiteSwitcher />
{showCurrentVisitors && (
<CurrentVisitors tooltipBoundaryRef={leftActionsRef} />
Expand Down
4 changes: 2 additions & 2 deletions assets/js/dashboard/site-switcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ export const SiteSwitcher = () => {
<Popover.Button
ref={buttonRef}
className={classNames(
'flex items-center rounded h-9 leading-5 font-bold dark:text-gray-100',
'flex items-center rounded h-9 text-sm leading-5 font-semibold dark:text-gray-100',
'hover:bg-gray-100 dark:hover:bg-gray-800'
)}
title={currentSite.domain}
Expand All @@ -188,7 +188,7 @@ export const SiteSwitcher = () => {
? 'All sites'
: currentSite.domain}
</span>
<ChevronDownIcon className="hidden lg:block h-5 w-5 ml-2 dark:text-gray-100" />
<ChevronDownIcon className="hidden lg:block size-4 ml-2 dark:text-gray-100" />
</Popover.Button>
<Transition
as="div"
Expand Down
Loading
Loading