Make various UI improvements (#5890)

- Fix invite modal z-index issues
- Improve invite modal design
- Add hover state to stats bars on dashboard
- Improve feature gate design
- Improve trial upgrade CTA design
This commit is contained in:
Sanne de Vries 2025-11-18 14:28:09 +01:00 committed by GitHub
parent a2ba1256d2
commit dec382ccd5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 99 additions and 91 deletions

View File

@ -26,7 +26,7 @@ export default function Bar({
return (
<div className="w-full h-full relative" style={style}>
<div
className={`absolute top-0 left-0 h-full ${bg || ''}`}
className={`absolute top-0 left-0 h-full rounded-sm transition-colors duration-150 ${bg || ''}`}
style={{ width: `${width}%` }}
></div>
{children}

View File

@ -54,7 +54,7 @@ export default function Conversions({ afterFetchData, onGoalFilterClick }) {
path: conversionsRoute.path,
search: (search) => search
}}
color="bg-red-50"
color="bg-red-50 group-hover:bg-red-100"
colMinWidth={90}
/>
)

View File

@ -138,7 +138,7 @@ export default function Properties({ afterFetchData }) {
search: (search) => search
}}
maybeHideDetails={true}
color="bg-red-50"
color="bg-red-50 group-hover:bg-red-100"
colMinWidth={90}
/>
)

View File

@ -53,7 +53,7 @@ function EntryPages({ afterFetchData }) {
search: (search) => search
}}
getExternalLinkUrl={getExternalLinkUrl}
color="bg-orange-50"
color="bg-orange-50 group-hover:bg-orange-100"
/>
)
}
@ -99,7 +99,7 @@ function ExitPages({ afterFetchData }) {
search: (search) => search
}}
getExternalLinkUrl={getExternalLinkUrl}
color="bg-orange-50"
color="bg-orange-50 group-hover:bg-orange-100"
/>
)
}
@ -141,7 +141,7 @@ function TopPages({ afterFetchData }) {
search: (search) => search
}}
getExternalLinkUrl={getExternalLinkUrl}
color="bg-orange-50"
color="bg-orange-50 group-hover:bg-orange-100"
/>
)
}

View File

@ -247,7 +247,7 @@ export default function ListReport<
return (
<div key={listItem.name} style={{ minHeight: ROW_HEIGHT }}>
<div
className="flex w-full items-center"
className="group flex w-full items-center hover:bg-gray-100/60 dark:hover:bg-gray-850 rounded-sm transition-colors duration-150"
style={{ marginTop: ROW_GAP_HEIGHT }}
>
{renderBarFor(listItem)}
@ -258,7 +258,7 @@ export default function ListReport<
}
function renderBarFor(listItem: TListItem) {
const lightBackground = color || 'bg-green-50'
const lightBackground = color || 'bg-green-50 group-hover:bg-green-100'
const metricToPlot = metrics.find((metric) => metric.meta.plot)?.key
return (
@ -267,7 +267,7 @@ export default function ListReport<
maxWidthDeduction={undefined}
count={listItem[metricToPlot]}
all={state.list}
bg={`${lightBackground} dark:bg-gray-500/15`}
bg={`${lightBackground} dark:bg-gray-500/15 dark:group-hover:bg-gray-500/30`}
plot={metricToPlot}
>
<div className="flex justify-start px-2 py-1.5 group text-sm dark:text-gray-300 relative z-9 break-all w-full">

View File

@ -83,7 +83,7 @@ function AllSources({ afterFetchData }) {
metrics={chooseMetrics()}
detailsLinkProps={{ path: sourcesRoute.path, search: (search) => search }}
renderIcon={renderIcon}
color="bg-blue-50"
color="bg-blue-50 group-hover:bg-blue-100"
/>
)
}
@ -122,7 +122,7 @@ function Channels({ onClick, afterFetchData }) {
path: channelsRoute.path,
search: (search) => search
}}
color="bg-blue-50"
color="bg-blue-50 group-hover:bg-blue-100"
/>
)
}
@ -166,7 +166,7 @@ function UTMSources({ tab, afterFetchData }) {
keyLabel={utmTag.label}
metrics={chooseMetrics()}
detailsLinkProps={{ path: route?.path, search: (search) => search }}
color="bg-blue-50"
color="bg-blue-50 group-hover:bg-blue-100"
/>
)
}

