Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ All notable changes to this project will be documented in this file.
- Keybind hints are hidden on smaller screens
- Site index is sortable alphanumerically and by traffic
- "Top referrers" and "Search terms" breakdowns are rendered side by side with other "Sources" tabs instead of replacing them
- Improved top bar and top stats UI/styling
- Moved graph interval picker, export button, imported data toggle and notices out of the graph and into a new options menu in the top bar

### Fixed

Expand All @@ -30,6 +32,8 @@ All notable changes to this project will be documented in this file.
- Fixed issue with all non-interactive events being counted as interactive
- Fixed countries map countries staying highlighted on Chrome
- Fixed comparison tooltip in the top pages report missing date labels
- Fixed top bar not scrolling horizontally on mobile
- Fixed incline/decline percentages not showing in top stats in comparison mode

## v3.2.0 - 2026-01-16

Expand Down
30 changes: 4 additions & 26 deletions assets/js/dashboard/components/combobox.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import React, {
useEffect,
useRef
} from 'react'
import { Spinner } from './icons'
import { Transition } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import classNames from 'classnames'
Expand Down Expand Up @@ -352,7 +353,9 @@ export default function PlausibleCombobox({
{!singleOption && renderMultiOptionContent()}
<div className="cursor-pointer absolute inset-y-0 right-0 flex items-center pr-2">
{!loading && <ChevronDownIcon className="h-4 w-4 text-gray-500" />}
{loading && <Spinner />}
{loading && (
<Spinner className="animate-spin size-4 text-indigo-500" />
)}
</div>
</div>
{isOpen && (
Expand All @@ -374,28 +377,3 @@ export default function PlausibleCombobox({
</div>
)
}

function Spinner() {
return (
<svg
className="animate-spin h-4 w-4 text-indigo-500"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ export function FeatureSetupNotice({
info,
callToAction,
onHideAction
}: {
feature: keyof typeof MODES
title: React.ReactNode
info: React.ReactNode
callToAction: { link: string; action: string }
onHideAction: () => void
}) {
const site = useSiteContext()
const sectionTitle = MODES[feature].title
Expand Down Expand Up @@ -72,14 +78,12 @@ export function FeatureSetupNotice({
return (
<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">
<div className="text-center 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 text-pretty">
<div className="text-center mt-4 font-small text-sm text-gray-500 dark:text-gray-200 text-pretty">
{info}
</div>

<div className="text-xs sm:text-sm flex my-6 justify-center">
{renderHideButton()}
{renderCallToAction()}
Expand Down
65 changes: 65 additions & 0 deletions assets/js/dashboard/components/icons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React from 'react'

export const GlobeIcon = ({ className }: { className?: string }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
className={className}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="M22 12H2M12 22c5.714-5.442 5.714-14.558 0-20M12 22C6.286 16.558 6.286 7.442 12 2"
/>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Z"
/>
</svg>
)

export const FilterIcon = ({ className }: { className?: string }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
className={className}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M6 12h12M2 5h20M10 19h4"
/>
</svg>
)

export const Spinner = ({ className }: { className?: string }) => (
<svg
className={className}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
)
26 changes: 26 additions & 0 deletions assets/js/dashboard/components/notice.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react'
import classNames from 'classnames'

export type NoticeProps = {
title: string
description?: string
className?: string
}

export const Notice = ({ title, description, className }: NoticeProps) => (
<div
className={classNames(
'flex flex-col gap-y-0.5 rounded-md bg-yellow-100/60 dark:bg-yellow-700/30 px-3 py-2.5',
className
)}
>
<p className="text-sm font-semibold text-gray-900 dark:text-gray-100">
{title}
</p>
{description && (
<p className="text-sm text-gray-600 dark:text-gray-200/60">
{description}
</p>
)}
</div>
)
6 changes: 3 additions & 3 deletions assets/js/dashboard/components/popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ const panel = {
const toggleButton = {
classNames: {
rounded:
'flex items-center rounded text-sm leading-tight h-9 transition-all duration-150',
'flex items-center rounded-md text-sm leading-tight h-8 transition-all duration-150',
shadow:
'bg-white dark:bg-gray-750 shadow-sm text-gray-800 dark:text-gray-200 dark:hover:bg-gray-700',
ghost:
'text-gray-700 dark:text-gray-100 hover:bg-gray-200 dark:hover:bg-gray-900',
truncatedText: 'truncate block font-medium',
'gap-x-1.5 px-2.5 font-medium text-gray-700 dark:text-gray-100 hover:bg-gray-150/80 dark:hover:bg-gray-800 aria-expanded:bg-gray-150/80 dark:aria-expanded:bg-gray-800',
truncatedText: 'truncate block',
linkLike:
'text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 transition-colors duration-150'
}
Expand Down
53 changes: 53 additions & 0 deletions assets/js/dashboard/components/segmented-control.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react'
import classNames from 'classnames'

export type SegmentedControlOption<T extends string> = {
value: T
label: string
}

export function SegmentedControl<T extends string>({
options,
selected,
onSelect,
ariaLabel,
getTestId
}: {
options: SegmentedControlOption<T>[]
selected: T
onSelect: (value: T) => void
ariaLabel: string
getTestId?: (value: T, isSelected: boolean) => string | undefined
}) {
return (
<div
role="group"
aria-label={ariaLabel}
className="inline-flex items-stretch rounded-lg border border-gray-300 p-0.5 dark:border-gray-600"
>
{options.map(({ value, label }) => {
const isSelected = value === selected
return (
<button
key={value}
type="button"
title={label}
aria-pressed={isSelected}
aria-label={label}
onClick={() => onSelect(value)}
data-testid={getTestId?.(value, isSelected)}
data-selected={isSelected}
className={classNames(
'flex-1 whitespace-nowrap rounded-md py-1 px-1.5 text-xs font-medium transition-colors',
isSelected
? 'bg-gray-150 text-gray-900 dark:bg-gray-600/80 dark:text-gray-100'
: 'text-gray-500 dark:text-gray-300'
)}
>
{label}
</button>
)
})}
</div>
)
}
20 changes: 20 additions & 0 deletions assets/js/dashboard/components/toggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react'
import classNames from 'classnames'

export function Toggle({ on, disabled }: { on: boolean; disabled?: boolean }) {
return (
<div
className={classNames(
'relative inline-flex h-4 w-7 shrink-0 rounded-full transition-colors duration-200',
on && !disabled ? 'bg-indigo-600' : 'bg-gray-200 dark:bg-gray-600'
)}
>
<span
className={classNames(
'inline-block mt-0.5 h-3 w-3 rounded-full bg-white shadow-sm transition-transform duration-200',
on ? 'translate-x-[14px]' : 'translate-x-0.5'
)}
/>
</div>
)
}
25 changes: 14 additions & 11 deletions assets/js/dashboard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { TopBar } from './nav-menu/top-bar'
import Behaviours from './stats/behaviours'
import { useDashboardStateContext } from './dashboard-state-context'
import { isRealTimeDashboard } from './util/filters'
import { DashboardOptionsProvider } from './stats/graph/dashboard-options-context'

function DashboardStats({
importedDataInView,
Expand Down Expand Up @@ -40,17 +41,19 @@ function Dashboard() {
const [importedDataInView, setImportedDataInView] = useState(false)

return (
<div className="mb-16 grid grid-cols-1 md:grid-cols-2 gap-5">
<TopBar showCurrentVisitors={!isRealTimeDashboard} />
<DashboardStats
importedDataInView={
isRealTimeDashboard ? undefined : importedDataInView
}
updateImportedDataInView={
isRealTimeDashboard ? undefined : setImportedDataInView
}
/>
</div>
<DashboardOptionsProvider>
<div className="mb-16 grid grid-cols-1 md:grid-cols-2 gap-5">
<TopBar showCurrentVisitors={!isRealTimeDashboard} />
<DashboardStats
importedDataInView={
isRealTimeDashboard ? undefined : importedDataInView
}
updateImportedDataInView={
isRealTimeDashboard ? undefined : setImportedDataInView
}
/>
</div>
</DashboardOptionsProvider>
)
}

Expand Down
Loading
Loading