Files
remnawave-minishop/frontend/src/admin/sections/UsersSection.svelte
T

907 lines
27 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script>
import { Input } from "$components/ui/index.js";
import {
ArrowDown,
ArrowUp,
ChevronsUpDown,
DollarSign,
Sliders,
X,
UsersRound,
} from "$components/ui/icons.js";
import Dialog from "$components/ui/dialog.svelte";
import { Label } from "$components/ui/primitives.js";
import {
AdminBadge,
AdminButton,
AdminEmptyState,
AdminPagination,
AdminSelect,
AdminTable,
AdminTableSkeleton,
} from "$components/patterns/admin/index.js";
import { getContext, onMount } from "svelte";
import { trafficOfLabel } from "../../lib/admin/format.js";
export let at = (key) => key;
export let fmtDateShort = (value) => value;
export let fmtMoney = (value) => value;
export let panelStatusBadge = () => ({});
export let resolvedAvatarUrl = () => "";
export let userDisplayName = () => "";
export let userInitials = () => "";
export let userSecondaryName = () => "";
const usersStore = getContext("usersStore");
$: ({
users,
usersTotal,
usersPage,
usersQuery,
usersFilter,
usersPanelStatus,
usersPremiumTraffic,
usersSort,
usersLoading,
} = $usersStore);
const USERS_PAGE_SIZE = 25;
let usersFilterSheetOpen = false;
$: usersHasMore = users.length === USERS_PAGE_SIZE;
const USERS_FILTER_OPTIONS = [
{ value: "all", label: at("filter_all", {}, "Все") },
{ value: "active", label: at("filter_not_banned", {}, "Не забанены") },
{ value: "banned", label: at("filter_banned", {}, "Забанены") },
{ value: "tg_linked", label: at("filter_tg_linked", {}, "С Telegram") },
{ value: "no_tg", label: at("filter_no_tg", {}, "Без Telegram") },
{ value: "email_linked", label: at("filter_email_linked", {}, "С email") },
{ value: "no_email", label: at("filter_no_email", {}, "Без email") },
{ value: "panel_linked", label: at("filter_panel_linked", {}, "С панелью") },
];
const SORT_COLUMNS = {
user: { asc: "name_asc", desc: "name_desc", defaultDirection: "asc" },
premium: { asc: "premium_ratio_asc", desc: "premium_ratio_desc", defaultDirection: "desc" },
paymentsTotal: {
asc: "payments_total_asc",
desc: "payments_total_desc",
defaultDirection: "desc",
},
paymentsCount: {
asc: "payments_count_asc",
desc: "payments_count_desc",
defaultDirection: "desc",
},
invited: {
asc: "invited_users_count_asc",
desc: "invited_users_count_desc",
defaultDirection: "desc",
},
subscriptionExpires: {
asc: "subscription_expires_at_asc",
desc: "subscription_expires_at_desc",
defaultDirection: "asc",
},
registration: { asc: "registered_asc", desc: "registered_desc", defaultDirection: "desc" },
};
const USERS_PANEL_STATUS_OPTIONS = [
{ value: "all", label: at("panel_status_all", {}, "Все статусы") },
{ value: "active", label: at("status_active", {}, "active") },
{ value: "expired", label: at("status_expired", {}, "expired") },
{ value: "limited", label: at("status_limited", {}, "limited") },
];
const USERS_PREMIUM_TRAFFIC_OPTIONS = [
{ value: "all", label: at("premium_traffic_filter_all", {}, "Все (премиум)") },
{ value: "none", label: at("premium_traffic_filter_none", {}, "Без лимита в тарифе") },
{
value: "unlimited",
label: at("premium_traffic_filter_unlimited", {}, "Безлимит (оверрайд)"),
},
{ value: "good", label: at("premium_traffic_filter_good", {}, "Премиум: норма") },
{ value: "warn", label: at("premium_traffic_filter_warn", {}, "Премиум: мало") },
{ value: "critical", label: at("premium_traffic_filter_critical", {}, "Премиум: исчерпан") },
];
function optionLabel(options, value) {
return options.find((item) => item.value === value)?.label || value;
}
function updateUsersFilterState(patch) {
usersStore.updateState({ ...patch, usersPage: 0 });
usersStore.loadUsers();
}
function resetUsersFilters() {
updateUsersFilterState({
usersFilter: "all",
usersPanelStatus: "all",
usersPremiumTraffic: "all",
});
}
function clearUsersFilter(key) {
if (key === "usersFilter") updateUsersFilterState({ usersFilter: "all" });
if (key === "usersPanelStatus") updateUsersFilterState({ usersPanelStatus: "all" });
if (key === "usersPremiumTraffic") updateUsersFilterState({ usersPremiumTraffic: "all" });
}
/** @param {Record<string, unknown> | null | undefined} pt */
function premiumTrafficBadgeVariant(pt) {
if (!pt || pt.state === "none") return "muted";
if (pt.state === "unlimited" || pt.state === "good") return "success";
if (pt.state === "warn") return "warning";
return "danger";
}
/** @param {Record<string, unknown> | null | undefined} pt */
function premiumTrafficBadgeText(pt) {
if (!pt || pt.state === "none") return "";
if (pt.state === "unlimited") return trafficOfLabel(pt.used_bytes, 0);
return trafficOfLabel(pt.used_bytes, pt.limit_bytes);
}
function userTableColumns() {
return [
{ key: "user", label: at("user", {}, "Пользователь"), sort: SORT_COLUMNS.user },
{
key: "premium",
label: at("premium_traffic_filter_label", {}, "Премиум трафик"),
sort: SORT_COLUMNS.premium,
},
{
key: "paymentsTotal",
label: at("users_col_payments_total", {}, "Сумма платежей"),
sort: SORT_COLUMNS.paymentsTotal,
},
{
key: "paymentsCount",
label: at("users_col_payments_count", {}, "Платежи"),
sort: SORT_COLUMNS.paymentsCount,
},
{
key: "invited",
label: at("users_col_invited", {}, "Приглашенные"),
sort: SORT_COLUMNS.invited,
},
{ key: "status", label: at("status", {}, "Статус") },
{
key: "subscriptionExpires",
label: at("users_col_subscription_expires", {}, "Истекает"),
sort: SORT_COLUMNS.subscriptionExpires,
},
{
key: "registration",
label: at("users_col_registration", {}, "Регистрация"),
sort: SORT_COLUMNS.registration,
},
];
}
function sortState(column) {
if (!column) return "none";
if (usersSort === column.asc) return "ascending";
if (usersSort === column.desc) return "descending";
return "none";
}
function nextSortValue(column) {
const state = sortState(column);
const defaultValue = column[column.defaultDirection] || column.asc;
if (state === "none") return defaultValue;
if (usersSort === defaultValue) {
return column.defaultDirection === "asc" ? column.desc : column.asc;
}
return "";
}
function toggleUsersSort(column) {
usersStore.updateState({ usersSort: nextSortValue(column), usersPage: 0 });
usersStore.loadUsers();
}
function sortTitle(column) {
const state = sortState(column);
if (state === "ascending") return at("sort_ascending", {}, "По возрастанию");
if (state === "descending") return at("sort_descending", {}, "По убыванию");
return at("sort_off", {}, "Без сортировки");
}
function rowPaymentsTotal(user) {
return fmtMoney(user?.payments_total_amount ?? 0, user?.payments_currency || "RUB");
}
$: activeUserFilterChips = [
usersFilter !== "all" && {
key: "usersFilter",
label: at("filter", {}, "Фильтр"),
value: optionLabel(USERS_FILTER_OPTIONS, usersFilter),
},
usersPanelStatus !== "all" && {
key: "usersPanelStatus",
label: at("panel_status", {}, "Статус панели"),
value: optionLabel(USERS_PANEL_STATUS_OPTIONS, usersPanelStatus),
},
usersPremiumTraffic !== "all" && {
key: "usersPremiumTraffic",
label: at("premium_traffic_filter_label", {}, "Премиум трафик"),
value: optionLabel(USERS_PREMIUM_TRAFFIC_OPTIONS, usersPremiumTraffic),
},
].filter(Boolean);
$: activeUsersFilterCount = activeUserFilterChips.length;
$: userTableHeaders = userTableColumns().map((column) => column.label);
onMount(() => {
usersStore.loadUsers();
});
</script>
{#snippet renderUserFilterControls()}
<Label.Root class="admin-toolbar-field admin-users-filter-field">
<span class="admin-toolbar-field-label">{at("filter", {}, "Фильтр")}</span>
<AdminSelect
value={usersFilter}
items={USERS_FILTER_OPTIONS}
class="admin-toolbar-select"
ariaLabel={at("filter", {}, "Фильтр")}
onValueChange={(value) => updateUsersFilterState({ usersFilter: value })}
/>
</Label.Root>
<Label.Root class="admin-toolbar-field admin-users-filter-field">
<span class="admin-toolbar-field-label">{at("panel_status", {}, "Статус панели")}</span>
<AdminSelect
value={usersPanelStatus}
items={USERS_PANEL_STATUS_OPTIONS}
class="admin-toolbar-select"
ariaLabel={at("panel_status", {}, "Статус панели")}
onValueChange={(value) => updateUsersFilterState({ usersPanelStatus: value })}
/>
</Label.Root>
<Label.Root class="admin-toolbar-field admin-users-filter-field">
<span class="admin-toolbar-field-label"
>{at("premium_traffic_filter_label", {}, "Премиум трафик")}</span
>
<AdminSelect
value={usersPremiumTraffic}
items={USERS_PREMIUM_TRAFFIC_OPTIONS}
class="admin-toolbar-select"
ariaLabel={at("premium_traffic_filter_label", {}, "Премиум трафик")}
onValueChange={(value) => updateUsersFilterState({ usersPremiumTraffic: value })}
/>
</Label.Root>
{/snippet}
{#snippet renderActiveUserFilterChips()}
{#if activeUsersFilterCount}
<div class="admin-users-filter-chips" aria-label={at("active_filters", {}, "Активные фильтры")}>
{#each activeUserFilterChips as chip (chip.key)}
<span class="admin-users-filter-chip">
<span class="admin-users-filter-chip-text">
<strong>{chip.label}</strong>
<span>{chip.value}</span>
</span>
<button
type="button"
aria-label={at("clear_filter", { label: chip.label }, "Сбросить фильтр")}
on:click={() => clearUsersFilter(chip.key)}
>
<X size={12} />
</button>
</span>
{/each}
</div>
{/if}
{/snippet}
<div class="admin-toolbar admin-toolbar-users">
<div class="admin-toolbar-search">
<Input
type="search"
class="input"
placeholder={at("users_search_placeholder", {}, "ID, @username или email")}
value={usersQuery}
on:input={(e) => usersStore.updateState({ usersQuery: e.target.value })}
on:keydown={(e) =>
e.key === "Enter" && (usersStore.updateState({ usersPage: 0 }), usersStore.loadUsers())}
/>
<AdminButton
variant="primary"
class="admin-users-search-button"
onclick={() => {
usersStore.updateState({ usersPage: 0 });
usersStore.loadUsers();
}}>{at("find", {}, "Найти")}</AdminButton
>
<AdminButton
variant={activeUsersFilterCount ? "primary" : "default"}
class="admin-users-filter-toggle"
aria-label={at("users_filters_open", {}, "Открыть фильтры")}
aria-haspopup="dialog"
aria-expanded={usersFilterSheetOpen}
onclick={() => {
usersFilterSheetOpen = true;
}}
>
<Sliders size={15} />
<span class="admin-users-filter-toggle-label">{at("filters", {}, "Фильтры")}</span>
{#if activeUsersFilterCount}
<span class="admin-users-filter-count">{activeUsersFilterCount}</span>
{/if}
</AdminButton>
</div>
<div class="admin-toolbar-controls">
<Label.Root class="admin-toolbar-field">
<span class="admin-toolbar-field-label">{at("filter", {}, "Фильтр")}</span>
<AdminSelect
value={usersFilter}
items={USERS_FILTER_OPTIONS}
class="admin-toolbar-select"
ariaLabel={at("filter", {}, "Фильтр")}
onValueChange={(value) => {
usersStore.updateState({ usersFilter: value, usersPage: 0 });
usersStore.loadUsers();
}}
/>
</Label.Root>
<Label.Root class="admin-toolbar-field">
<span class="admin-toolbar-field-label">{at("panel_status", {}, "Статус панели")}</span>
<AdminSelect
value={usersPanelStatus}
items={USERS_PANEL_STATUS_OPTIONS}
class="admin-toolbar-select"
ariaLabel={at("panel_status", {}, "Статус панели")}
onValueChange={(value) => {
usersStore.updateState({ usersPanelStatus: value, usersPage: 0 });
usersStore.loadUsers();
}}
/>
</Label.Root>
<Label.Root class="admin-toolbar-field">
<span class="admin-toolbar-field-label"
>{at("premium_traffic_filter_label", {}, "Премиум трафик")}</span
>
<AdminSelect
value={usersPremiumTraffic}
items={USERS_PREMIUM_TRAFFIC_OPTIONS}
class="admin-toolbar-select"
ariaLabel={at("premium_traffic_filter_label", {}, "Премиум трафик")}
onValueChange={(value) => {
usersStore.updateState({ usersPremiumTraffic: value, usersPage: 0 });
usersStore.loadUsers();
}}
/>
</Label.Root>
<div class="admin-toolbar-summary">
<span class="admin-toolbar-field-label">{at("total", {}, "Всего")}</span>
<strong>{usersTotal}</strong>
</div>
</div>
{@render renderActiveUserFilterChips()}
</div>
<Dialog
open={usersFilterSheetOpen}
class="admin-dialog admin-users-filter-dialog"
title={at("users_filters_title", {}, "Фильтры пользователей")}
description={at("users_filters_description", {}, "Уточните список пользователей")}
closeLabel={at("close_menu", {}, "Закрыть меню")}
onclose={() => {
usersFilterSheetOpen = false;
}}
>
<div class="admin-users-filter-sheet-body">
<div class="admin-users-filter-fields admin-users-filter-fields-sheet">
{@render renderUserFilterControls()}
</div>
{@render renderActiveUserFilterChips()}
<div class="admin-users-filter-sheet-actions">
<AdminButton
variant="ghost"
disabled={activeUsersFilterCount === 0}
onclick={resetUsersFilters}
>
{at("reset", {}, "Сбросить")}
</AdminButton>
<AdminButton
variant="primary"
onclick={() => {
usersFilterSheetOpen = false;
}}
>
{at("done", {}, "Готово")}
</AdminButton>
</div>
</div>
</Dialog>
<div class="admin-users-table-wrap">
{#if usersLoading}
<AdminTableSkeleton
headers={userTableHeaders}
rows={USERS_PAGE_SIZE}
widths={["220px", "128px", "112px", "78px", "88px", "96px", "112px", "112px"]}
/>
{:else if !users.length}
<AdminEmptyState tone="card"
><span class="admin-muted">{at("users_empty", {}, "Никого не найдено")}</span
></AdminEmptyState
>
{:else}
<AdminTable class="admin-users-table">
<thead>
<tr>
{#each userTableColumns() as column (column.key)}
<th aria-sort={column.sort ? sortState(column.sort) : undefined}>
{#if column.sort}
<button
type="button"
class="admin-sort-header"
title={sortTitle(column.sort)}
on:click={() => toggleUsersSort(column.sort)}
>
<span>{column.label}</span>
<span
class="admin-sort-state"
data-state={sortState(column.sort)}
aria-hidden="true"
>
{#if sortState(column.sort) === "ascending"}
<ArrowUp size={13} />
{:else if sortState(column.sort) === "descending"}
<ArrowDown size={13} />
{:else}
<ChevronsUpDown size={13} />
{/if}
</span>
</button>
{:else}
{column.label}
{/if}
</th>
{/each}
</tr>
</thead>
<tbody>
{#each users as user}
{@const avatar = resolvedAvatarUrl(user)}
{@const badge = panelStatusBadge(user)}
<tr
class="is-clickable"
role="button"
tabindex="0"
data-user-id={user.user_id}
on:click={() => usersStore.openUser(user)}
on:keydown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
usersStore.openUser(user);
}
}}
>
<td class="admin-users-cell-user" data-label={at("user", {}, "Пользователь")}>
<div class="admin-users-cell-user-inner">
<span class="admin-avatar admin-avatar-sm">
{#if avatar}
<img src={avatar} alt="" loading="lazy" referrerpolicy="no-referrer" />
{:else}
<span>{userInitials(user)}</span>
{/if}
</span>
<div class="admin-users-cell-user-text">
<span class="admin-users-cell-name">{userDisplayName(user)}</span>
<span class="admin-users-cell-secondary">{userSecondaryName(user)}</span>
<span class="admin-users-cell-id">#{user.user_id}</span>
</div>
</div>
</td>
<td
class="admin-users-cell-premium"
data-label={at("premium_traffic_filter_label", {}, "Премиум трафик")}
>
{#if user.premium_traffic && user.premium_traffic.state !== "none"}
<AdminBadge
variant={premiumTrafficBadgeVariant(user.premium_traffic)}
class="admin-user-premium-badge"
>
{premiumTrafficBadgeText(user.premium_traffic)}
</AdminBadge>
{:else}
<span class="admin-user-premium-placeholder"
>{at("premium_traffic_na", {}, "—")}</span
>
{/if}
</td>
<td
class="admin-users-cell-money"
data-label={at("users_col_payments_total", {}, "Сумма платежей")}
>
<AdminBadge variant="success" class="admin-user-money-badge">
{rowPaymentsTotal(user)}
</AdminBadge>
</td>
<td
class="admin-users-cell-counter"
data-label={at("users_col_payments_count", {}, "Платежи")}
>
<span class="admin-user-counter">
<DollarSign size={12} />
<span>{user.payments_count ?? 0}</span>
</span>
</td>
<td
class="admin-users-cell-counter"
data-label={at("users_col_invited", {}, "Приглашенные")}
>
<span class="admin-user-counter">
<UsersRound size={13} />
<span>{user.invited_users_count ?? 0}</span>
</span>
</td>
<td data-label={at("status", {}, "Статус")}>
<AdminBadge variant={badge.variant}>{badge.label}</AdminBadge>
</td>
<td
class="admin-users-cell-date admin-cell-mono"
data-label={at("users_col_subscription_expires", {}, "Истекает")}
>
{fmtDateShort(user.subscription_expires_at || user.panel_status_expired_at)}
</td>
<td
class="admin-users-cell-date admin-cell-mono"
data-label={at("users_col_registration", {}, "Регистрация")}
>
{fmtDateShort(user.registration_date)}
</td>
</tr>
{/each}
</tbody>
</AdminTable>
{/if}
</div>
<AdminPagination
meta={`${at("page", {}, "Страница")} ${usersPage + 1}`}
prevLabel={at("back", {}, "Назад")}
nextLabel={at("next", {}, "Далее")}
prevDisabled={usersPage === 0}
nextDisabled={!usersHasMore}
onPrev={() => {
usersStore.updateState({ usersPage: Math.max(0, usersPage - 1) });
usersStore.loadUsers();
}}
onNext={() => {
usersStore.updateState({ usersPage: usersPage + 1 });
usersStore.loadUsers();
}}
/>
<style>
:global(.admin-toolbar-users .admin-toolbar-controls) {
grid-template-columns: repeat(3, minmax(150px, 1fr)) minmax(82px, auto);
gap: 10px;
}
:global(.admin-users-search-button) {
min-width: 82px;
}
:global(.admin-users-filter-toggle) {
display: none;
position: relative;
align-items: center;
gap: 7px;
min-width: 0;
}
.admin-users-filter-count {
display: inline-grid;
min-width: 18px;
height: 18px;
place-items: center;
padding: 0 5px;
border-radius: 999px;
background: color-mix(in srgb, var(--admin-bg) 74%, transparent);
color: inherit;
font-size: 11px;
font-weight: 750;
line-height: 1;
}
.admin-users-filter-chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
min-width: 0;
}
.admin-users-filter-chip {
display: inline-flex;
align-items: center;
gap: 7px;
max-width: 100%;
min-height: 28px;
padding: 3px 5px 3px 10px;
border: 1px solid var(--admin-border);
border-radius: 999px;
background: color-mix(in srgb, var(--admin-muted) 8%, transparent);
color: var(--admin-text);
font-size: 12px;
}
.admin-users-filter-chip-text {
display: inline-flex;
align-items: center;
gap: 5px;
min-width: 0;
max-width: 260px;
}
.admin-users-filter-chip strong {
color: var(--admin-muted);
font-size: 11px;
font-weight: 650;
}
.admin-users-filter-chip-text > span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.admin-users-filter-chip button {
display: inline-grid;
width: 20px;
height: 20px;
place-items: center;
border: 0;
border-radius: 999px;
background: transparent;
color: var(--admin-muted);
cursor: pointer;
}
.admin-users-filter-chip button:hover,
.admin-users-filter-chip button:focus-visible {
background: color-mix(in srgb, var(--admin-muted) 14%, transparent);
color: var(--admin-text);
outline: none;
}
.admin-users-filter-fields-sheet,
.admin-users-filter-sheet-body {
display: grid;
gap: 12px;
}
.admin-users-filter-sheet-actions {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 8px;
padding-top: 2px;
}
:global(.admin-users-filter-dialog) {
width: min(100%, 420px);
}
.admin-users-table-wrap :global(.admin-table-wrap) {
overflow-x: auto;
}
.admin-users-table-wrap :global(.admin-users-table) {
min-width: 1080px;
}
.admin-sort-header {
display: inline-flex;
align-items: center;
gap: 5px;
max-width: 100%;
margin: -4px -6px;
padding: 4px 6px;
border: 0;
border-radius: 6px;
background: transparent;
color: inherit;
font: inherit;
letter-spacing: inherit;
text-transform: inherit;
cursor: pointer;
}
.admin-sort-header:hover,
.admin-sort-header:focus-visible {
color: var(--admin-text);
background: color-mix(in srgb, var(--admin-muted) 10%, transparent);
outline: none;
}
.admin-sort-header:focus-visible {
box-shadow: 0 0 0 2px var(--admin-ring);
}
.admin-sort-state {
display: inline-flex;
align-items: center;
color: var(--admin-dim);
}
.admin-sort-state[data-state="ascending"],
.admin-sort-state[data-state="descending"] {
color: color-mix(in srgb, var(--accent) 72%, var(--admin-muted));
}
.admin-users-cell-user-inner {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.admin-users-cell-user-text {
display: grid;
gap: 2px;
min-width: 0;
text-align: left;
}
.admin-users-cell-name {
font-weight: 650;
font-size: 13px;
line-height: 1.25;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.admin-users-cell-secondary {
font-size: 11px;
color: var(--admin-dim);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.admin-users-cell-id {
font-family: var(--font-mono);
font-size: 11px;
color: var(--admin-muted);
}
.admin-users-cell-premium {
white-space: nowrap;
}
.admin-users-cell-premium :global(.admin-user-premium-badge) {
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 11px;
font-variant-numeric: tabular-nums;
}
.admin-user-premium-placeholder {
color: var(--admin-dim);
font-size: 12px;
font-variant-numeric: tabular-nums;
}
.admin-users-cell-money,
.admin-users-cell-counter {
white-space: nowrap;
}
.admin-users-cell-money :global(.admin-user-money-badge) {
font-variant-numeric: tabular-nums;
}
.admin-user-counter {
display: inline-flex;
align-items: center;
gap: 5px;
color: var(--admin-text);
font-size: 12px;
font-weight: 650;
font-variant-numeric: tabular-nums;
}
.admin-user-counter :global(svg) {
color: var(--admin-muted);
flex: 0 0 auto;
}
.admin-users-cell-date {
white-space: nowrap;
font-size: 12px;
color: var(--admin-muted);
}
.admin-users-table-wrap :global(.admin-users-table tbody tr.is-clickable:focus-visible) {
outline: 2px solid var(--admin-ring);
outline-offset: -2px;
}
@media (max-width: 720px) {
:global(.admin-toolbar-users .admin-toolbar-search) {
grid-template-columns: minmax(0, 1fr) auto auto;
}
:global(.admin-toolbar-users .admin-toolbar-controls) {
display: none;
}
:global(.admin-users-search-button) {
min-width: 0;
padding-inline: 10px;
}
:global(.admin-users-filter-toggle) {
display: inline-flex;
min-width: 38px;
padding-inline: 10px;
}
.admin-users-filter-toggle-label {
display: none;
}
.admin-users-filter-chips {
gap: 5px;
}
.admin-users-filter-chip-text {
max-width: min(250px, calc(100vw - 96px));
}
:global(.dialog:has(.admin-users-filter-dialog)) {
align-items: end;
padding: max(12px, env(safe-area-inset-top)) 0 0;
}
:global(.admin-users-filter-dialog) {
width: 100%;
max-height: min(82dvh, 620px);
padding: 16px;
border-right: 0;
border-bottom: 0;
border-left: 0;
border-radius: 18px 18px 0 0;
}
.admin-users-table-wrap :global(.admin-users-table thead) {
display: table-header-group;
}
.admin-users-table-wrap :global(.admin-users-table tbody tr) {
display: table-row;
padding: 0;
border-bottom: 0;
}
.admin-users-table-wrap :global(.admin-users-table tbody tr:last-child td) {
border-bottom: 0;
}
.admin-users-table-wrap :global(.admin-users-table tbody td) {
display: table-cell;
padding: 12px 16px;
border-bottom: 1px solid var(--admin-border);
}
.admin-users-table-wrap :global(.admin-users-table tbody td::before) {
content: none;
}
}
</style>