View File

@ -20,15 +20,24 @@ defmodule PlausibleWeb.Components.Billing do
<div
:if={@locked?}
id="feature-gate-overlay"
class="absolute backdrop-blur-[6px] bg-white/50 dark:bg-gray-800/50 inset-0 flex justify-center items-center rounded-md"
class="absolute backdrop-blur-[8px] bg-white/70 dark:bg-gray-800/50 inset-0 flex justify-center items-center"
>
<div class="px-6 flex flex-col items-center text-gray-500 dark:text-gray-400">
<Heroicons.lock_closed solid class="size-8 mb-2" />
<span id="lock-notice" class="text-center max-w-sm sm:max-w-md">
To gain access to this feature,
<.upgrade_call_to_action current_role={@current_role} current_team={@current_team} />.
</span>
<div class="px-6 flex flex-col items-center gap-y-3">
<div class="flex-shrink-0 bg-white dark:bg-gray-700 max-w-max rounded-md p-2 border border-gray-200 dark:border-gray-600 text-indigo-500">
<Heroicons.lock_closed solid class="size-6 -mt-px pb-px" />
</div>
<div class="flex flex-col gap-y-1.5 items-center">
<h3 class="font-medium text-gray-900 dark:text-gray-100">
Upgrade to unlock
</h3>
<span
id="lock-notice"
class="max-w-sm sm:max-w-md mb-2 text-sm text-gray-600 dark:text-gray-100/60 leading-normal text-center"
>
To access this feature,
<.upgrade_call_to_action current_role={@current_role} current_team={@current_team} />
</span>
</div>
</div>
</div>
"""
@ -359,23 +368,25 @@ defmodule PlausibleWeb.Components.Billing do
cond do
not is_nil(assigns.current_role) and assigns.current_role not in [:owner, :billing] ->
~H"please reach out to the team owner to upgrade their subscription"
~H"ask your team owner to upgrade their subscription."
upgrade_assistance_required? ->
~H"""
please contact <a href="mailto:hello@plausible.io" class="underline">hello@plausible.io</a>
to upgrade your subscription
contact
<.styled_link href="mailto:hello@plausible.io" class="font-medium">
hello@plausible.io
</.styled_link>
to upgrade your subscription.
"""
true ->
~H"""
please
<.link
class="underline inline-block"
<.styled_link
class="inline-block font-medium"
href={Routes.billing_path(PlausibleWeb.Endpoint, :choose_plan)}
>
upgrade your subscription
</.link>
upgrade your subscription.
</.styled_link>
"""
end
end

View File

@ -30,6 +30,8 @@ defmodule PlausibleWeb.Components.Generic do
"bg-indigo-600 text-white hover:bg-indigo-700 focus-visible:outline-indigo-600 disabled:bg-indigo-400/60 disabled:dark:bg-indigo-600/30 disabled:dark:text-white/35",
"secondary" =>
"border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-700 text-gray-800 dark:text-gray-100 hover:text-gray-900 hover:shadow-sm dark:hover:bg-gray-600 dark:hover:text-white disabled:text-gray-700/40 disabled:hover:shadow-none dark:disabled:text-gray-500 dark:disabled:bg-gray-800 dark:disabled:border-gray-800",
"yellow" =>
"bg-yellow-600/90 text-white hover:bg-yellow-600 focus-visible:outline-yellow-600 disabled:bg-yellow-400/60 disabled:dark:bg-yellow-600/30 disabled:dark:text-white/35",
"danger" =>
"border border-gray-300 dark:border-gray-800 text-red-600 bg-white dark:bg-gray-800 hover:text-red-700 hover:shadow-sm dark:hover:text-red-400 dark:text-red-500 active:text-red-800 disabled:text-red-700/40 disabled:hover:shadow-none dark:disabled:text-red-500/35 dark:disabled:bg-gray-800"
}
@ -95,7 +97,7 @@ defmodule PlausibleWeb.Components.Generic do
theme_class =
if assigns.disabled do
"bg-gray-400 text-white transition-colors duration-150 dark:text-white dark:text-gray-400 dark:bg-gray-700 cursor-not-allowed"
"bg-gray-400 text-white transition-all duration-150 dark:text-white dark:text-gray-400 dark:bg-gray-700 cursor-not-allowed"
else
@button_themes[assigns.theme]
end
@ -515,7 +517,7 @@ defmodule PlausibleWeb.Components.Generic do
current_role={@current_role}
current_team={@current_team}
>
<div class="p-6">
<div class="p-6 pb-14">
{render_slot(@inner_block)}
</div>
</PlausibleWeb.Components.Billing.feature_gate>

View File

@ -12,11 +12,11 @@ defmodule PlausibleWeb.Live.Components.PrimaModal do
<Modal.modal_overlay
transition_enter={{"ease-out duration-300", "opacity-0", "opacity-100"}}
transition_leave={{"ease-in duration-200", "opacity-100", "opacity-0"}}
class="fixed inset-0 bg-gray-500/75 dark:bg-gray-800/75"
class="fixed inset-0 z-[9999] bg-gray-500/75 dark:bg-gray-800/75"
/>
<div class="fixed inset-0 w-screen overflow-y-auto">
<div class="flex min-h-full items-end justify-center p-4 sm:items-center sm:p-0">
<div class="fixed inset-0 z-[9999] w-screen overflow-y-auto sm:pt-[10vmin]">
<div class="flex min-h-full items-end justify-center p-4 sm:items-start sm:p-0">
<Modal.modal_panel
id={@id <> "-panel"}
class="relative overflow-hidden rounded-lg bg-white dark:bg-gray-900 text-left shadow-xl sm:w-full sm:max-w-lg"
@ -41,7 +41,7 @@ defmodule PlausibleWeb.Live.Components.PrimaModal do
def modal_title(assigns) do
~H"""
<Modal.modal_title as={&h2/1}>
<Modal.modal_title as={&h2/1} class="text-lg font-semibold text-gray-900 dark:text-gray-100">
{render_slot(@inner_block)}
</Modal.modal_title>
"""

View File

@ -656,38 +656,31 @@ defmodule PlausibleWeb.Live.Sites do
def invitation_modal(assigns) do
~H"""
<PlausibleWeb.Live.Components.PrimaModal.modal id={@id}>
<div class="bg-white dark:bg-gray-850 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="p-5 pb-3 sm:p-6 sm:pb-3">
<div class="hidden sm:block absolute top-0 right-0 pt-4 pr-4">
<button
phx-click={Prima.Modal.close()}
class="bg-white dark:bg-gray-800 rounded-md text-gray-400 dark:text-gray-500 hover:text-gray-500 dark:hover:text-gray-400 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-indigo-500"
class="text-gray-400 dark:text-gray-500 hover:text-gray-500 dark:hover:text-gray-400"
>
<span class="sr-only">Close</span>
<Heroicons.x_mark class="size-6" />
</button>
</div>
<div class="sm:flex sm:items-start">
<div class="mx-auto shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-green-100 sm:mx-0 sm:h-10 sm:w-10">
<Heroicons.user_group class="size-6" />
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<PlausibleWeb.Live.Components.PrimaModal.modal_title>
Invitation for {@site.domain}
</PlausibleWeb.Live.Components.PrimaModal.modal_title>
<div class="mt-2">
<p class="text-sm text-gray-500 dark:text-gray-200">
You've been invited to the {@site.domain} analytics dashboard as <b class="capitalize">{@invitation.invitation.role}</b>.
</p>
<div
:if={
!(Map.get(@invitation, :exceeded_limits) || Map.get(@invitation, :no_plan)) &&
@invitation.invitation.role == :owner
}
class="mt-2 text-sm text-gray-500 dark:text-gray-200"
>
<div class="flex flex-col gap-y-4 text-center sm:text-left">
<PlausibleWeb.Live.Components.PrimaModal.modal_title>
You're invited to {@site.domain}
</PlausibleWeb.Live.Components.PrimaModal.modal_title>
<div>
<p class="text-sm text-gray-600 dark:text-gray-400 text-pretty">
You've been added as <b class="capitalize">{@invitation.invitation.role}</b>
to the {@site.domain} analytics dashboard.
<%= if !(Map.get(@invitation, :exceeded_limits) || Map.get(@invitation, :no_plan)) &&
@invitation.invitation.role == :owner do %>
If you accept the ownership transfer, you will be responsible for billing going forward.
</div>
</div>
<% else %>
Welcome aboard!
<% end %>
</p>
</div>
</div>
<.notice
@ -730,30 +723,30 @@ defmodule PlausibleWeb.Live.Sites do
You are unable to accept the ownership of this site because your account does not have a subscription. To become the owner of this site, you should upgrade to a suitable plan.
</.notice>
</div>
<div class="bg-gray-50 dark:bg-gray-850 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<div class="flex flex-col sm:flex-row-reverse gap-3 p-5 sm:p-6">
<.button
:if={!(Map.get(@invitation, :exceeded_limits) || Map.get(@invitation, :no_plan))}
mt?={false}
class="sm:ml-3 w-full sm:w-auto sm:text-sm"
class="w-full sm:w-auto sm:text-sm"
data-method="post"
data-csrf={Plug.CSRFProtection.get_csrf_token()}
data-to={"/sites/invitations/#{@invitation.invitation.invitation_id}/accept"}
data-autofocus
>
Accept &amp; Continue
Accept and continue
</.button>
<.button_link
:if={Map.get(@invitation, :exceeded_limits) || Map.get(@invitation, :no_plan)}
mt?={false}
href={Routes.billing_path(PlausibleWeb.Endpoint, :choose_plan)}
class="sm:ml-3 w-full sm:w-auto sm:text-sm"
class="w-full sm:w-auto sm:text-sm"
data-autofocus
>
Upgrade
</.button_link>
<.button_link
mt?={false}
class="w-full sm:w-auto mr-2 sm:text-sm mt-2 sm:mt-0"
class="w-full sm:w-auto sm:text-sm"
href="#"
theme="secondary"
data-method="post"

