Compare commits

...

3 Commits

15 changed files with 767 additions and 16 deletions

View File

@@ -147,6 +147,7 @@ CONSTANCE_CONFIG = {
"TF_DEFAULT_SEASON_MONTH": (config("TF_DEFAULT_SEASON_MONTH", default=8, cast=int), "Default season start month", int),
"TF_DEFAULT_SEASON_DAY": (config("TF_DEFAULT_SEASON_DAY", default=1, cast=int), "Default season start day", int),
"TF_DEFAULT_SEASON_DURATION": (config("TF_DEFAULT_SEASON_DURATION", default="1y", cast=str), "Default season duration", str),
"TF_ENABLE_TEAMS": (config("TF_ENABLE_TEAMS", default=True, cast=bool), "Enable teams", bool),
}
PHONENUMBER_DEFAULT_FORMAT = "INTERNATIONAL"

View File

@@ -5,7 +5,7 @@ from .views import MemberAddView, MemberDeleteView, MemberEditView, MemberListVi
app_name = "members"
urlpatterns = [
path("", MemberListView.as_view(), name="list"),
# path("add/", MemberAddView.as_view(), name="add"),
path("add/", MemberAddView.as_view(), name="add"),
# path("<int:pk>/edit/", MemberEditView.as_view(), name="edit"),
path("<int:pk>/delete/", MemberDeleteView.as_view(), name="delete"),
# path("load/", MemberLoadView.as_view(), name="load"),

View File

@@ -5,12 +5,13 @@ from django.contrib.messages.views import SuccessMessageMixin
from django.http import HttpResponse, HttpResponseRedirect
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from django.views.generic import DeleteView
from django.views.generic import DeleteView, UpdateView, CreateView
from django_filters.views import FilterView
from rules.contrib.views import PermissionRequiredMixin
from members.filters import MemberFilter
from members.models import Member
from members.forms import MemberForm
from ..mixins import HTMXViewMixin
class MemberListView(HTMXViewMixin, PermissionRequiredMixin, FilterView):
@@ -19,6 +20,7 @@ class MemberListView(HTMXViewMixin, PermissionRequiredMixin, FilterView):
permission_denied_message = _("You do not have permission to view this page.")
permission_required = "members.view_member"
partial_name = "members/member_filter.html#content"
menu_highlight = "members"
def handle_no_permission(self) -> HttpResponseRedirect:
messages.error(self.request, self.get_permission_denied_message())
@@ -36,7 +38,22 @@ class MemberListView(HTMXViewMixin, PermissionRequiredMixin, FilterView):
return kwargs
class MemberAddView: ...
class MemberAddView(HTMXViewMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView):
model = Member
form_class = MemberForm
permission_required = "members.add_member"
permission_denied_message = _("You do not have permission to view this page.")
success_message = _("Member <strong>%(name)s</strong> has been created successfully.")
success_url = reverse_lazy("backend:members:list")
partial_name = "members/member_form.html#content"
menu_highlight = "members"
def handle_no_permission(self) -> HttpResponseRedirect:
messages.error(self.request, self.get_permission_denied_message())
return HttpResponseRedirect(reverse_lazy("backend:index"))
def get_success_message(self, cleaned_data):
return self.success_message % dict(cleaned_data, name=self.object.user.get_full_name())
class MemberEditView: ...
@@ -48,6 +65,8 @@ class MemberDeleteView(HTMXViewMixin, PermissionRequiredMixin, SuccessMessageMix
permission_required = "members.delete_member"
permission_denied_message = _("You do not have permission to view this page.")
success_url = reverse_lazy("backend:members:list")
partial_name = "members/member_confirm_delete.html#content"
menu_highlight = "members"
def handle_no_permission(self) -> HttpResponseRedirect:
messages.error(self.request, self.get_permission_denied_message())

View File

@@ -1,3 +1,5 @@
import json
from django.http import HttpResponse
from django.template.loader import render_to_string
from django_htmx.http import HttpResponseClientRedirect, HttpResponseClientRefresh
@@ -33,6 +35,9 @@ class HTMXViewMixin:
# Optional: automatically push URL on GET
htmx_push_url = None
# Optional: name of the menu item to highlight
menu_highlight = None
# Optional: trigger events after rendering
htmx_trigger = None
htmx_trigger_after_settle = None
@@ -62,9 +67,21 @@ class HTMXViewMixin:
response.headers["HX-Push-Url"] = self.htmx_push_url or request.get_full_path()
print(response.headers)
# Build HX-Trigger payload
trigger_payload = {}
# 1. User-defined triggers
if self.htmx_trigger:
response.headers["HX-Trigger"] = self.htmx_trigger
trigger_payload.update(json.loads(self.htmx_trigger))
# 2. Auto menu highlight trigger
if self.menu_highlight:
trigger_payload["menuHighlight"] = self.menu_highlight
# Emit HX-Trigger if anything is present
if trigger_payload:
response.headers["HX-Trigger"] = json.dumps(trigger_payload)
if self.htmx_trigger_after_settle:
response.headers["HX-Trigger-After-Settle"] = self.htmx_trigger_after_settle

View File

@@ -24,5 +24,5 @@ class MemberAdmin(admin.ModelAdmin):
fieldsets = [
("GENERAL INFORMATION", {"fields": ["user", "family_members", "birthday", "license", "access_token"]}),
("CONTACT_INFORMATION", {"fields": ["phone_number", "emergency_phone_number"]}),
("METADATA", {"fields": ["created", "updated"]}),
("METADATA", {"fields": ["notes", "created", "updated"]}),
]

View File

@@ -8,7 +8,7 @@ class MemberFilter(django_filters.FilterSet):
user__first_name = django_filters.CharFilter(field_name="user__first_name", label=_("First name"), lookup_expr="icontains")
user__last_name = django_filters.CharFilter(field_name="user__last_name", label=_("Last name"), lookup_expr="icontains")
license = django_filters.CharFilter(label=_("License"), lookup_expr="icontains")
user__is_active = django_filters.TypedChoiceFilter( field_name='user__is_active', label=_("Active?"), initial="true", choices=( ('', 'All'), ('true', 'Yes'), ('false', 'No'), ), coerce=lambda x: x.lower() == 'true' )
user__is_active = django_filters.TypedChoiceFilter( field_name='user__is_active', label=_("Active?"), initial="true", choices=( ('', 'All users'), ('true', 'Active users'), ('false', 'Inactive users'), ), coerce=lambda x: x.lower() == 'true' )
class Meta:
model = Member

42
members/forms.py Normal file
View File

@@ -0,0 +1,42 @@
from django import forms
from django.utils.translation import gettext_lazy as _
from .models import Member
class MemberForm(forms.ModelForm):
first_name = forms.CharField(label=_("First name"), max_length=250)
last_name = forms.CharField(label=_("Last name"), max_length=250)
email = forms.EmailField(label=_("Email"))
admin = forms.BooleanField(label=_("Admin?"), required=False, help_text=_("If checked will mark this user as a site admin granting them all permissions"))
password = forms.CharField(label=_("Password"), widget=forms.PasswordInput, required=False)
password_confirmation = forms.CharField(label=_("Confirm password"), widget=forms.PasswordInput, required=False)
class Meta:
model = Member
fields = ["phone_number", "emergency_phone_number", "license", "birthday", "family_members"]
localized_fields = fields
def save(self, commit: bool = True) -> Member:
password = None
if self.cleaned_data["password"] is not None and self.cleaned_data["password"] != "" and self.cleaned_data["password"] == self.cleaned_data["password_confirmation"]:
password = self.cleaned_data["password"]
member = Member.create(first_name=self.cleaned_data["first_name"], last_name=self.cleaned_data["last_name"], email=self.cleaned_data["email"], password=password, member=self.instance)
member.phone_number = self.cleaned_data["phone_number"]
member.emergency_phone_number = self.cleaned_data["emergency_phone_number"]
member.license = self.cleaned_data["license"]
member.birthday = self.cleaned_data["birthday"]
if self.cleaned_data["admin"]:
member.user.is_superuser = True
member.user.save(update_fields=["is_superuser"])
member.save(update_fields=["phone_number", "emergency_phone_number", "license", "birthday"])
member.family_members.set(self.cleaned_data["family_members"])
return member

View File

@@ -1,4 +1,9 @@
import secrets
import string
from typing import Optional
from django.conf import settings
from django.contrib.auth import get_user_model
from django.db import models
from django.utils.translation import gettext_lazy as _
from phonenumber_field.modelfields import PhoneNumberField
@@ -22,6 +27,7 @@ class Member(RulesModel):
phone_number = PhoneNumberField(_("phone number"), blank=True, null=True)
emergency_phone_number = PhoneNumberField(_("emergency phone number"), blank=True, null=True)
notes = models.TextField(_("notes"), blank=True, null=True)
access_token = models.CharField(_("access token"), max_length=255, blank=True, null=True)
@@ -44,3 +50,57 @@ class Member(RulesModel):
def __str__(self):
return self.user.get_full_name()
@classmethod
def create(cls, first_name: str, last_name: str, email: str, password: Optional[str] = None, member: Optional["Member"] = None) -> "Member":
"""Creates a new member based on the provided details"""
if member is not None and member.pk is not None:
member.user.first_name = first_name
member.user.last_name = last_name
member.user.email = email
member.user.username = email
if password is not None and password != "":
member.user.set_password(password)
else:
# First check to see if a user already exists in the system
user, created = get_user_model().objects.get_or_create(
username=email,
defaults={
"first_name": first_name,
"last_name": last_name,
"email": email
}
)
if not created:
user.first_name = first_name
user.last_name = last_name
user.email = email
user.username = email
if hasattr(user, "member"):
member = user.member
if password is not None and password != "":
user.set_password(password)
else:
member = cls()
initial_password = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(20))
if password is None or password == "":
password = initial_password
member.notes = f"Initial password: {initial_password}"
user.set_password(password)
member.user = user
if not member.user.is_active:
member.user.is_active = True
member.user.save()
member.save()
return member

View File

@@ -9,7 +9,7 @@
{% if is_member_manager %}
<li class="menu-title">Members</li>
<li><a href="{{ members_list }}" {% if members_list in request.path %}class="menu-active"{% endif %}><i class="fa-solid fa-users"></i> Members</a></li>
<li><a href="{{ members_list }}" class="menu-item {% if members_list in request.path %}menu-active{% endif %}" data-menu="members" hx-get="{% url "backend:members:list" %}" hx-target="#content"><i class="fa-solid fa-users"></i> Members</a></li>
{% endif %}
<li class="menu-title mt-4">Navigation</li>

View File

@@ -0,0 +1,29 @@
{% if messages %}
<div id="messages" class="flex flex-col p-2 m-4 -mt-4 gap-y-2" {% if request.htmx %} hx-swap-oob="true"{% endif %}>
{% for message in messages %}
{% if message.level == DEFAULT_MESSAGE_LEVELS.SUCCESS %}
<div role="alert" class="alert alert-success">
<i class="text-lg fa-solid fa-check"></i>
<span>{{ message|safe }}</span>
</div>
{% elif message.level == DEFAULT_MESSAGE_LEVELS.WARNING %}
<div role="alert" class="alert alert-warning">
<i class="text-lg fa-solid fa-ban"></i>
<span>{{ message|safe }}</span>
</div>
{% elif message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
<div role="alert" class="alert alert-error">
<i class="text-lg fa-solid fa-exclamation"></i>
<span>{{ message|safe }}</span>
</div>
{% else %}
<div role="alert" class="alert alert-info">
<i class="text-lg fa-solid fa-info"></i>
<span>{{ message|safe }}</span>
</div>
{% endif %}
{% endfor %}
</div>
{% else %}
<div id="messages"{% if request.htmx %} hx-swap-oob="true"{% endif %}></div>
{% endif %}

View File

@@ -101,11 +101,15 @@
<!-- MAIN CONTENT-->
<div class="drawer-content flex w-full">
<main class="bg-base-100 border border-base-300 rounded-xl m-4 ml-2 p-6 w-full" id="content">
{% block content %}
<h1 class="text-3xl font-bold">Welcome!</h1>
<p>This is your main content area.</p>
{% endblock %}
<main class="bg-base-100 border border-base-300 rounded-xl m-4 ml-2 p-6 w-full">
{% include "backend/partials/messages.html" %}
<div id="content">
{% block content %}
<h1 class="text-3xl font-bold">Welcome!</h1>
<p>This is your main content area.</p>
{% endblock %}
</div>
</main>
</div>
</div>
@@ -116,6 +120,13 @@
</footer>
<script>
document.body.addEventListener("menuHighlight", (event) => {
const active = event.detail.value;
document.querySelectorAll(".menu-item").forEach(el => {
el.classList.toggle("menu-active", el.dataset.menu === active);
});
});
{% comment %}// Shrinking navbar on scroll
const navbar = document.getElementById("mainNavbar");
let lastScroll = 0; window.addEventListener("scroll", () => {

View File

@@ -15,11 +15,11 @@
{% endblocktranslate %}
</div>
<form method="post">
<form method="post" hx-post="{% url "backend:members:delete" object.pk %}" hx-target="#content">
{% csrf_token %}
<div class="flex flex-row gap-2 mt-8">
<a href="{% url "backend:members:list" %}" class="btn btn-neutral btn-outline grow lg:grow-0">{% translate "Cancel" %}</a>
<a href="{% url "backend:members:list" %}" class="btn btn-neutral btn-outline grow lg:grow-0" hx-get="{% url "backend:members:list" %}" hx-target="#content">{% translate "Cancel" %}</a>
<button type="submit" class="btn btn-error grow lg:grow-0"><i class="fa-solid fa-trash"></i>{% translate "Delete" %}</button>
</div>
</form>

View File

@@ -7,6 +7,10 @@
{% block content %}
{% partialdef content inline %}
{% if request.htmx %}
{% include "backend/partials/messages.html" %}
{% endif %}
<h1 class="page-title">{% translate "Members" %}</h1>
<div class="lg:hidden collapse collapse-plus bg-base-100 border-neutral border">
@@ -59,7 +63,7 @@
<i class="fa-solid fa-file-upload"></i>{% translate "Load members from file" %}
</a>
<a class="btn btn-neutral btn-outline btn-sm grow" href="">
<a class="btn btn-neutral btn-outline btn-sm grow" href="{% url "backend:members:add" %}" hx-get="{% url "backend:members:add" %}" hx-target="#content">
<i class="fa-solid fa-plus"></i>{% translate "Add member" %}
</a>
</div>
@@ -129,7 +133,7 @@
<i class="fa-solid fa-eye"></i>{% translate "Details" %}
</a>
<a class="btn btn-outline btn-error btn-sm" href="{% url "backend:members:delete" member.pk %}">
<a class="btn btn-outline btn-error btn-sm" href="{% url "backend:members:delete" member.pk %}" hx-get="{% url "backend:members:delete" member.pk %}" hx-target="#content">
<i class="fa-solid fa-trash"></i>{% translate "Delete" %}
</a>
</div>

View File

@@ -0,0 +1,157 @@
{% extends "backend/base.html" %}
{% load i18n %}
{% load form_field %}
{% load avatar %}
{% block content %}
{% partialdef content inline %}
<h1 class="page-title">{% translate "Members" %}</h1>
<div class="flex flex-row gap-2 items-center lg:hidden">
<div class="text-3xl min-w-12">
<a href="{% url "backend:members:list" %}" hx-get="{% url "backend:members:list" %}" hx-target="#content">
<i class="fa-solid fa-angle-left"></i>
</a>
</div>
<div class="flex flex-row gap-2 grow justify-center items-center">
{% if member %}
<div class="font-bold text-xl">{{ member.user.get_full_name }}</div>
{% if member.user.is_superuser %}
<div class="tooltip" data-tip="{% translate "This user is a site admin" %}">
<div class="badge badge-sm badge-accent">
<i class="fa-solid fa-user-shield"></i>
</div>
</div>
{% endif %}
{% else %}
<div class="font-bold text-xl">{% translate "Create new member" %}</div>
{% endif %}
</div>
<div class="flex min-w-12 justify-end">
{% if member %}
<a class="btn btn-error btn-outline" href="{% url "backend:members:delete" member.pk %}">
<i class="fa-solid fa-trash"></i>
</a>
{% else %}
&nbsp;
{% endif %}
</div>
</div>
{% if member %}
<div class="mt-4 lg:hidden flex flex-row gap-2">
<div class="mt-4 lg:hidden flex flex-row gap-2">
{% if member.phone_number %}
<a href="{{ member.phone_number.as_rfc3966 }}" class="btn btn-info btn-outline btn-sm grow">
<i class="fa-solid fa-phone"></i>
{{ member.phone }}
</a>
{% endif %}
{% if member.emergency_phone_number %}
<a href="{{ member.emergency_phone_number.as_rfc3966 }}" class="btn btn-error btn-outline btn-sm grow">
<i class="fa-solid fa-file-medical"></i>
{{ member.emergency_phone_number }}
</a>
{% endif %}
</div>
{% if config.TF_ENABLE_TEAMS %}
<a href="?member__user__first_name={{ member.user.first_name }}&member__user__last_name={{ member.user.last_name }}" class="btn btn-sm w-full mt-2 btn-outline btn-neutral lg:hidden">
<i class="fa-solid fa-ticket"></i>{% translate "View team memberships" %}
</a>
{% endif %}
<div class="flex flex-row items-center mt-8 gap-x-3 hidden lg:flex">
{% avatar first_name=member.user.first_name last_name=member.user.last_name %}
<h2 class="page-subtitle border-b-0! mt-0!">{{ member.user.get_full_name }}</h2>
{% if member.user.is_superuser %}
<div class="badge badge-accent"><i class="fa-solid fa-user-shield"></i>{% translate "Admin" %}</div>
{% endif %}
<div class="justify-end hidden gap-2 lg:flex lg:flex-row grow">
{% if member.phone %}
<a href="{{ member.phone.as_rfc3966 }}" class="btn btn-outline btn-info"><i class="fa-solid fa-phone"></i>{{ member.phone }}</a>
{% endif %}
{% if member.emergency_phone %}
<a href="{{ member.emergency_phone.as_rfc3966 }}" class="btn btn-outline btn-error"><i class="fa-solid fa-file-medical"></i>{{ member.emergency_phone }}</a>
{% endif %}
{% if config.TF_ENABLE_TEAMS %}
<a href="?member__user__first_name={{ member.user.first_name }}&member__user__last_name={{ member.user.last_name }}" class="btn btn-outline btn-neutral">
<i class="fa-solid fa-ticket"></i>{% translate "Team Memberships" %}
</a>
{% endif %}
<a href="{% url "backend:members:members_delete" member.id %}" class="btn btn-error btn-outline">
<i class="fa-solid fa-trash"></i>{% translate "Delete" %}
</a>
</div>
</div>
{% else %}
<h2 class="page-subtitle border-b-0! hidden lg:flex">{% translate "Create new member" %}</h2>
{% endif %}
{% if form.errors %}
<div class="flex flex-row items-center gap-2 p-2 m-4 rounded-lg bg-error">
<i class="mr-2 text-3xl fa-solid fa-exclamation-triangle text-error-content"></i>
<div class="flex flex-col">
<div class="mb-1 font-semibold text-error-content">{% translate "Error" %}</div>
<div class="text-sm text-error-content">{% translate "Please correct the errors below before saving again." %}</div>
</div>
</div>
{% endif %}
<form method="post">
{% csrf_token %}
<h2 class="page-subtitle">{% translate "Personal information" %}</h2>
<div class="grid grid-cols-1 gap-4 mt-2 lg:grid-cols-2">
{% form_field form.first_name %}
{% form_field form.last_name %}
{% form_field form.birthday %}
</div>
<h2 class="page-subtitle">{% translate "Contact information" %}</h2>
<div class="grid grid-cols-1 gap-4 mt-2 lg:grid-cols-2">
{% form_field form.email %}
{% form_field form.phone_number %}
{% form_field form.emergency_phone_number %}
</div>
<h2 class="page-subtitle">{% translate "Family information" %}</h2>
<div class="grid grid-cols-1 gap-4 mt-2 lg:grid-cols-2">
{% form_field form.family_members %}
</div>
<h2 class="page-subtitle">{% translate "Club information" %}</h2>
<div class="grid grid-cols-1 gap-4 mt-2 lg:grid-cols-2">
{% form_field form.license %}
{% form_field form.admin show_as_toggle=True %}
</div>
<h2 class="page-subtitle">{% translate "Password" %}</h2>
<div class="mt-2 text-sm text-justify">{% blocktranslate %}Setting the password here will overwrite the current password for this member, after changing the member will be prompted to set a new password at the next login.<br /><br />If both fields are empty the current password will not be changed.{% endblocktranslate %}</div>
<div class="grid grid-cols-1 gap-4 mt-2 lg:grid-cols-2">
{% form_field form.password %}
{% form_field form.password_confirmation %}
</div>
<button class="w-full mt-8 btn btn-neutral" type="submit">
<i class="fa-solid fa-floppy-disk"></i>{% translate "Save" %}
</button>
</form>
<script type="text/javascript">
new Choices(document.querySelector("#id_family_members"));
</script>
{% endpartialdef content %}
{% endblock content %}

View File

@@ -69,6 +69,12 @@
}
}
h2.page-subtitle {
@apply text-xl font-bold;
@apply mt-8;
@apply border-b border-base-content;
}
div.action_bar {
@apply flex flex-col lg:flex-row;
@apply items-center;
@@ -98,4 +104,409 @@
@apply min-w-fit lg:w-fit;
}
}
}
.choices {
position: relative;
overflow: hidden;
margin-bottom: 0px;
font-size: 16px
}
.choices:focus {
outline: 0
}
.choices:last-child {
margin-bottom: 0
}
.choices.is-open {
overflow: visible
}
.choices.is-disabled .choices__inner,
.choices.is-disabled .choices__input {
background-color: #eaeaea;
cursor: not-allowed;
-webkit-user-select: none;
user-select: none
}
.choices.is-disabled .choices__item {
cursor: not-allowed
}
.choices [hidden] {
display: none !important
}
.choices[data-type*=select-one] {
cursor: pointer
}
.choices[data-type*=select-one] .choices__inner {
padding-bottom: 7.5px
}
.choices[data-type*=select-one] .choices__input {
display: block;
width: 100%;
padding: 10px;
border-bottom: 1px solid #ddd;
background-color: #fff;
margin: 0
}
.choices[data-type*=select-one] .choices__button {
background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHZpZXdCb3g9IjAgMCAyMSAyMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSIjMDAwIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Ik0yLjU5Mi4wNDRsMTguMzY0IDE4LjM2NC0yLjU0OCAyLjU0OEwuMDQ0IDIuNTkyeiIvPjxwYXRoIGQ9Ik0wIDE4LjM2NEwxOC4zNjQgMGwyLjU0OCAyLjU0OEwyLjU0OCAyMC45MTJ6Ii8+PC9nPjwvc3ZnPg==);
padding: 0;
background-size: 8px;
position: absolute;
top: 50%;
right: 0;
margin-top: -10px;
margin-right: 25px;
height: 20px;
width: 20px;
border-radius: 10em;
opacity: .25
}
.choices[data-type*=select-one] .choices__button:focus,
.choices[data-type*=select-one] .choices__button:hover {
opacity: 1
}
.choices[data-type*=select-one] .choices__button:focus {
box-shadow: 0 0 0 2px #005f75
}
.choices[data-type*=select-one] .choices__item[data-placeholder] .choices__button {
display: none
}
.choices[data-type*=select-one]::after {
content: "";
height: 0;
width: 0;
border-style: solid;
border-color: #333 transparent transparent;
border-width: 5px;
position: absolute;
right: 11.5px;
top: 50%;
margin-top: -2.5px;
pointer-events: none
}
.choices[data-type*=select-one].is-open::after {
border-color: transparent transparent #333;
margin-top: -7.5px
}
.choices[data-type*=select-one][dir=rtl]::after {
left: 11.5px;
right: auto
}
.choices[data-type*=select-one][dir=rtl] .choices__button {
right: auto;
left: 0;
margin-left: 25px;
margin-right: 0
}
.choices[data-type*=select-multiple] .choices__inner,
.choices[data-type*=text] .choices__inner {
cursor: text
}
.choices[data-type*=select-multiple] .choices__button,
.choices[data-type*=text] .choices__button {
position: relative;
display: inline-block;
margin: 0-4px 0 2px;
padding-left: 16px;
background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHZpZXdCb3g9IjAgMCAyMSAyMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSIjRkZGIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Ik0yLjU5Mi4wNDRsMTguMzY0IDE4LjM2NC0yLjU0OCAyLjU0OEwuMDQ0IDIuNTkyeiIvPjxwYXRoIGQ9Ik0wIDE4LjM2NEwxOC4zNjQgMGwyLjU0OCAyLjU0OEwyLjU0OCAyMC45MTJ6Ii8+PC9nPjwvc3ZnPg==);
background-size: 8px;
width: 8px;
line-height: 1;
opacity: .75;
border-radius: 0
}
.choices[data-type*=select-multiple] .choices__button:focus,
.choices[data-type*=select-multiple] .choices__button:hover,
.choices[data-type*=text] .choices__button:focus,
.choices[data-type*=text] .choices__button:hover {
opacity: 1
}
.choices__inner {
display: inline-block;
vertical-align: top;
width: 100%;
background-color: var(--color-base-100);
padding: 7.5px 7.5px 3.75px;
border: 1px solid color-mix(in oklab, var(--color-base-content) 20%, #0000);
border-radius: var(--radius-field);
font-size: 14px;
min-height: 44px;
overflow: hidden
}
.is-focused .choices__inner,
.is-open .choices__inner {
border-color: var(--color-base-content);
isolation: isolate;
border-radius: var(--radius-field);
}
.is-focused {
border: 2px solid var(--color-base-content);
border-radius: calc(var(--radius-field) * 2);
margin: -2px;
padding: 2px; /* adjust offset */
}
.is-open .choices__inner {
border-radius: var(--radius-field);
}
.is-flipped.is-open .choices__inner {
border-radius: var(--radius-field);
}
.choices__list {
margin: 0;
padding-left: 0;
list-style: none
}
.choices__list--single {
display: inline-block;
padding: 4px 16px 4px 4px;
width: 100%
}
[dir=rtl] .choices__list--single {
padding-right: 4px;
padding-left: 16px
}
.choices__list--single .choices__item {
width: 100%
}
.choices__list--multiple {
display: inline
}
.choices__list--multiple .choices__item {
display: inline-block;
vertical-align: middle;
border-radius: var(--radius-selector);
padding: 4px 10px;
font-size: 12px;
font-weight: 500;
margin-right: 3.75px;
margin-bottom: 3.75px;
background-color: var(--color-primary);
border: 1px solid var(--color-primary);
color: #fff;
word-break: break-all;
box-sizing: border-box
}
.choices__list--multiple .choices__item[data-deletable] {
padding-right: 5px
}
[dir=rtl] .choices__list--multiple .choices__item {
margin-right: 0;
margin-left: 3.75px
}
.choices__list--multiple .choices__item.is-highlighted {
background-color: var(--color-secondary);
border: 1px solid var(--color-secondary)
}
.is-disabled .choices__list--multiple .choices__item {
background-color: #aaa;
border: 1px solid #919191
}
.choices__list--dropdown,
.choices__list[aria-expanded] {
display: none;
z-index: 1;
position: absolute;
width: 100%;
background-color: var(--color-base-100);
border: 1px solid #ddd;
top: 100%;
margin-top: 3px;
border-radius: var(--radius-field);
overflow: hidden;
word-break: break-all
}
.is-active.choices__list--dropdown,
.is-active.choices__list[aria-expanded] {
display: block
}
.is-open .choices__list--dropdown,
.is-open .choices__list[aria-expanded] {
border-color: #b7b7b7
}
.is-flipped .choices__list--dropdown,
.is-flipped .choices__list[aria-expanded] {
top: auto;
bottom: 100%;
margin-top: 0;
margin-bottom: -1px;
border-radius: .25rem .25rem 0 0
}
.choices__list--dropdown .choices__list,
.choices__list[aria-expanded] .choices__list {
position: relative;
max-height: 300px;
overflow: auto;
-webkit-overflow-scrolling: touch;
will-change: scroll-position
}
.choices__list--dropdown .choices__item,
.choices__list[aria-expanded] .choices__item {
position: relative;
padding: 10px;
font-size: 14px
}
[dir=rtl] .choices__list--dropdown .choices__item,
[dir=rtl] .choices__list[aria-expanded] .choices__item {
text-align: right
}
@media (min-width:640px) {
.choices__list--dropdown .choices__item--selectable[data-select-text],
.choices__list[aria-expanded] .choices__item--selectable[data-select-text] {
padding-right: 100px
}
.choices__list--dropdown .choices__item--selectable[data-select-text]::after,
.choices__list[aria-expanded] .choices__item--selectable[data-select-text]::after {
content: attr(data-select-text);
font-size: 12px;
opacity: 0;
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%)
}
[dir=rtl] .choices__list--dropdown .choices__item--selectable[data-select-text],
[dir=rtl] .choices__list[aria-expanded] .choices__item--selectable[data-select-text] {
text-align: right;
padding-left: 100px;
padding-right: 10px
}
[dir=rtl] .choices__list--dropdown .choices__item--selectable[data-select-text]::after,
[dir=rtl] .choices__list[aria-expanded] .choices__item--selectable[data-select-text]::after {
right: auto;
left: 10px
}
}
.choices__list--dropdown .choices__item--selectable.is-highlighted,
.choices__list[aria-expanded] .choices__item--selectable.is-highlighted {
background-color: var(--color-base-100)
}
.choices__list--dropdown .choices__item--selectable.is-highlighted::after,
.choices__list[aria-expanded] .choices__item--selectable.is-highlighted::after {
opacity: .5
}
.choices__item {
cursor: default
}
.choices__item--selectable {
cursor: pointer
}
.choices__item--disabled {
cursor: not-allowed;
-webkit-user-select: none;
user-select: none;
opacity: .5
}
.choices__heading {
font-weight: 600;
font-size: 12px;
padding: 10px;
border-bottom: 1px solid #f7f7f7;
color: gray
}
.choices__button {
text-indent: -9999px;
appearance: none;
border: 0;
background-color: transparent;
background-repeat: no-repeat;
background-position: center;
cursor: pointer
}
.choices__button:focus,
.choices__input:focus {
outline: 0
}
.choices__input {
display: inline-block;
vertical-align: baseline;
background-color: var(--color-base-100);
font-size: 14px;
margin-bottom: 5px;
border: 0;
border-radius: 0;
max-width: 100%;
padding: 4px 0 4px 2px
}
.choices__input::-webkit-search-cancel-button,
.choices__input::-webkit-search-decoration,
.choices__input::-webkit-search-results-button,
.choices__input::-webkit-search-results-decoration {
display: none
}
.choices__input::-ms-clear,
.choices__input::-ms-reveal {
display: none;
width: 0;
height: 0
}
[dir=rtl] .choices__input {
padding-right: 2px;
padding-left: 0
}
.choices__placeholder {
opacity: .5
}