View File

@ -47,12 +47,19 @@
:if={ee?() and Plausible.Teams.on_trial?(@conn.assigns[:current_team])}
class="hidden sm:block"
>
<.styled_link
class="flex items-center h-[40px] px-3 py-2 text-sm text-yellow-700 hover:text-yellow-800 dark:text-yellow-500 dark:hover:text-yellow-500 font-medium rounded-md bg-yellow-100 dark:bg-yellow-800/40 dark:hover:bg-yellow-800/50 transition-colors duration-150"
href={Routes.settings_path(@conn, :subscription)}
>
{trial_notification(@conn.assigns[:current_team])}
</.styled_link>
<div class="flex items-center p-1 rounded-lg bg-yellow-100 dark:bg-yellow-600/35 overflow-hidden">
<span class="px-3 text-sm font-medium text-gray-900 dark:text-gray-100">
{trial_notification(@conn.assigns[:current_team])}
</span>
<.button_link
href={Routes.settings_path(@conn, :subscription)}
theme="yellow"
mt?={false}
class="!px-3 !py-2"
>
Upgrade
</.button_link>
</div>
</li>
<li class="w-full sm:w-auto">
<.dropdown>

View File

@ -17,10 +17,9 @@ defmodule PlausibleWeb.Components.BillingTest do
}
|> render_feature_gate()
assert class_of_element(html, "#feature-gate-inner-block-container") =~
"pointer-events-none"
assert class_of_element(html, "#feature-gate-overlay") =~ "backdrop-blur"
assert element_exists?(html, "#feature-gate-inner-block-container")
assert element_exists?(html, "#feature-gate-overlay")
assert text_of_element(html, "#feature-gate-overlay") =~ "Upgrade to unlock"
end
test "renders a blur overlay for a teamless account" do
@ -32,10 +31,9 @@ defmodule PlausibleWeb.Components.BillingTest do
}
|> render_feature_gate()
assert class_of_element(html, "#feature-gate-inner-block-container") =~
"pointer-events-none"
assert class_of_element(html, "#feature-gate-overlay") =~ "backdrop-blur"
assert element_exists?(html, "#feature-gate-inner-block-container")
assert element_exists?(html, "#feature-gate-overlay")
assert text_of_element(html, "#feature-gate-overlay") =~ "Upgrade to unlock"
end
test "does not render a blur overlay if feature access is granted", %{user: user} do
@ -47,9 +45,7 @@ defmodule PlausibleWeb.Components.BillingTest do
}
|> render_feature_gate()
refute class_of_element(html, "#feature-gate-inner-block-container") =~
"pointer-events-none"
assert element_exists?(html, "#feature-gate-inner-block-container")
refute element_exists?(html, "#feature-gate-overlay")
end
@ -89,7 +85,7 @@ defmodule PlausibleWeb.Components.BillingTest do
}
|> render_feature_gate()
assert text_of_element(html, "#lock-notice") =~ "reach out to the team owner"
assert text_of_element(html, "#lock-notice") =~ "ask your team owner"
end
end

View File

@ -50,7 +50,7 @@ defmodule PlausibleWeb.Components.Billing.NoticeTest do
)
assert rendered =~ "This team is limited to 10 users"
assert rendered =~ "please reach out to the team owner to upgrade their subscription"
assert rendered =~ "ask your team owner to upgrade their subscription"
end
@tag :ee_only

View File

@ -21,7 +21,7 @@ defmodule PlausibleWeb.Live.FunnelSettingsTest do
conn = get(conn, "/#{site.domain}/settings/funnels")
resp = conn |> html_response(200)
assert text(resp) =~ "please upgrade your subscription"
assert text(resp) =~ "upgrade your subscription"
end
test "lists funnels for the site and renders help link", %{conn: conn, site: site} do
@ -33,7 +33,7 @@ defmodule PlausibleWeb.Live.FunnelSettingsTest do
assert resp =~ "From blog to signup"
assert resp =~ "From signup to blog"
refute resp =~ "Your account does not have access"
refute resp =~ "please upgrade your subscription"
refute resp =~ "upgrade your subscription"
assert element_exists?(resp, "a[href=\"https://plausible.io/docs/funnel-analysis\"]")
end

View File

@ -21,7 +21,7 @@ defmodule PlausibleWeb.Live.PropsSettingsTest do
|> html_response(200)
|> text_of_element("#lock-notice")
assert lock_notice =~ "please upgrade your subscription"
assert lock_notice =~ "upgrade your subscription"
end
@tag :ee_only
@ -38,7 +38,7 @@ defmodule PlausibleWeb.Live.PropsSettingsTest do
|> html_response(200)
|> text_of_element("#lock-notice")
refute lock_notice =~ "please upgrade your subscription"
refute lock_notice =~ "upgrade your subscription"
end
test "lists props for the site and renders links", %{conn: conn, site: site} do
@ -56,7 +56,7 @@ defmodule PlausibleWeb.Live.PropsSettingsTest do
assert resp =~ "amount"
assert resp =~ "logged_in"
assert resp =~ "is_customer"
refute resp =~ "please upgrade your subscription"
refute resp =~ "upgrade your subscription"
end
test "lists props with disallow actions", %{conn: conn, site: site} do
@ -133,7 +133,7 @@ defmodule PlausibleWeb.Live.PropsSettingsTest do
assert resp =~ "amount"
assert resp =~ "logged_in"
assert resp =~ "is_customer"
refute resp =~ "please upgrade your subscription"
refute resp =~ "upgrade your subscription"
end
test "if no props are allowed, a proper info is displayed", %{

View File

@ -93,10 +93,9 @@ defmodule PlausibleWeb.Live.TeamSetupTest do
{:ok, _lv, html} = live(conn, @url)
assert class_of_element(html, "#feature-gate-inner-block-container") =~
"pointer-events-none"
assert class_of_element(html, "#feature-gate-overlay") =~ "backdrop-blur-[6px]"
assert element_exists?(html, "#feature-gate-inner-block-container")
assert element_exists?(html, "#feature-gate-overlay")
assert text_of_element(html, "#feature-gate-overlay") =~ "Upgrade to unlock"
end
end