Compare commits

...

25 Commits

Author SHA1 Message Date
renovate[bot] f866529307 ⬆️(dependencies) update python dependencies 2026-03-23 01:05:24 +00:00
renovate[bot] 99eaec9653 ⬆️(dependencies) update next to v15.5.14 [SECURITY] 2026-03-20 10:51:17 +00:00
renovate[bot] 2ab3a67a44 ⬆️(dependencies) update next to v15.5.13 [SECURITY] 2026-03-20 10:30:25 +00:00
Marie PUPO JEAMMET 0fd29e82d5 🧑‍💻(makefile) build front when bootstraping
add frontend-dev to build command to rebuild front image
2026-03-19 11:39:14 +00:00
Eléonore 0776b7d95a 💄(ui-kit) regie v2 ui-kit (#1083)
update regie to ui-kit v2
2026-03-19 10:56:07 +01:00
Eléonore Voisin 19f7bb77c0 🚚(route) prioritize mail domains as default landing page
add line in changelog
2026-03-19 10:30:19 +01:00
Louis 83cae4036c (frontend) update test to expect /mail-domains/ as fallback redirect
The fallback now always redirects to /mail-domains/, so the test
with TEAMS_DISPLAY=true should expect /mail-domains/ instead of /teams/.
2026-03-19 10:30:19 +01:00
Louis ed07eecad4 🐛(frontend) fix fallback redirect to always go to /mail-domains/
The ternary was incorrectly inverted, causing users to be sent to
/teams/ when TEAMS_DISPLAY was off. Simplify to always redirect
to /mail-domains/ as the fallback.
2026-03-19 10:30:19 +01:00
Louis 46ba75b119 🚸(frontend) prioritize mail domains as default landing page
(made with ai, review carefully)
 Swap the redirect priority so that users landing on the root page
are directed to /mail-domains/ instead of /teams/ by default.
This applies both to the permission-based checks and the fallback
using the TEAMS_DISPLAY feature flag.
2026-03-19 10:30:19 +01:00
Louis 1b4d1043af 🍱(static) update logo in invitation email template (#1085)
Replace logo-footer-mail.png with an updated version in the
invitation email template. Remove unused logo-suite-numerique.png.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 08:43:29 +01:00
Eléonore bc458b3f68 (datagrid) add sort to mailboxes list + mail domain list (#1057)
add sort to mailboxes list + mail domain list

(invitations) allow delete invitations by an admin (#1052)

allow delete invitations mails domains access by an admin
2026-03-16 16:23:29 +00:00
renovate[bot] 5bc845172b ⬆️(dependencies) update PyJWT to v2.12.0 [SECURITY] 2026-03-16 11:47:31 +00:00
Marie PUPO JEAMMET 8a35e862ff ♻️(tests) finish cleaning dimail token fixture
finish cleaning the fixtures returning valid
token as dimail
2026-03-13 15:20:04 +00:00
Marie PUPO JEAMMET bc0dbcb678 🩹(mailboxes) enforce mailboxes are lowercase
enforce lowercase for new mailboxes, upon creation
and when sending to dimail
2026-03-13 15:20:04 +00:00
Stephan Meijer 68ec85e94a ⬆️(ci) upgrade GitHub Actions workflow steps to latest versions
Update all GitHub Actions to their latest major versions for improved
performance, security patches, and Node.js runtime compatibility.

Signed-off-by: Stephan Meijer <me@stephanmeijer.com>
2026-03-13 14:47:35 +00:00
Marie PUPO JEAMMET ca754d5bcd ⬆️(security) upgrade tornado to fix CVE
Upgrade tornado to fix this CVE
https://github.com/tornadoweb/tornado/security/advisories/GHSA-qjxf-f2mg-c6mc
2026-03-13 14:29:15 +00:00
renovate[bot] 06df255072 ⬆️(dependencies) update python dependencies 2026-03-13 14:29:15 +00:00
Jacob Weisz da1a9e1d7a ✏️(typo) fix readme typo
Small fix of a typo in the README
2026-03-12 18:01:45 +00:00
Louis 74ada8fd6b 🐛(i18n) fix missing translations for status tag labels
(made with ai, review carefully)
Status labels in the Tag component ("enabled", "action required", etc.) were
not translated because `t(status)` used a dynamic variable that i18next-parser
cannot extract.

Replace with a static `translations` map using literal `t()` calls so the keys
are properly detected during extraction and sent to Crowdin for translation.

Co-authored-by: BEY Quentin <quentin.bey@mail.numerique.gouv.fr>
2026-03-12 17:33:22 +00:00
renovate[bot] 0c123bb9de ⬆️(dependencies) update django to v5.2.12 [SECURITY] 2026-03-12 17:17:47 +00:00
renovate[bot] 2a6367c3ac ⬆️(dependencies) update joserfc to v1.6.3 [SECURITY] 2026-03-12 17:17:47 +00:00
Quentin BEY 79bdc6606e ⬆️(dependencies) bump django-lasuite to 0.0.25
Required to use the django-lasuite custom admin.
2026-03-10 14:47:33 +01:00
Quentin BEY c53434411d 🔥(admin) switch to django-lasuite customization
The admin application override has been moved into
the django-lasuite library.
2026-02-26 09:21:56 +01:00
Marie PUPO JEAMMET b9c69e7c08 🚨(lint) fix linter warnings
fix linter warnings after ruff upgrade
2026-02-18 18:13:42 +00:00
renovate[bot] b1e0a24626 ⬆️(dependencies) update python dependencies 2026-02-18 18:13:42 +00:00
162 changed files with 16214 additions and 7393 deletions
+2 -2
View File
@@ -20,7 +20,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Create empty source files
run: |
touch src/backend/locale/django.pot
@@ -48,7 +48,7 @@ jobs:
CROWDIN_BASE_PATH: "../src/"
# frontend i18n
- name: Restore the frontend cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: "src/frontend/**/node_modules"
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
+3 -3
View File
@@ -20,7 +20,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
# Backend i18n
- name: Set up Python
uses: actions/setup-python@v6
@@ -32,7 +32,7 @@ jobs:
run: uv sync --locked --all-extras
working-directory: src/backend
- name: Restore the mail templates
uses: actions/cache@v4
uses: actions/cache@v5
id: mail-templates
with:
path: "src/backend/core/templates/mail"
@@ -48,7 +48,7 @@ jobs:
DJANGO_CONFIGURATION=Build uv run python manage.py makemessages -a --keep-pot
# frontend i18n
- name: Restore the frontend cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: "src/frontend/**/node_modules"
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
+8 -8
View File
@@ -20,16 +20,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Restore the frontend cache
uses: actions/cache@v4
uses: actions/cache@v5
id: front-node_modules
with:
path: "src/frontend/**/node_modules"
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
- name: Setup Node.js
if: steps.front-node_modules.outputs.cache-hit != 'true'
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: ${{ inputs.node_version }}
- name: Install dependencies
@@ -37,7 +37,7 @@ jobs:
run: cd src/frontend/ && yarn install --frozen-lockfile
- name: Cache install frontend
if: steps.front-node_modules.outputs.cache-hit != 'true'
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: "src/frontend/**/node_modules"
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
@@ -50,10 +50,10 @@ jobs:
working-directory: src/mail
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Restore the mail templates
uses: actions/cache@v4
uses: actions/cache@v5
id: mail-templates
with:
path: "src/backend/core/templates/mail"
@@ -61,7 +61,7 @@ jobs:
- name: Setup Node.js
if: steps.mail-templates.outputs.cache-hit != 'true'
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: ${{ inputs.node_version }}
@@ -79,7 +79,7 @@ jobs:
- name: Cache mail templates
if: steps.mail-templates.outputs.cache-hit != 'true'
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: "src/backend/core/templates/mail"
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
steps:
-
name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
-
name: Call argocd github webhook
run: |
+3 -3
View File
@@ -21,7 +21,7 @@ jobs:
steps:
-
name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
-
name: Docker meta
id: meta
@@ -48,7 +48,7 @@ jobs:
steps:
-
name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
-
name: Docker meta
id: meta
@@ -85,7 +85,7 @@ jobs:
steps:
-
name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
-
name: Docker meta
id: meta
+16 -16
View File
@@ -22,7 +22,7 @@ jobs:
if: github.event_name == 'pull_request' # Makes sense only for pull requests
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: show
@@ -49,7 +49,7 @@ jobs:
github.event_name == 'pull_request'
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Check that the CHANGELOG has been modified in the current branch
@@ -59,7 +59,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Check CHANGELOG max line length
run: |
max_line_length=$(cat CHANGELOG.md | grep -Ev "^\[.*\]: https://github.com" | wc -L)
@@ -73,10 +73,10 @@ jobs:
needs: dependencies
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Restore the frontend cache
uses: actions/cache@v4
uses: actions/cache@v5
id: front-node_modules
with:
path: 'src/frontend/**/node_modules'
@@ -86,7 +86,7 @@ jobs:
run: cd src/frontend/ && yarn ci:build
- name: Cache build frontend
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: src/frontend/apps/desk/out/
key: build-front-${{ github.run_id }}
@@ -96,10 +96,10 @@ jobs:
needs: dependencies
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Restore the frontend cache
uses: actions/cache@v4
uses: actions/cache@v5
id: front-node_modules
with:
path: 'src/frontend/**/node_modules'
@@ -113,10 +113,10 @@ jobs:
needs: dependencies
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Restore the frontend cache
uses: actions/cache@v4
uses: actions/cache@v5
id: front-node_modules
with:
path: 'src/frontend/**/node_modules'
@@ -136,7 +136,7 @@ jobs:
shardTotal: [4]
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set services env variables
run: |
@@ -144,7 +144,7 @@ jobs:
cat env.d/development/common.e2e.dist >> env.d/development/common
- name: Restore the mail templates
uses: actions/cache@v4
uses: actions/cache@v5
id: mail-templates
with:
path: "src/backend/core/templates/mail"
@@ -152,14 +152,14 @@ jobs:
fail-on-cache-miss: true
- name: Restore the frontend cache
uses: actions/cache@v4
uses: actions/cache@v5
id: front-node_modules
with:
path: 'src/frontend/**/node_modules'
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
- name: Restore the build cache
uses: actions/cache@v4
uses: actions/cache@v5
id: cache-build
with:
path: src/frontend/apps/desk/out/
@@ -269,13 +269,13 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Create writable /data
run: |
sudo mkdir -p /data/media && \
sudo mkdir -p /data/static
- name: Restore the mail templates
uses: actions/cache@v4
uses: actions/cache@v5
id: mail-templates
with:
path: "src/backend/core/templates/mail"
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
+12
View File
@@ -8,6 +8,17 @@ and this project adheres to
## [Unreleased]
### Fixed
- ✨(mailboxes) enforce lowercase on mailboxes
- 🐛(i18n) fix missing translations for status tag labels
- 🚚(route) prioritize mail domains as default landing page
### Changed
- 🍱(static) update logo in invitation email template #1085
- ✨(uiV2) use Lasuite UI kit, new layout
## [1.23.1] - 2026-02-16
- ✨(invitations) refresh expired invitations
@@ -18,6 +29,7 @@ and this project adheres to
- ✨(demo) add aliases to demo #1050
- ✨(front) add icon to button to configure a domain
- ✨(datagrid) add sort to mailboxes list + mail domain list
- ✨(invitations) allow delete invitations mails domains access by an admin
- ✨(front) delete invitations mails domains access
- ✨(front) add show invitations mails domains access #1040
+6 -6
View File
@@ -107,7 +107,7 @@ bootstrap: \
# -- Docker/compose
build: ## build the app-dev container
@$(COMPOSE) build app-dev
@$(COMPOSE) build app-dev frontend-dev
@$(COMPOSE) build dimail
.PHONY: build
@@ -241,21 +241,21 @@ resetdb: ## flush database and create a superuser "admin"
.PHONY: resetdb
env.d/development/common:
cp -n env.d/development/common.dist env.d/development/common
cp --update=none env.d/development/common.dist env.d/development/common
env.d/development/france:
cp -n env.d/development/france.dist env.d/development/france
cp --update=none env.d/development/france.dist env.d/development/france
env.d/development/postgresql:
cp -n env.d/development/postgresql.dist env.d/development/postgresql
cp --update=none env.d/development/postgresql.dist env.d/development/postgresql
env.d/development/kc_postgresql:
cp -n env.d/development/kc_postgresql.dist env.d/development/kc_postgresql
cp --update=none env.d/development/kc_postgresql.dist env.d/development/kc_postgresql
# -- Internationalization
env.d/development/crowdin:
cp -n env.d/development/crowdin.dist env.d/development/crowdin
cp --update=none env.d/development/crowdin.dist env.d/development/crowdin
crowdin-download: ## Download translated message from Crowdin
@$(COMPOSE_RUN_CROWDIN) download -c crowdin/config.yml
+1 -1
View File
@@ -1,6 +1,6 @@
# People
People is an application to handle users and teams, and distribute permissions accross [La Suite](https://lasuite.numerique.gouv.fr/).
People is an application to handle users and teams, and distribute permissions across [La Suite](https://lasuite.numerique.gouv.fr/).
It is built on top of [Django Rest
Framework](https://www.django-rest-framework.org/).
-1
View File
@@ -1 +0,0 @@
"""People custom admin site."""
-9
View File
@@ -1,9 +0,0 @@
"""Custom Django admin site application configuration."""
from django.contrib.admin.apps import AdminConfig
class PeopleAdminConfig(AdminConfig):
"""Declare our custom Django admin site."""
default_site = "admin.sites.PeopleAdminSite"
-15
View File
@@ -1,15 +0,0 @@
"""Custom Django admin site for the People app."""
from django.conf import settings
from django.contrib import admin
class PeopleAdminSite(admin.AdminSite):
"""People custom admin site."""
def each_context(self, request):
"""Add custom context to the admin site."""
return super().each_context(request) | {
"ADMIN_HEADER_BACKGROUND": settings.ADMIN_HEADER_BACKGROUND,
"ADMIN_HEADER_COLOR": settings.ADMIN_HEADER_COLOR,
}
@@ -1,13 +0,0 @@
{% extends "admin/base_site.html" %}
{% block extrastyle %}{{ block.super }}
<style>
html[data-theme="light"], :root {
{% if ADMIN_HEADER_BACKGROUND %}--header-bg: {{ ADMIN_HEADER_BACKGROUND }};{% endif %}
{% if ADMIN_HEADER_COLOR %}--header-color: {{ ADMIN_HEADER_COLOR }};{% endif %}
}
ul.messagelist li.info {
background-color: #EEEEEE;
}
</style>
{% endblock %}
@@ -11,7 +11,7 @@ def update_team_paths(apps, schema_editor):
steplen = 5
# Initialize NumConv with the specified custom alphabet
converter = NumConv(len(alphabet), alphabet)
converter = NumConv(alphabet)
nodes = Team.objects.all().order_by("created_at")
for i, node in enumerate(nodes, 1):
+3 -3
View File
@@ -814,7 +814,7 @@ class Team(MP_Node, BaseModel):
except AttributeError:
try:
role = self.accesses.filter(user=user).values("role")[0]["role"]
except (TeamAccess.DoesNotExist, IndexError):
except TeamAccess.DoesNotExist, IndexError:
role = None
is_owner_or_admin = role in [RoleChoices.OWNER, RoleChoices.ADMIN]
@@ -901,7 +901,7 @@ class TeamAccess(BaseModel):
role = self._meta.model.objects.filter(
team=self.team_id, user=user
).values("role")[0]["role"]
except (self._meta.model.DoesNotExist, IndexError):
except self._meta.model.DoesNotExist, IndexError:
role = None
is_team_owner_or_admin = role in [RoleChoices.OWNER, RoleChoices.ADMIN]
@@ -1100,7 +1100,7 @@ class Invitation(BaseInvitation):
role = self.team.accesses.filter(user=user).values("role")[0][
"role"
]
except (self._meta.model.DoesNotExist, IndexError):
except self._meta.model.DoesNotExist, IndexError:
role = None
can_delete = role in [RoleChoices.OWNER, RoleChoices.ADMIN]
+1 -1
View File
@@ -9,7 +9,7 @@ plugins_urlpatterns = []
for app in settings.INSTALLED_PLUGINS:
try:
plugins_urlpatterns.append(path("", include(f"{app}.urls")))
except (ImportError, AttributeError):
except ImportError, AttributeError:
# Skip if app doesn't have urls.py
continue
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

+7 -5
View File
@@ -111,7 +111,7 @@ class MailDomain(BaseModel):
if user.is_authenticated:
try:
role = self.accesses.filter(user=user).values("role")[0]["role"]
except (MailDomainAccess.DoesNotExist, IndexError):
except MailDomainAccess.DoesNotExist, IndexError:
role = None
is_owner_or_admin = role in [
@@ -224,7 +224,7 @@ class MailDomainAccess(BaseModel):
authenticated_user_role = user.mail_domain_accesses.get(
domain=self.domain
).role
except (MailDomainAccess.DoesNotExist, IndexError):
except MailDomainAccess.DoesNotExist, IndexError:
return []
# only an owner can set an owner role
@@ -252,7 +252,7 @@ class MailDomainAccess(BaseModel):
if user.is_authenticated:
try:
role = user.mail_domain_accesses.filter(domain=self.domain).get().role
except (MailDomainAccess.DoesNotExist, IndexError):
except MailDomainAccess.DoesNotExist, IndexError:
role = None
is_owner_or_admin = role in [
@@ -351,6 +351,8 @@ Last name and Domain already exists.",
raise exceptions.ValidationError(
_("You can't create or update a mailbox for a disabled domain.")
)
self.local_part = self.local_part.lower()
return super().save(*args, **kwargs)
@property
@@ -459,7 +461,7 @@ class MailDomainInvitation(BaseInvitation):
role = self.domain.accesses.filter(user=user).values("role")[0][
"role"
]
except (self._meta.model.DoesNotExist, IndexError):
except self._meta.model.DoesNotExist, IndexError:
role = None
can_delete = role in [
@@ -515,7 +517,7 @@ class Alias(BaseModel):
edit aliases, but also viewer if the alias points to their email."""
try:
role = user.mail_domain_accesses.get(domain=self.domain).role
except (MailDomainAccess.DoesNotExist, IndexError):
except MailDomainAccess.DoesNotExist, IndexError:
role = None
is_owner_or_admin = role in [
@@ -58,7 +58,7 @@ def test_api_mailboxes__create_no_access_forbidden_not_found(mailbox_data):
assert not models.Mailbox.objects.exists()
def test_api_mailboxes__create_viewer_failure(mailbox_data):
def test_api_mailboxes__create_viewer_forbidden(mailbox_data):
"""Users with viewer role should not be able to create mailbox on the mail domain."""
mail_domain = factories.MailDomainEnabledFactory()
access = factories.MailDomainAccessFactory(
@@ -89,7 +89,7 @@ def test_api_mailboxes__create_display_name_must_be_unique():
new_mailbox_data = {
"local_part": "something_else",
"first_name": existing_mailbox.first_name.upper(), # ensure case-insensitivity
"first_name": existing_mailbox.first_name.upper(), # checks case-insensitivity
"last_name": existing_mailbox.last_name.lower(),
}
response = client.post(
@@ -239,6 +239,35 @@ def test_api_mailboxes__create_with_accent_success(role, dimail_token_ok):
}
@responses.activate
def test_api_mailboxes__create_lowercase(dimail_token_ok, mailbox_data):
"""Should lowercase emails on creation."""
access = factories.MailDomainAccessFactory(role=enums.MailDomainRoleChoices.ADMIN)
mailbox_data["local_part"] = "WeiRdCaSe"
# ensure response
# token response in fixtures
responses.add(
responses.POST,
re.compile(rf".*/domains/{access.domain.name}/mailboxes/"),
body=response_mailbox_created(
f"{mailbox_data['local_part']}@{access.domain.name}"
),
status=status.HTTP_201_CREATED,
content_type="application/json",
)
client = APIClient()
client.force_login(access.user)
response = client.post(
f"/api/v1.0/mail-domains/{access.domain.slug}/mailboxes/",
mailbox_data,
format="json",
)
assert response.status_code == status.HTTP_201_CREATED
assert response.json()["local_part"] == mailbox_data["local_part"].lower()
def test_api_mailboxes__create_administrator_missing_fields():
"""
Administrator users should not be able to create mailboxes
@@ -113,7 +113,7 @@ Please add a valid secondary email before trying again."
],
)
@responses.activate
def test_api_mailboxes__reset_password_admin_successful(role):
def test_api_mailboxes__reset_password_admin_successful(role, dimail_token_ok): # pylint: disable=W0613
"""Owner and admin users should be able to reset password on mailboxes.
New password should be sent to secondary email."""
mail_domain = factories.MailDomainEnabledFactory()
@@ -124,12 +124,7 @@ def test_api_mailboxes__reset_password_admin_successful(role):
client.force_login(access.user)
dimail_url = settings.MAIL_PROVISIONING_API_URL
responses.add(
responses.GET,
f"{dimail_url}/token/",
body=dimail.TOKEN_OK,
status=200,
)
# token response in fixtures
responses.add(
responses.POST,
f"{dimail_url}/domains/{mail_domain.name}/mailboxes/{mailbox.local_part}/reset_password/",
@@ -161,7 +156,7 @@ def test_api_mailboxes__reset_password_non_existing():
@responses.activate
def test_api_mailboxes__reset_password_connexion_failed():
def test_api_mailboxes__reset_password_connexion_failed(dimail_token_ok): # pylint: disable=W0613
"""
No mail is sent when password reset failed because of connexion error.
"""
@@ -175,12 +170,7 @@ def test_api_mailboxes__reset_password_connexion_failed():
client.force_login(access.user)
dimail_url = settings.MAIL_PROVISIONING_API_URL
responses.add(
responses.GET,
f"{dimail_url}/token/",
body=dimail.TOKEN_OK,
status=200,
)
# token response in fixtures
responses.add(
responses.POST,
f"{dimail_url}/domains/{mail_domain.name}/mailboxes/{mailbox.local_part}/reset_password/",
@@ -9,17 +9,15 @@ import responses
from rest_framework import status
from mailbox_manager import factories
from mailbox_manager.tests.fixtures.dimail import TOKEN_OK
## DIMAIL RESPONSES
@pytest.fixture(name="dimail_token_ok")
def fixture_dimail_token_ok():
"""Mock dimail response when /token/ endpoit is given valid credentials."""
responses.add(
responses.GET,
responses.get(
re.compile(r".*/token/"),
body=TOKEN_OK,
json={"access_token": "token", "token_type": "bearer"},
status=status.HTTP_200_OK,
content_type="application/json",
)
+1 -5
View File
@@ -287,10 +287,6 @@ DOMAIN_SPEC = [
]
## TOKEN
TOKEN_OK = json.dumps({"access_token": "token", "token_type": "bearer"})
## ALLOWS
def response_allows_created(user_name, domain_name):
"""mimic dimail response upon successful allows creation.
@@ -301,4 +297,4 @@ def response_allows_created(user_name, domain_name):
## MAILBOXES
def response_mailbox_created(email_address):
"""mimic dimail response upon successful mailbox creation."""
return json.dumps({"email": email_address, "password": "password"})
return json.dumps({"email": email_address.lower(), "password": "password"})
@@ -19,7 +19,6 @@ from .fixtures.dimail import (
CHECK_DOMAIN_BROKEN,
CHECK_DOMAIN_OK,
DOMAIN_SPEC,
TOKEN_OK,
response_mailbox_created,
)
@@ -101,7 +100,9 @@ def test_fetch_domain_status__should_switch_to_failed_when_domain_broken(client)
@responses.activate
@pytest.mark.django_db
def test_fetch_domain_status__should_switch_to_enabled_when_domain_ok(client):
def test_fetch_domain_status__should_switch_to_enabled_when_domain_ok(
client, dimail_token_ok
): # pylint: disable=W0613
"""Test admin action should switch domain state to ENABLED
when dimail's response is "ok". It should also activate any pending mailbox."""
admin = core_factories.UserFactory(is_staff=True, is_superuser=True)
@@ -127,13 +128,7 @@ def test_fetch_domain_status__should_switch_to_enabled_when_domain_ok(client):
content_type="application/json",
)
# we need to get a token to create mailboxes
responses.add(
responses.GET,
re.compile(r".*/token/"),
body=TOKEN_OK,
status=status.HTTP_200_OK,
content_type="application/json",
)
# token response in fixtures
responses.add(
responses.POST,
re.compile(rf".*/domains/{domain1.name}/mailboxes/"),
@@ -208,7 +203,7 @@ def test_fetch_domain_expected_config__should_not_fetch_for_disabled_domain(clie
@responses.activate
@pytest.mark.django_db
def test_send_pending_mailboxes(client):
def test_send_pending_mailboxes(client, dimail_token_ok): # pylint: disable=W0613
"""Test admin action to send pending mailboxes to dimail."""
admin = core_factories.UserFactory(is_staff=True, is_superuser=True)
client.force_login(admin)
@@ -223,13 +218,7 @@ def test_send_pending_mailboxes(client):
url = reverse("admin:mailbox_manager_maildomain_changelist")
for mailbox in mailboxes:
responses.add(
responses.GET,
re.compile(r".*/token/"),
body=TOKEN_OK,
status=status.HTTP_200_OK,
content_type="application/json",
)
# token response in fixtures
responses.add(
responses.POST,
re.compile(rf".*/domains/{domain.name}/mailboxes/"),
@@ -247,7 +236,7 @@ def test_send_pending_mailboxes(client):
@responses.activate
@pytest.mark.django_db
def test_send_pending_mailboxes__listing_failed_mailboxes(client):
def test_send_pending_mailboxes__listing_failed_mailboxes(client, dimail_token_ok): # pylint: disable=W0613
"""Test admin action to send pending mailboxes to dimail."""
admin = core_factories.UserFactory(is_staff=True, is_superuser=True)
client.force_login(admin)
@@ -261,13 +250,7 @@ def test_send_pending_mailboxes__listing_failed_mailboxes(client):
}
url = reverse("admin:mailbox_manager_maildomain_changelist")
responses.add(
responses.GET,
re.compile(r".*/token/"),
body=TOKEN_OK,
status=status.HTTP_200_OK,
content_type="application/json",
)
# token response in fixtures
responses.add(
responses.POST,
re.compile(rf".*/domains/{domain.name}/mailboxes/"),
+2 -4
View File
@@ -173,9 +173,6 @@ class Base(Configuration):
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [
os.path.join(BASE_DIR, "templates"),
os.path.join(
BASE_DIR, "admin", "templates"
), # enforce load before Django's admin
],
"OPTIONS": {
"context_processors": [
@@ -227,7 +224,6 @@ class Base(Configuration):
INSTALLED_APPS = [
# People
"admin.apps.PeopleAdminConfig", # replaces 'django.contrib.admin'
"core",
"demo",
"mailbox_manager.apps.MailboxManagerConfig",
@@ -246,6 +242,7 @@ class Base(Configuration):
"parler",
"rest_framework",
"treebeard",
"lasuite.admin", # must be before django.contrib.admin
# Django
"django.contrib.auth",
"django.contrib.contenttypes",
@@ -254,6 +251,7 @@ class Base(Configuration):
"django.contrib.sites",
"django.contrib.messages",
"django.contrib.staticfiles",
"django.contrib.admin",
# OIDC third party
"mozilla_django_oidc",
]
+20 -20
View File
@@ -26,42 +26,42 @@ readme = "README.md"
requires-python = "~=3.14.2"
dependencies = [
"Brotli==1.2.0",
"PyJWT==2.10.1",
"boto3==1.42.34",
"PyJWT==2.12.1",
"boto3==1.42.68",
"celery[redis]==5.6.2",
"django-celery-beat==2.8.1",
"django-celery-beat==2.9.0",
"django-celery-results==2.6.0",
"django-configurations==2.5.1",
"django-cors-headers==4.9.0",
"django-countries==8.2.0",
"django-extensions==4.1",
"django-lasuite==0.0.23",
"django-lasuite==0.0.25",
"django-oauth-toolkit==3.2.0",
"django-parler==2.3",
"django-redis==6.0.0",
"django-storages==1.14.6",
"django-timezone-field>=5.1",
"django-treebeard==4.8.0",
"django-treebeard==5.0.5",
"django-zxcvbn-password-validator==1.5.3",
"django==5.2.11",
"django==6.0.3",
"djangorestframework==3.16.1",
"dockerflow==2024.4.2",
"dockerflow==2026.3.4",
"drf_spectacular==0.29.0",
"drf_spectacular[sidecar]==0.29.0",
"easy_thumbnails==2.10.1",
"factory_boy==3.3.3",
"flower==2.0.1",
"gunicorn==24.1.1",
"gunicorn==25.1.0",
"jaraco.context>=6.1.0",
"joserfc==1.6.1",
"joserfc==1.6.3",
"jsonschema==4.26.0",
"mozilla-django-oidc==5.0.2",
"nested-multipart-parser==1.6.0",
"psycopg[binary]==3.3.2",
"redis<=7.1.0",
"psycopg[binary]==3.3.3",
"redis<=7.3.0",
"requests==2.32.5",
"sentry-sdk[django]==2.50.0",
"whitenoise==6.11.0",
"sentry-sdk[django]==2.54.0",
"whitenoise==6.12.0",
]
[project.urls]
@@ -72,20 +72,20 @@ dependencies = [
[project.optional-dependencies]
dev = [
"drf-spectacular-sidecar==2026.1.1",
"drf-spectacular-sidecar==2026.3.1",
"ipdb==0.13.13",
"ipython==9.9.0",
"ipython==9.11.0",
"jq==1.11.0",
"pyfakefs==6.0.0",
"pyfakefs==6.1.5",
"pylint-django==2.7.0",
"pylint==4.0.4",
"pylint==4.0.5",
"pytest-cov==7.0.0",
"pytest-django==4.11.1",
"pytest-django==4.12.0",
"pytest==9.0.2",
"pytest-icdiff==0.9",
"pytest-xdist==3.8.0",
"responses==0.25.8",
"ruff==0.14.14",
"responses==0.26.0",
"ruff==0.15.6",
"types-requests==2.32.4.20260107",
"freezegun==1.5.5",
]
+122 -128
View File
@@ -61,30 +61,30 @@ wheels = [
[[package]]
name = "boto3"
version = "1.42.34"
version = "1.42.68"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "botocore" },
{ name = "jmespath" },
{ name = "s3transfer" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d0/69/c0d4cc77add3cdf66f8573555d71dc23ba32dfe77df40e1c91385f7a9bdc/boto3-1.42.34.tar.gz", hash = "sha256:75d7443c81a029283442fad138629be1eefaa3e6d430c28118a0f4cdbd57855d", size = 112876, upload-time = "2026-01-23T20:29:47.213Z" }
sdist = { url = "https://files.pythonhosted.org/packages/06/ae/60c642aa5413e560b671da825329f510b29a77274ed0f580bde77562294d/boto3-1.42.68.tar.gz", hash = "sha256:3f349f967ab38c23425626d130962bcb363e75f042734fe856ea8c5a00eef03c", size = 112761, upload-time = "2026-03-13T19:32:17.137Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/55/25c543864abc270f5fdd7814fa7b69fd23de1c40fb3d7993f4b6391f8d3b/boto3-1.42.34-py3-none-any.whl", hash = "sha256:db3fb539e3f806b911ec4ca991f2f8bff333c5f0b87132a82e28b521fc5ec164", size = 140574, upload-time = "2026-01-23T20:29:45.916Z" },
{ url = "https://files.pythonhosted.org/packages/fb/f6/dc6e993479dbb597d68223fbf61cb026511737696b15bd7d2a33e9b2c24f/boto3-1.42.68-py3-none-any.whl", hash = "sha256:dbff353eb7dc93cbddd7926ed24793e0174c04adbe88860dfa639568442e4962", size = 140556, upload-time = "2026-03-13T19:32:14.951Z" },
]
[[package]]
name = "botocore"
version = "1.42.42"
version = "1.42.73"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jmespath" },
{ name = "python-dateutil" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7d/96/4eca9755ca444402c46c73cc8ff252c8eb73ab0ccf35ca76d89e7b7820ac/botocore-1.42.42.tar.gz", hash = "sha256:cb75639f5ba7bf73b83ac18bcd87f07b7f484f302748da974dad2801a83a1d60", size = 14926585, upload-time = "2026-02-04T20:28:33.66Z" }
sdist = { url = "https://files.pythonhosted.org/packages/28/23/0c88ca116ef63b1ae77c901cd5d2095d22a8dbde9e80df74545db4a061b4/botocore-1.42.73.tar.gz", hash = "sha256:575858641e4949aaf2af1ced145b8524529edf006d075877af6b82ff96ad854c", size = 15008008, upload-time = "2026-03-20T19:39:40.082Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e6/51/aac7e419521d5519e13087a7198623655648c939822bd7f4bdc9ccbe07f9/botocore-1.42.42-py3-none-any.whl", hash = "sha256:1c9df5fc31e9073a9aa956271c4007d72f5d342cafca5f4154ea099bc6f83085", size = 14600186, upload-time = "2026-02-04T20:28:29.268Z" },
{ url = "https://files.pythonhosted.org/packages/8e/65/971f3d55015f4d133a6ff3ad74cd39f4b8dd8f53f7775a3c2ad378ea5145/botocore-1.42.73-py3-none-any.whl", hash = "sha256:7b62e2a12f7a1b08eb7360eecd23bb16fe3b7ab7f5617cf91b25476c6f86a0fe", size = 14681861, upload-time = "2026-03-20T19:39:35.341Z" },
]
[[package]]
@@ -292,14 +292,11 @@ wheels = [
[[package]]
name = "cron-descriptor"
version = "2.0.6"
version = "1.4.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7c/31/0b21d1599656b2ffa6043e51ca01041cd1c0f6dacf5a3e2b620ed120e7d8/cron_descriptor-2.0.6.tar.gz", hash = "sha256:e39d2848e1d8913cfb6e3452e701b5eec662ee18bea8cc5aa53ee1a7bb217157", size = 49456, upload-time = "2025-09-03T16:30:22.434Z" }
sdist = { url = "https://files.pythonhosted.org/packages/02/83/70bd410dc6965e33a5460b7da84cf0c5a7330a68d6d5d4c3dfdb72ca117e/cron_descriptor-1.4.5.tar.gz", hash = "sha256:f51ce4ffc1d1f2816939add8524f206c376a42c87a5fca3091ce26725b3b1bca", size = 30666, upload-time = "2024-08-24T18:16:48.654Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/21/cc/361326a54ad92e2e12845ad15e335a4e14b8953665007fb514d3393dfb0f/cron_descriptor-2.0.6-py3-none-any.whl", hash = "sha256:3a1c0d837c0e5a32e415f821b36cf758eb92d510e6beff8fbfe4fa16573d93d6", size = 74446, upload-time = "2025-09-03T16:30:21.397Z" },
{ url = "https://files.pythonhosted.org/packages/88/20/2cfe598ead23a715a00beb716477cfddd3e5948cf203c372d02221e5b0c6/cron_descriptor-1.4.5-py3-none-any.whl", hash = "sha256:736b3ae9d1a99bc3dbfc5b55b5e6e7c12031e7ba5de716625772f8b02dcd6013", size = 50370, upload-time = "2024-08-24T18:16:46.783Z" },
]
[[package]]
@@ -375,21 +372,21 @@ wheels = [
[[package]]
name = "django"
version = "5.2.11"
version = "6.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "sqlparse" },
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/17/f2/3e57ef696b95067e05ae206171e47a8e53b9c84eec56198671ef9eaa51a6/django-5.2.11.tar.gz", hash = "sha256:7f2d292ad8b9ee35e405d965fbbad293758b858c34bbf7f3df551aeeac6f02d3", size = 10885017, upload-time = "2026-02-03T13:52:50.554Z" }
sdist = { url = "https://files.pythonhosted.org/packages/80/e1/894115c6bd70e2c8b66b0c40a3c367d83a5a48c034a4d904d31b62f7c53a/django-6.0.3.tar.gz", hash = "sha256:90be765ee756af8a6cbd6693e56452404b5ad15294f4d5e40c0a55a0f4870fe1", size = 10872701, upload-time = "2026-03-03T13:55:15.026Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/91/a7/2b112ab430575bf3135b8304ac372248500d99c352f777485f53fdb9537e/django-5.2.11-py3-none-any.whl", hash = "sha256:e7130df33ada9ab5e5e929bc19346a20fe383f5454acb2cc004508f242ee92c0", size = 8291375, upload-time = "2026-02-03T13:52:42.47Z" },
{ url = "https://files.pythonhosted.org/packages/72/b1/23f2556967c45e34d3d3cf032eb1bd3ef925ee458667fb99052a0b3ea3a6/django-6.0.3-py3-none-any.whl", hash = "sha256:2e5974441491ddb34c3f13d5e7a9f97b07ba03bf70234c0a9c68b79bbb235bc3", size = 8358527, upload-time = "2026-03-03T13:55:10.552Z" },
]
[[package]]
name = "django-celery-beat"
version = "2.8.1"
version = "2.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "celery" },
@@ -399,9 +396,9 @@ dependencies = [
{ name = "python-crontab" },
{ name = "tzdata" },
]
sdist = { url = "https://files.pythonhosted.org/packages/aa/11/0c8b412869b4fda72828572068312b10aafe7ccef7b41af3633af31f9d4b/django_celery_beat-2.8.1.tar.gz", hash = "sha256:dfad0201c0ac50c91a34700ef8fa0a10ee098cc7f3375fe5debed79f2204f80a", size = 175802, upload-time = "2025-05-13T06:58:29.246Z" }
sdist = { url = "https://files.pythonhosted.org/packages/05/45/fc97bc1d9af8e7dc07f1e37044d9551a30e6793249864cef802341e2e3a8/django_celery_beat-2.9.0.tar.gz", hash = "sha256:92404650f52fcb44cf08e2b09635cb1558327c54b1a5d570f0e2d3a22130934c", size = 177667, upload-time = "2026-02-28T16:45:34.749Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/61/e5/3a0167044773dee989b498e9a851fc1663bea9ab879f1179f7b8a827ac10/django_celery_beat-2.8.1-py3-none-any.whl", hash = "sha256:da2b1c6939495c05a551717509d6e3b79444e114a027f7b77bf3727c2a39d171", size = 104833, upload-time = "2025-05-13T06:58:27.309Z" },
{ url = "https://files.pythonhosted.org/packages/71/ae/9befa7ae37f5e5c41be636a254fcf47ff30dd5c88bd115070e252f6b9162/django_celery_beat-2.9.0-py3-none-any.whl", hash = "sha256:4a9e5ebe26d6f8d7215e1fc5c46e466016279dc102435a28141108649bdf2157", size = 105013, upload-time = "2026-02-28T16:45:32.822Z" },
]
[[package]]
@@ -469,7 +466,7 @@ wheels = [
[[package]]
name = "django-lasuite"
version = "0.0.23"
version = "0.0.25"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
@@ -480,9 +477,9 @@ dependencies = [
{ name = "requests" },
{ name = "requests-toolbelt" },
]
sdist = { url = "https://files.pythonhosted.org/packages/59/a7/4dc94272ad9cb9d8ea0af6731c551d51d9196b0f93a4976b2b41b3eb4038/django_lasuite-0.0.23.tar.gz", hash = "sha256:34bc2bf84feab8694846cbdb573762f02c79e355384d0dc5b395e0e416dfb12d", size = 34228, upload-time = "2026-01-14T14:02:56.313Z" }
sdist = { url = "https://files.pythonhosted.org/packages/5e/bc/5826a3e5ada5dad1ff6af9a2e1eb598a0dc2cb9c4d2dbf011f1bbb310c9e/django_lasuite-0.0.25.tar.gz", hash = "sha256:ee44783942e6ead74a732f6d7280c5fca961b66581350ce01c0589c3e80684cf", size = 34819, upload-time = "2026-03-10T13:40:28.705Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/13/9f/5956361330194e9b8a33388dfb107ea08a7cf12ddccc48608f7cc2c2ca4e/django_lasuite-0.0.23-py3-none-any.whl", hash = "sha256:8726ca7d8ed6c12078e5168ead9dff51706bf9e73dc84921c8d211fef31961ff", size = 52543, upload-time = "2026-01-14T14:02:54.479Z" },
{ url = "https://files.pythonhosted.org/packages/61/2f/819ebbc9d3a4b8233cef1e9b0b607724e4acf527988393b04a4036b9c235/django_lasuite-0.0.25-py3-none-any.whl", hash = "sha256:4c10f625005cd41d05e8d34269f1a3b58fea8be5296528bd58596227f50d9884", size = 54034, upload-time = "2026-03-10T13:40:26.903Z" },
]
[[package]]
@@ -551,14 +548,14 @@ wheels = [
[[package]]
name = "django-treebeard"
version = "4.8.0"
version = "5.0.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7a/d2/615c2d33b3b55e106ee590b164c0d4a8372527293c85c61175e81ded2aea/django_treebeard-4.8.0.tar.gz", hash = "sha256:61b8076b576107da21f6f6040774c0d17025200c2efdb70dd1f14b18c9206c3a", size = 292517, upload-time = "2025-12-03T03:58:40.625Z" }
sdist = { url = "https://files.pythonhosted.org/packages/19/31/d9380569d2e185458240497c948591d9183bd5c77d11800441b1ccee6933/django_treebeard-5.0.5.tar.gz", hash = "sha256:edca8ef8f92a92d607a1310838fe02806414d0678187316b43b48677aff2f51c", size = 299223, upload-time = "2026-02-19T13:47:17.211Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/aa/3a/3731f6d5696b5dfd1c7830a64450e1f49b512cb192b2d3e37b72cc75470b/django_treebeard-4.8.0-py3-none-any.whl", hash = "sha256:ecaa7a0bbfa86a48fc91ff335513e83c841cdfe1add6229d04ac7096198d5d51", size = 70997, upload-time = "2025-12-03T03:58:37.292Z" },
{ url = "https://files.pythonhosted.org/packages/21/27/fac0bc188340e39b35ec916b6da2919c387df2e512d622af665e59600f97/django_treebeard-5.0.5-py3-none-any.whl", hash = "sha256:879b3103c2d25fb98750e3b61d9bcda58c18e4617f5e1156bbc50a79104c9f1f", size = 76185, upload-time = "2026-02-19T13:47:15.636Z" },
]
[[package]]
@@ -588,11 +585,11 @@ wheels = [
[[package]]
name = "dockerflow"
version = "2024.4.2"
version = "2026.3.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0e/73/39d187c76e7f7e9ffd84acbfade0e137da6def46f955761128c93c9396f1/dockerflow-2024.4.2.tar.gz", hash = "sha256:f4216a3a809093860d7b2db84ba0a25c894cb8eb98b74f4f6a04badbc4f6b0a4", size = 67602, upload-time = "2024-04-24T20:13:39.934Z" }
sdist = { url = "https://files.pythonhosted.org/packages/9c/03/935f8399094611981b37634a7ee078484b3995f744135339e51385638f6c/dockerflow-2026.3.4.tar.gz", hash = "sha256:6cc7fcab2a078d06aeca8b02e22fd7f6766dca322fb30aed6910684dbc08690c", size = 68701, upload-time = "2026-03-04T09:01:28.178Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/44/55/496c65f8c869c9c1ddc86b1bd086ac08b29806e3058c8982c9a6cb1e99a6/dockerflow-2024.4.2-py2.py3-none-any.whl", hash = "sha256:b9f92455449ba46555f57db34cccefc4c49d3533c67793624ab7e80a1625caa7", size = 35862, upload-time = "2024-04-24T20:13:38.519Z" },
{ url = "https://files.pythonhosted.org/packages/86/b9/2d7bf30eb0c24e60e61e4f6ae7574e123d3fa51b39a612e146c94158446a/dockerflow-2026.3.4-py2.py3-none-any.whl", hash = "sha256:38fe0ad30a4aedf68e589919695d502fc6f584a761b56a91f56e80cbadfbf61a", size = 36424, upload-time = "2026-03-04T09:01:26.631Z" },
]
[[package]]
@@ -619,14 +616,14 @@ sidecar = [
[[package]]
name = "drf-spectacular-sidecar"
version = "2026.1.1"
version = "2026.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1e/81/c7b0e3ccbd5a039c4f4fcfecf88391a666ca1406a953886e2f39295b1c90/drf_spectacular_sidecar-2026.1.1.tar.gz", hash = "sha256:6f7c173a8ddbbbdafc7a27e028614b65f07a89ca90f996a432d57460463b56be", size = 2468060, upload-time = "2026-01-01T11:27:12.682Z" }
sdist = { url = "https://files.pythonhosted.org/packages/aa/42/2f8c1b2846399d47094ec414bc0d6a7cce7ba95fd6545a97285eee89f7f1/drf_spectacular_sidecar-2026.3.1.tar.gz", hash = "sha256:5b7fedad66e3851f2f442480792c08115d79217959d01645b93d3d2258938be1", size = 2461501, upload-time = "2026-03-01T11:31:19.708Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/db/96/38725edda526f3e9e597f531beeec94b0ef433d9494f06a13b7636eecb6e/drf_spectacular_sidecar-2026.1.1-py3-none-any.whl", hash = "sha256:af8df62f1b594ec280351336d837eaf2402ab25a6bc2a1fad7aee9935821070f", size = 2489520, upload-time = "2026-01-01T11:27:11.056Z" },
{ url = "https://files.pythonhosted.org/packages/c1/28/2d5e64d101ebc5180674fcaf7b5a35e398e2f8d9688b2e8d52b0e1394e7d/drf_spectacular_sidecar-2026.3.1-py3-none-any.whl", hash = "sha256:864edb83e022e13e3941c325c3cc0c954c843fa2e1d0bc95e81887664b2d3dad", size = 2481725, upload-time = "2026-03-01T11:31:18.469Z" },
]
[[package]]
@@ -714,14 +711,14 @@ wheels = [
[[package]]
name = "gunicorn"
version = "24.1.1"
version = "25.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
]
sdist = { url = "https://files.pythonhosted.org/packages/78/0a/10739c03537ec5b131a867bf94df2e412b437696c7e5d26970e2198a80d2/gunicorn-24.1.1.tar.gz", hash = "sha256:f006d110e5cb3102859b4f5cd48335dbd9cc28d0d27cd24ddbdafa6c60929408", size = 287567, upload-time = "2026-01-24T01:15:31.359Z" }
sdist = { url = "https://files.pythonhosted.org/packages/66/13/ef67f59f6a7896fdc2c1d62b5665c5219d6b0a9a1784938eb9a28e55e128/gunicorn-25.1.0.tar.gz", hash = "sha256:1426611d959fa77e7de89f8c0f32eed6aa03ee735f98c01efba3e281b1c47616", size = 594377, upload-time = "2026-02-13T11:09:58.989Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/96/90/cfe637677916fc6f53cd2b05d5746e249f683e1fa14c9e745a88c66f7290/gunicorn-24.1.1-py3-none-any.whl", hash = "sha256:757f6b621fc4f7581a90600b2cd9df593461f06a41d7259cb9b94499dc4095a8", size = 114920, upload-time = "2026-01-24T01:15:29.656Z" },
{ url = "https://files.pythonhosted.org/packages/da/73/4ad5b1f6a2e21cf1e85afdaad2b7b1a933985e2f5d679147a1953aaa192c/gunicorn-25.1.0-py3-none-any.whl", hash = "sha256:d0b1236ccf27f72cfe14bce7caadf467186f19e865094ca84221424e839b8b8b", size = 197067, upload-time = "2026-02-13T11:09:57.146Z" },
]
[[package]]
@@ -784,7 +781,7 @@ wheels = [
[[package]]
name = "ipython"
version = "9.9.0"
version = "9.11.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
@@ -798,9 +795,9 @@ dependencies = [
{ name = "stack-data" },
{ name = "traitlets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/46/dd/fb08d22ec0c27e73c8bc8f71810709870d51cadaf27b7ddd3f011236c100/ipython-9.9.0.tar.gz", hash = "sha256:48fbed1b2de5e2c7177eefa144aba7fcb82dac514f09b57e2ac9da34ddb54220", size = 4425043, upload-time = "2026-01-05T12:36:46.233Z" }
sdist = { url = "https://files.pythonhosted.org/packages/86/28/a4698eda5a8928a45d6b693578b135b753e14fa1c2b36ee9441e69a45576/ipython-9.11.0.tar.gz", hash = "sha256:2a94bc4406b22ecc7e4cb95b98450f3ea493a76bec8896cda11b78d7752a6667", size = 4427354, upload-time = "2026-03-05T08:57:30.549Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/86/92/162cfaee4ccf370465c5af1ce36a9eacec1becb552f2033bb3584e6f640a/ipython-9.9.0-py3-none-any.whl", hash = "sha256:b457fe9165df2b84e8ec909a97abcf2ed88f565970efba16b1f7229c283d252b", size = 621431, upload-time = "2026-01-05T12:36:44.669Z" },
{ url = "https://files.pythonhosted.org/packages/b2/90/45c72becc57158facc6a6404f663b77bbcea2519ca57f760e2879ae1315d/ipython-9.11.0-py3-none-any.whl", hash = "sha256:6922d5bcf944c6e525a76a0a304451b60a2b6f875e86656d8bc2dfda5d710e19", size = 624222, upload-time = "2026-03-05T08:57:28.94Z" },
]
[[package]]
@@ -856,14 +853,14 @@ wheels = [
[[package]]
name = "joserfc"
version = "1.6.1"
version = "1.6.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c7/3d/82375487dcc2bcdf136a68e1a8543165feccbbc8833dfc451f87a5f83b81/joserfc-1.6.1.tar.gz", hash = "sha256:7759a14d732d93503317468c0dd258510c4f64b30759cf42e96016c97b38c4b7", size = 226277, upload-time = "2025-12-30T08:45:07.289Z" }
sdist = { url = "https://files.pythonhosted.org/packages/ce/90/b8cc8635c4ce2e5e8104bf26ef147f6e599478f6329107283cdc53aae97f/joserfc-1.6.3.tar.gz", hash = "sha256:c00c2830db969b836cba197e830e738dd9dda0955f1794e55d3c636f17f5c9a6", size = 229090, upload-time = "2026-02-25T15:33:38.167Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/01/9674cc6d478406ae61d910cb16ca8b5699a8a9e6a2019987ebe5a5957d1d/joserfc-1.6.1-py3-none-any.whl", hash = "sha256:74d158c9d56be54c710cdcb2a0741372254b682ad2101a0f72e5bd0e925695f0", size = 70349, upload-time = "2025-12-30T08:45:05.573Z" },
{ url = "https://files.pythonhosted.org/packages/12/4f/124b3301067b752f44f292f0b9a74e837dd75ff863ee39500a082fc4c733/joserfc-1.6.3-py3-none-any.whl", hash = "sha256:6beab3635358cbc565cb94fb4c53d0557e6d10a15b933e2134939351590bda9a", size = 70465, upload-time = "2026-02-25T15:33:36.997Z" },
]
[[package]]
@@ -1082,59 +1079,59 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "boto3", specifier = "==1.42.34" },
{ name = "boto3", specifier = "==1.42.68" },
{ name = "brotli", specifier = "==1.2.0" },
{ name = "celery", extras = ["redis"], specifier = "==5.6.2" },
{ name = "django", specifier = "==5.2.11" },
{ name = "django-celery-beat", specifier = "==2.8.1" },
{ name = "django", specifier = "==6.0.3" },
{ name = "django-celery-beat", specifier = "==2.9.0" },
{ name = "django-celery-results", specifier = "==2.6.0" },
{ name = "django-configurations", specifier = "==2.5.1" },
{ name = "django-cors-headers", specifier = "==4.9.0" },
{ name = "django-countries", specifier = "==8.2.0" },
{ name = "django-extensions", specifier = "==4.1" },
{ name = "django-lasuite", specifier = "==0.0.23" },
{ name = "django-lasuite", specifier = "==0.0.25" },
{ name = "django-oauth-toolkit", specifier = "==3.2.0" },
{ name = "django-parler", specifier = "==2.3" },
{ name = "django-redis", specifier = "==6.0.0" },
{ name = "django-storages", specifier = "==1.14.6" },
{ name = "django-timezone-field", specifier = ">=5.1" },
{ name = "django-treebeard", specifier = "==4.8.0" },
{ name = "django-treebeard", specifier = "==5.0.5" },
{ name = "django-zxcvbn-password-validator", specifier = "==1.5.3" },
{ name = "djangorestframework", specifier = "==3.16.1" },
{ name = "dockerflow", specifier = "==2024.4.2" },
{ name = "dockerflow", specifier = "==2026.3.4" },
{ name = "drf-spectacular", specifier = "==0.29.0" },
{ name = "drf-spectacular", extras = ["sidecar"], specifier = "==0.29.0" },
{ name = "drf-spectacular-sidecar", marker = "extra == 'dev'", specifier = "==2026.1.1" },
{ name = "drf-spectacular-sidecar", marker = "extra == 'dev'", specifier = "==2026.3.1" },
{ name = "easy-thumbnails", specifier = "==2.10.1" },
{ name = "factory-boy", specifier = "==3.3.3" },
{ name = "flower", specifier = "==2.0.1" },
{ name = "freezegun", marker = "extra == 'dev'", specifier = "==1.5.5" },
{ name = "gunicorn", specifier = "==24.1.1" },
{ name = "gunicorn", specifier = "==25.1.0" },
{ name = "ipdb", marker = "extra == 'dev'", specifier = "==0.13.13" },
{ name = "ipython", marker = "extra == 'dev'", specifier = "==9.9.0" },
{ name = "ipython", marker = "extra == 'dev'", specifier = "==9.11.0" },
{ name = "jaraco-context", specifier = ">=6.1.0" },
{ name = "joserfc", specifier = "==1.6.1" },
{ name = "joserfc", specifier = "==1.6.3" },
{ name = "jq", marker = "extra == 'dev'", specifier = "==1.11.0" },
{ name = "jsonschema", specifier = "==4.26.0" },
{ name = "mozilla-django-oidc", specifier = "==5.0.2" },
{ name = "nested-multipart-parser", specifier = "==1.6.0" },
{ name = "psycopg", extras = ["binary"], specifier = "==3.3.2" },
{ name = "pyfakefs", marker = "extra == 'dev'", specifier = "==6.0.0" },
{ name = "pyjwt", specifier = "==2.10.1" },
{ name = "pylint", marker = "extra == 'dev'", specifier = "==4.0.4" },
{ name = "psycopg", extras = ["binary"], specifier = "==3.3.3" },
{ name = "pyfakefs", marker = "extra == 'dev'", specifier = "==6.1.5" },
{ name = "pyjwt", specifier = "==2.12.1" },
{ name = "pylint", marker = "extra == 'dev'", specifier = "==4.0.5" },
{ name = "pylint-django", marker = "extra == 'dev'", specifier = "==2.7.0" },
{ name = "pytest", marker = "extra == 'dev'", specifier = "==9.0.2" },
{ name = "pytest-cov", marker = "extra == 'dev'", specifier = "==7.0.0" },
{ name = "pytest-django", marker = "extra == 'dev'", specifier = "==4.11.1" },
{ name = "pytest-django", marker = "extra == 'dev'", specifier = "==4.12.0" },
{ name = "pytest-icdiff", marker = "extra == 'dev'", specifier = "==0.9" },
{ name = "pytest-xdist", marker = "extra == 'dev'", specifier = "==3.8.0" },
{ name = "redis", specifier = "<=7.1.0" },
{ name = "redis", specifier = "<=7.3.0" },
{ name = "requests", specifier = "==2.32.5" },
{ name = "responses", marker = "extra == 'dev'", specifier = "==0.25.8" },
{ name = "ruff", marker = "extra == 'dev'", specifier = "==0.14.14" },
{ name = "sentry-sdk", extras = ["django"], specifier = "==2.50.0" },
{ name = "responses", marker = "extra == 'dev'", specifier = "==0.26.0" },
{ name = "ruff", marker = "extra == 'dev'", specifier = "==0.15.6" },
{ name = "sentry-sdk", extras = ["django"], specifier = "==2.54.0" },
{ name = "types-requests", marker = "extra == 'dev'", specifier = "==2.32.4.20260107" },
{ name = "whitenoise", specifier = "==6.11.0" },
{ name = "whitenoise", specifier = "==6.12.0" },
]
provides-extras = ["dev"]
@@ -1233,14 +1230,14 @@ wheels = [
[[package]]
name = "psycopg"
version = "3.3.2"
version = "3.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e0/1a/7d9ef4fdc13ef7f15b934c393edc97a35c281bb7d3c3329fbfcbe915a7c2/psycopg-3.3.2.tar.gz", hash = "sha256:707a67975ee214d200511177a6a80e56e654754c9afca06a7194ea6bbfde9ca7", size = 165630, upload-time = "2025-12-06T17:34:53.899Z" }
sdist = { url = "https://files.pythonhosted.org/packages/d3/b6/379d0a960f8f435ec78720462fd94c4863e7a31237cf81bf76d0af5883bf/psycopg-3.3.3.tar.gz", hash = "sha256:5e9a47458b3c1583326513b2556a2a9473a1001a56c9efe9e587245b43148dd9", size = 165624, upload-time = "2026-02-18T16:52:16.546Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8c/51/2779ccdf9305981a06b21a6b27e8547c948d85c41c76ff434192784a4c93/psycopg-3.3.2-py3-none-any.whl", hash = "sha256:3e94bc5f4690247d734599af56e51bae8e0db8e4311ea413f801fef82b14a99b", size = 212774, upload-time = "2025-12-06T17:31:41.414Z" },
{ url = "https://files.pythonhosted.org/packages/c8/5b/181e2e3becb7672b502f0ed7f16ed7352aca7c109cfb94cf3878a9186db9/psycopg-3.3.3-py3-none-any.whl", hash = "sha256:f96525a72bcfade6584ab17e89de415ff360748c766f0106959144dcbb38c698", size = 212768, upload-time = "2026-02-18T16:46:27.365Z" },
]
[package.optional-dependencies]
@@ -1250,20 +1247,20 @@ binary = [
[[package]]
name = "psycopg-binary"
version = "3.3.2"
version = "3.3.3"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bd/75/ad18c0b97b852aba286d06befb398cc6d383e9dfd0a518369af275a5a526/psycopg_binary-3.3.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9ca24062cd9b2270e4d77576042e9cc2b1d543f09da5aba1f1a3d016cea28390", size = 4596371, upload-time = "2025-12-06T17:34:18.007Z" },
{ url = "https://files.pythonhosted.org/packages/5a/79/91649d94c8d89f84af5da7c9d474bfba35b08eb8f492ca3422b08f0a6427/psycopg_binary-3.3.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c749770da0947bc972e512f35366dd4950c0e34afad89e60b9787a37e97cb443", size = 4675139, upload-time = "2025-12-06T17:34:21.374Z" },
{ url = "https://files.pythonhosted.org/packages/56/ac/b26e004880f054549ec9396594e1ffe435810b0673e428e619ed722e4244/psycopg_binary-3.3.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:03b7cd73fb8c45d272a34ae7249713e32492891492681e3cf11dff9531cf37e9", size = 5456120, upload-time = "2025-12-06T17:34:25.102Z" },
{ url = "https://files.pythonhosted.org/packages/4b/8d/410681dccd6f2999fb115cc248521ec50dd2b0aba66ae8de7e81efdebbee/psycopg_binary-3.3.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:43b130e3b6edcb5ee856c7167ccb8561b473308c870ed83978ae478613764f1c", size = 5133484, upload-time = "2025-12-06T17:34:28.933Z" },
{ url = "https://files.pythonhosted.org/packages/66/30/ebbab99ea2cfa099d7b11b742ce13415d44f800555bfa4ad2911dc645b71/psycopg_binary-3.3.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c1feba5a8c617922321aef945865334e468337b8fc5c73074f5e63143013b5a", size = 6731818, upload-time = "2025-12-06T17:34:33.094Z" },
{ url = "https://files.pythonhosted.org/packages/70/02/d260646253b7ad805d60e0de47f9b811d6544078452579466a098598b6f4/psycopg_binary-3.3.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cabb2a554d9a0a6bf84037d86ca91782f087dfff2a61298d0b00c19c0bc43f6d", size = 4983859, upload-time = "2025-12-06T17:34:36.457Z" },
{ url = "https://files.pythonhosted.org/packages/72/8d/e778d7bad1a7910aa36281f092bd85c5702f508fd9bb0ea2020ffbb6585c/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74bc306c4b4df35b09bc8cecf806b271e1c5d708f7900145e4e54a2e5dedfed0", size = 4516388, upload-time = "2025-12-06T17:34:40.129Z" },
{ url = "https://files.pythonhosted.org/packages/bd/f1/64e82098722e2ab3521797584caf515284be09c1e08a872551b6edbb0074/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d79b0093f0fbf7a962d6a46ae292dc056c65d16a8ee9361f3cfbafd4c197ab14", size = 4192382, upload-time = "2025-12-06T17:34:43.279Z" },
{ url = "https://files.pythonhosted.org/packages/fa/d0/c20f4e668e89494972e551c31be2a0016e3f50d552d7ae9ac07086407599/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1586e220be05547c77afc326741dd41cc7fba38a81f9931f616ae98865439678", size = 3928660, upload-time = "2025-12-06T17:34:46.757Z" },
{ url = "https://files.pythonhosted.org/packages/0f/e1/99746c171de22539fd5eb1c9ca21dc805b54cfae502d7451d237d1dbc349/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:458696a5fa5dad5b6fb5d5862c22454434ce4fe1cf66ca6c0de5f904cbc1ae3e", size = 4239169, upload-time = "2025-12-06T17:34:49.751Z" },
{ url = "https://files.pythonhosted.org/packages/72/f7/212343c1c9cfac35fd943c527af85e9091d633176e2a407a0797856ff7b9/psycopg_binary-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:04bb2de4ba69d6f8395b446ede795e8884c040ec71d01dd07ac2b2d18d4153d1", size = 3642122, upload-time = "2025-12-06T17:34:52.506Z" },
{ url = "https://files.pythonhosted.org/packages/a2/71/7a57e5b12275fe7e7d84d54113f0226080423a869118419c9106c083a21c/psycopg_binary-3.3.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:497852c5eaf1f0c2d88ab74a64a8097c099deac0c71de1cbcf18659a8a04a4b2", size = 4607368, upload-time = "2026-02-18T16:51:19.295Z" },
{ url = "https://files.pythonhosted.org/packages/c7/04/cb834f120f2b2c10d4003515ef9ca9d688115b9431735e3936ae48549af8/psycopg_binary-3.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:258d1ea53464d29768bf25930f43291949f4c7becc706f6e220c515a63a24edd", size = 4687047, upload-time = "2026-02-18T16:51:23.84Z" },
{ url = "https://files.pythonhosted.org/packages/40/e9/47a69692d3da9704468041aa5ed3ad6fc7f6bb1a5ae788d261a26bbca6c7/psycopg_binary-3.3.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:111c59897a452196116db12e7f608da472fbff000693a21040e35fc978b23430", size = 5487096, upload-time = "2026-02-18T16:51:29.645Z" },
{ url = "https://files.pythonhosted.org/packages/0b/b6/0e0dd6a2f802864a4ae3dbadf4ec620f05e3904c7842b326aafc43e5f464/psycopg_binary-3.3.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:17bb6600e2455993946385249a3c3d0af52cd70c1c1cdbf712e9d696d0b0bf1b", size = 5168720, upload-time = "2026-02-18T16:51:36.499Z" },
{ url = "https://files.pythonhosted.org/packages/6f/0d/977af38ac19a6b55d22dff508bd743fd7c1901e1b73657e7937c7cccb0a3/psycopg_binary-3.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:642050398583d61c9856210568eb09a8e4f2fe8224bf3be21b67a370e677eead", size = 6762076, upload-time = "2026-02-18T16:51:43.167Z" },
{ url = "https://files.pythonhosted.org/packages/34/40/912a39d48322cf86895c0eaf2d5b95cb899402443faefd4b09abbba6b6e1/psycopg_binary-3.3.3-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:533efe6dc3a7cba5e2a84e38970786bb966306863e45f3db152007e9f48638a6", size = 4997623, upload-time = "2026-02-18T16:51:47.707Z" },
{ url = "https://files.pythonhosted.org/packages/98/0c/c14d0e259c65dc7be854d926993f151077887391d5a081118907a9d89603/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5958dbf28b77ce2033482f6cb9ef04d43f5d8f4b7636e6963d5626f000efb23e", size = 4532096, upload-time = "2026-02-18T16:51:51.421Z" },
{ url = "https://files.pythonhosted.org/packages/39/21/8b7c50a194cfca6ea0fd4d1f276158307785775426e90700ab2eba5cd623/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a6af77b6626ce92b5817bf294b4d45ec1a6161dba80fc2d82cdffdd6814fd023", size = 4208884, upload-time = "2026-02-18T16:51:57.336Z" },
{ url = "https://files.pythonhosted.org/packages/c7/2c/a4981bf42cf30ebba0424971d7ce70a222ae9b82594c42fc3f2105d7b525/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:47f06fcbe8542b4d96d7392c476a74ada521c5aebdb41c3c0155f6595fc14c8d", size = 3944542, upload-time = "2026-02-18T16:52:04.266Z" },
{ url = "https://files.pythonhosted.org/packages/60/e9/b7c29b56aa0b85a4e0c4d89db691c1ceef08f46a356369144430c155a2f5/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e7800e6c6b5dc4b0ca7cc7370f770f53ac83886b76afda0848065a674231e856", size = 4254339, upload-time = "2026-02-18T16:52:10.444Z" },
{ url = "https://files.pythonhosted.org/packages/98/5a/291d89f44d3820fffb7a04ebc8f3ef5dda4f542f44a5daea0c55a84abf45/psycopg_binary-3.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:165f22ab5a9513a3d7425ffb7fcc7955ed8ccaeef6d37e369d6cc1dff1582383", size = 3652796, upload-time = "2026-02-18T16:52:14.02Z" },
]
[[package]]
@@ -1295,11 +1292,11 @@ wheels = [
[[package]]
name = "pyfakefs"
version = "6.0.0"
version = "6.1.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8b/56/70596e4ad6eee8bba7f80c1017cd68da4d190a6e7a8fc1d52ea671c6baad/pyfakefs-6.0.0.tar.gz", hash = "sha256:059fd0b2174bfeed499ecb0a59bccff557f267cc6d885afc0e5b76e78b6d50da", size = 224391, upload-time = "2025-12-21T19:18:13.79Z" }
sdist = { url = "https://files.pythonhosted.org/packages/61/87/fe3f1fbbf18480b57e6b195d4d416e01d8e55bf88743982ccb3a8845c2a2/pyfakefs-6.1.5.tar.gz", hash = "sha256:d70fb1c622a1d5c729462d31bb58eead99c1830986bc049bf8975c480a49d19b", size = 226814, upload-time = "2026-03-15T19:05:18.025Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0d/3d/515b79c9da62acc6ed1fc51bec878b61ac9c2475d9300aa7f5b4c94d8387/pyfakefs-6.0.0-py3-none-any.whl", hash = "sha256:44ef5ab0294e7e623b8e56b4f2d8c9468b737d6e8641053063c43033d2c8c180", size = 237909, upload-time = "2025-12-21T19:18:11.999Z" },
{ url = "https://files.pythonhosted.org/packages/e0/d8/a60fb94ab2b5a9bf0e38d6e93667904d4e844d8b7e9cfb5ef0304d68da6b/pyfakefs-6.1.5-py3-none-any.whl", hash = "sha256:2c482f36bad7d1f7516d58d17818fb0dbe397a3372ec5274869386a9b7cd4883", size = 239885, upload-time = "2026-03-15T19:05:16.488Z" },
]
[[package]]
@@ -1313,16 +1310,16 @@ wheels = [
[[package]]
name = "pyjwt"
version = "2.10.1"
version = "2.12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" },
{ url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" },
]
[[package]]
name = "pylint"
version = "4.0.4"
version = "4.0.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "astroid" },
@@ -1333,9 +1330,9 @@ dependencies = [
{ name = "platformdirs" },
{ name = "tomlkit" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5a/d2/b081da1a8930d00e3fc06352a1d449aaf815d4982319fab5d8cdb2e9ab35/pylint-4.0.4.tar.gz", hash = "sha256:d9b71674e19b1c36d79265b5887bf8e55278cbe236c9e95d22dc82cf044fdbd2", size = 1571735, upload-time = "2025-11-30T13:29:04.315Z" }
sdist = { url = "https://files.pythonhosted.org/packages/e4/b6/74d9a8a68b8067efce8d07707fe6a236324ee1e7808d2eb3646ec8517c7d/pylint-4.0.5.tar.gz", hash = "sha256:8cd6a618df75deb013bd7eb98327a95f02a6fb839205a6bbf5456ef96afb317c", size = 1572474, upload-time = "2026-02-20T09:07:33.621Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a6/92/d40f5d937517cc489ad848fc4414ecccc7592e4686b9071e09e64f5e378e/pylint-4.0.4-py3-none-any.whl", hash = "sha256:63e06a37d5922555ee2c20963eb42559918c20bd2b21244e4ef426e7c43b92e0", size = 536425, upload-time = "2025-11-30T13:29:02.53Z" },
{ url = "https://files.pythonhosted.org/packages/d5/6f/9ac2548e290764781f9e7e2aaf0685b086379dabfb29ca38536985471eaf/pylint-4.0.5-py3-none-any.whl", hash = "sha256:00f51c9b14a3b3ae08cff6b2cdd43f28165c78b165b628692e428fb1f8dc2cf2", size = 536694, upload-time = "2026-02-20T09:07:31.028Z" },
]
[[package]]
@@ -1394,14 +1391,14 @@ wheels = [
[[package]]
name = "pytest-django"
version = "4.11.1"
version = "4.12.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/fb/55d580352db26eb3d59ad50c64321ddfe228d3d8ac107db05387a2fadf3a/pytest_django-4.11.1.tar.gz", hash = "sha256:a949141a1ee103cb0e7a20f1451d355f83f5e4a5d07bdd4dcfdd1fd0ff227991", size = 86202, upload-time = "2025-04-03T18:56:09.338Z" }
sdist = { url = "https://files.pythonhosted.org/packages/13/2b/db9a193df89e5660137f5428063bcc2ced7ad790003b26974adf5c5ceb3b/pytest_django-4.12.0.tar.gz", hash = "sha256:df94ec819a83c8979c8f6de13d9cdfbe76e8c21d39473cfe2b40c9fc9be3c758", size = 91156, upload-time = "2026-02-14T18:40:49.235Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/be/ac/bd0608d229ec808e51a21044f3f2f27b9a37e7a0ebaca7247882e67876af/pytest_django-4.11.1-py3-none-any.whl", hash = "sha256:1b63773f648aa3d8541000c26929c1ea63934be1cfa674c76436966d73fe6a10", size = 25281, upload-time = "2025-04-03T18:56:07.678Z" },
{ url = "https://files.pythonhosted.org/packages/83/a5/41d091f697c09609e7ef1d5d61925494e0454ebf51de7de05f0f0a728f1d/pytest_django-4.12.0-py3-none-any.whl", hash = "sha256:3ff300c49f8350ba2953b90297d23bf5f589db69545f56f1ec5f8cff5da83e85", size = 26123, upload-time = "2026-02-14T18:40:47.381Z" },
]
[[package]]
@@ -1538,16 +1535,16 @@ wheels = [
[[package]]
name = "responses"
version = "0.25.8"
version = "0.26.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyyaml" },
{ name = "requests" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0e/95/89c054ad70bfef6da605338b009b2e283485835351a9935c7bfbfaca7ffc/responses-0.25.8.tar.gz", hash = "sha256:9374d047a575c8f781b94454db5cab590b6029505f488d12899ddb10a4af1cf4", size = 79320, upload-time = "2025-08-08T19:01:46.709Z" }
sdist = { url = "https://files.pythonhosted.org/packages/9f/b4/b7e040379838cc71bf5aabdb26998dfbe5ee73904c92c1c161faf5de8866/responses-0.26.0.tar.gz", hash = "sha256:c7f6923e6343ef3682816ba421c006626777893cb0d5e1434f674b649bac9eb4", size = 81303, upload-time = "2026-02-19T14:38:05.574Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1c/4c/cc276ce57e572c102d9542d383b2cfd551276581dc60004cb94fe8774c11/responses-0.25.8-py3-none-any.whl", hash = "sha256:0c710af92def29c8352ceadff0c3fe340ace27cf5af1bbe46fb71275bcd2831c", size = 34769, upload-time = "2025-08-08T19:01:45.018Z" },
{ url = "https://files.pythonhosted.org/packages/ce/04/7f73d05b556da048923e31a0cc878f03be7c5425ed1f268082255c75d872/responses-0.26.0-py3-none-any.whl", hash = "sha256:03ec4409088cd5c66b71ecbbbd27fe2c58ddfad801c66203457b3e6a04868c37", size = 35099, upload-time = "2026-02-19T14:38:03.847Z" },
]
[[package]]
@@ -1589,28 +1586,27 @@ wheels = [
[[package]]
name = "ruff"
version = "0.14.14"
version = "0.15.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" }
sdist = { url = "https://files.pythonhosted.org/packages/51/df/f8629c19c5318601d3121e230f74cbee7a3732339c52b21daa2b82ef9c7d/ruff-0.15.6.tar.gz", hash = "sha256:8394c7bb153a4e3811a4ecdacd4a8e6a4fa8097028119160dffecdcdf9b56ae4", size = 4597916, upload-time = "2026-03-12T23:05:47.51Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" },
{ url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" },
{ url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" },
{ url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" },
{ url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" },
{ url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" },
{ url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" },
{ url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" },
{ url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" },
{ url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" },
{ url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" },
{ url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" },
{ url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" },
{ url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" },
{ url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" },
{ url = "https://files.pythonhosted.org/packages/2c/aa/9f89c719c467dfaf8ad799b9bae0df494513fb21d31a6059cb5870e57e74/ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", size = 10502442, upload-time = "2026-01-22T22:30:38.93Z" },
{ url = "https://files.pythonhosted.org/packages/87/44/90fa543014c45560cae1fffc63ea059fb3575ee6e1cb654562197e5d16fb/ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", size = 11630486, upload-time = "2026-01-22T22:30:10.852Z" },
{ url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" },
{ url = "https://files.pythonhosted.org/packages/9e/2f/4e03a7e5ce99b517e98d3b4951f411de2b0fa8348d39cf446671adcce9a2/ruff-0.15.6-py3-none-linux_armv6l.whl", hash = "sha256:7c98c3b16407b2cf3d0f2b80c80187384bc92c6774d85fefa913ecd941256fff", size = 10508953, upload-time = "2026-03-12T23:05:17.246Z" },
{ url = "https://files.pythonhosted.org/packages/70/60/55bcdc3e9f80bcf39edf0cd272da6fa511a3d94d5a0dd9e0adf76ceebdb4/ruff-0.15.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee7dcfaad8b282a284df4aa6ddc2741b3f4a18b0555d626805555a820ea181c3", size = 10942257, upload-time = "2026-03-12T23:05:23.076Z" },
{ url = "https://files.pythonhosted.org/packages/e7/f9/005c29bd1726c0f492bfa215e95154cf480574140cb5f867c797c18c790b/ruff-0.15.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3bd9967851a25f038fc8b9ae88a7fbd1b609f30349231dffaa37b6804923c4bb", size = 10322683, upload-time = "2026-03-12T23:05:33.738Z" },
{ url = "https://files.pythonhosted.org/packages/5f/74/2f861f5fd7cbb2146bddb5501450300ce41562da36d21868c69b7a828169/ruff-0.15.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13f4594b04e42cd24a41da653886b04d2ff87adbf57497ed4f728b0e8a4866f8", size = 10660986, upload-time = "2026-03-12T23:05:53.245Z" },
{ url = "https://files.pythonhosted.org/packages/c1/a1/309f2364a424eccb763cdafc49df843c282609f47fe53aa83f38272389e0/ruff-0.15.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2ed8aea2f3fe57886d3f00ea5b8aae5bf68d5e195f487f037a955ff9fbaac9e", size = 10332177, upload-time = "2026-03-12T23:05:56.145Z" },
{ url = "https://files.pythonhosted.org/packages/30/41/7ebf1d32658b4bab20f8ac80972fb19cd4e2c6b78552be263a680edc55ac/ruff-0.15.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70789d3e7830b848b548aae96766431c0dc01a6c78c13381f423bf7076c66d15", size = 11170783, upload-time = "2026-03-12T23:06:01.742Z" },
{ url = "https://files.pythonhosted.org/packages/76/be/6d488f6adca047df82cd62c304638bcb00821c36bd4881cfca221561fdfc/ruff-0.15.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:542aaf1de3154cea088ced5a819ce872611256ffe2498e750bbae5247a8114e9", size = 12044201, upload-time = "2026-03-12T23:05:28.697Z" },
{ url = "https://files.pythonhosted.org/packages/71/68/e6f125df4af7e6d0b498f8d373274794bc5156b324e8ab4bf5c1b4fc0ec7/ruff-0.15.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c22e6f02c16cfac3888aa636e9eba857254d15bbacc9906c9689fdecb1953ab", size = 11421561, upload-time = "2026-03-12T23:05:31.236Z" },
{ url = "https://files.pythonhosted.org/packages/f1/9f/f85ef5fd01a52e0b472b26dc1b4bd228b8f6f0435975442ffa4741278703/ruff-0.15.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98893c4c0aadc8e448cfa315bd0cc343a5323d740fe5f28ef8a3f9e21b381f7e", size = 11310928, upload-time = "2026-03-12T23:05:45.288Z" },
{ url = "https://files.pythonhosted.org/packages/8c/26/b75f8c421f5654304b89471ed384ae8c7f42b4dff58fa6ce1626d7f2b59a/ruff-0.15.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:70d263770d234912374493e8cc1e7385c5d49376e41dfa51c5c3453169dc581c", size = 11235186, upload-time = "2026-03-12T23:05:50.677Z" },
{ url = "https://files.pythonhosted.org/packages/fc/d4/d5a6d065962ff7a68a86c9b4f5500f7d101a0792078de636526c0edd40da/ruff-0.15.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:55a1ad63c5a6e54b1f21b7514dfadc0c7fb40093fa22e95143cf3f64ebdcd512", size = 10635231, upload-time = "2026-03-12T23:05:37.044Z" },
{ url = "https://files.pythonhosted.org/packages/d6/56/7c3acf3d50910375349016cf33de24be021532042afbed87942858992491/ruff-0.15.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8dc473ba093c5ec238bb1e7429ee676dca24643c471e11fbaa8a857925b061c0", size = 10340357, upload-time = "2026-03-12T23:06:04.748Z" },
{ url = "https://files.pythonhosted.org/packages/06/54/6faa39e9c1033ff6a3b6e76b5df536931cd30caf64988e112bbf91ef5ce5/ruff-0.15.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:85b042377c2a5561131767974617006f99f7e13c63c111b998f29fc1e58a4cfb", size = 10860583, upload-time = "2026-03-12T23:05:58.978Z" },
{ url = "https://files.pythonhosted.org/packages/cb/1e/509a201b843b4dfb0b32acdedf68d951d3377988cae43949ba4c4133a96a/ruff-0.15.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cef49e30bc5a86a6a92098a7fbf6e467a234d90b63305d6f3ec01225a9d092e0", size = 11410976, upload-time = "2026-03-12T23:05:39.955Z" },
{ url = "https://files.pythonhosted.org/packages/6c/25/3fc9114abf979a41673ce877c08016f8e660ad6cf508c3957f537d2e9fa9/ruff-0.15.6-py3-none-win32.whl", hash = "sha256:bbf67d39832404812a2d23020dda68fee7f18ce15654e96fb1d3ad21a5fe436c", size = 10616872, upload-time = "2026-03-12T23:05:42.451Z" },
{ url = "https://files.pythonhosted.org/packages/89/7a/09ece68445ceac348df06e08bf75db72d0e8427765b96c9c0ffabc1be1d9/ruff-0.15.6-py3-none-win_amd64.whl", hash = "sha256:aee25bc84c2f1007ecb5037dff75cef00414fdf17c23f07dc13e577883dca406", size = 11787271, upload-time = "2026-03-12T23:05:20.168Z" },
{ url = "https://files.pythonhosted.org/packages/7f/d0/578c47dd68152ddddddf31cd7fc67dc30b7cdf639a86275fda821b0d9d98/ruff-0.15.6-py3-none-win_arm64.whl", hash = "sha256:c34de3dd0b0ba203be50ae70f5910b17188556630e2178fd7d79fc030eb0d837", size = 11060497, upload-time = "2026-03-12T23:05:25.968Z" },
]
[[package]]
@@ -1627,15 +1623,15 @@ wheels = [
[[package]]
name = "sentry-sdk"
version = "2.50.0"
version = "2.54.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/15/8a/3c4f53d32c21012e9870913544e56bfa9e931aede080779a0f177513f534/sentry_sdk-2.50.0.tar.gz", hash = "sha256:873437a989ee1b8b25579847bae8384515bf18cfed231b06c591b735c1781fe3", size = 401233, upload-time = "2026-01-20T12:53:16.244Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c8/e9/2e3a46c304e7fa21eaa70612f60354e32699c7102eb961f67448e222ad7c/sentry_sdk-2.54.0.tar.gz", hash = "sha256:2620c2575128d009b11b20f7feb81e4e4e8ae08ec1d36cbc845705060b45cc1b", size = 413813, upload-time = "2026-03-02T15:12:41.355Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4e/5b/cbc2bb9569f03c8e15d928357e7e6179e5cfab45544a3bbac8aec4caf9be/sentry_sdk-2.50.0-py2.py3-none-any.whl", hash = "sha256:0ef0ed7168657ceb5a0be081f4102d92042a125462d1d1a29277992e344e749e", size = 424961, upload-time = "2026-01-20T12:53:14.826Z" },
{ url = "https://files.pythonhosted.org/packages/53/39/be412cc86bc6247b8f69e9383d7950711bd86f8d0a4a4b0fe8fad685bc21/sentry_sdk-2.54.0-py2.py3-none-any.whl", hash = "sha256:fd74e0e281dcda63afff095d23ebcd6e97006102cdc8e78a29f19ecdf796a0de", size = 439198, upload-time = "2026-03-02T15:12:39.546Z" },
]
[package.optional-dependencies]
@@ -1686,21 +1682,19 @@ wheels = [
[[package]]
name = "tornado"
version = "6.5.4"
version = "6.5.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/37/1d/0a336abf618272d53f62ebe274f712e213f5a03c0b2339575430b8362ef2/tornado-6.5.4.tar.gz", hash = "sha256:a22fa9047405d03260b483980635f0b041989d8bcc9a313f8fe18b411d84b1d7", size = 513632, upload-time = "2025-12-15T19:21:03.836Z" }
sdist = { url = "https://files.pythonhosted.org/packages/f8/f1/3173dfa4a18db4a9b03e5d55325559dab51ee653763bb8745a75af491286/tornado-6.5.5.tar.gz", hash = "sha256:192b8f3ea91bd7f1f50c06955416ed76c6b72f96779b962f07f911b91e8d30e9", size = 516006, upload-time = "2026-03-10T21:31:02.067Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ab/a9/e94a9d5224107d7ce3cc1fab8d5dc97f5ea351ccc6322ee4fb661da94e35/tornado-6.5.4-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d6241c1a16b1c9e4cc28148b1cda97dd1c6cb4fb7068ac1bedc610768dff0ba9", size = 443909, upload-time = "2025-12-15T19:20:48.382Z" },
{ url = "https://files.pythonhosted.org/packages/db/7e/f7b8d8c4453f305a51f80dbb49014257bb7d28ccb4bbb8dd328ea995ecad/tornado-6.5.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2d50f63dda1d2cac3ae1fa23d254e16b5e38153758470e9956cbc3d813d40843", size = 442163, upload-time = "2025-12-15T19:20:49.791Z" },
{ url = "https://files.pythonhosted.org/packages/ba/b5/206f82d51e1bfa940ba366a8d2f83904b15942c45a78dd978b599870ab44/tornado-6.5.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1cf66105dc6acb5af613c054955b8137e34a03698aa53272dbda4afe252be17", size = 445746, upload-time = "2025-12-15T19:20:51.491Z" },
{ url = "https://files.pythonhosted.org/packages/8e/9d/1a3338e0bd30ada6ad4356c13a0a6c35fbc859063fa7eddb309183364ac1/tornado-6.5.4-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50ff0a58b0dc97939d29da29cd624da010e7f804746621c78d14b80238669335", size = 445083, upload-time = "2025-12-15T19:20:52.778Z" },
{ url = "https://files.pythonhosted.org/packages/50/d4/e51d52047e7eb9a582da59f32125d17c0482d065afd5d3bc435ff2120dc5/tornado-6.5.4-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5fb5e04efa54cf0baabdd10061eb4148e0be137166146fff835745f59ab9f7f", size = 445315, upload-time = "2025-12-15T19:20:53.996Z" },
{ url = "https://files.pythonhosted.org/packages/27/07/2273972f69ca63dbc139694a3fc4684edec3ea3f9efabf77ed32483b875c/tornado-6.5.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9c86b1643b33a4cd415f8d0fe53045f913bf07b4a3ef646b735a6a86047dda84", size = 446003, upload-time = "2025-12-15T19:20:56.101Z" },
{ url = "https://files.pythonhosted.org/packages/d1/83/41c52e47502bf7260044413b6770d1a48dda2f0246f95ee1384a3cd9c44a/tornado-6.5.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:6eb82872335a53dd063a4f10917b3efd28270b56a33db69009606a0312660a6f", size = 445412, upload-time = "2025-12-15T19:20:57.398Z" },
{ url = "https://files.pythonhosted.org/packages/10/c7/bc96917f06cbee182d44735d4ecde9c432e25b84f4c2086143013e7b9e52/tornado-6.5.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6076d5dda368c9328ff41ab5d9dd3608e695e8225d1cd0fd1e006f05da3635a8", size = 445392, upload-time = "2025-12-15T19:20:58.692Z" },
{ url = "https://files.pythonhosted.org/packages/0c/1a/d7592328d037d36f2d2462f4bc1fbb383eec9278bc786c1b111cbbd44cfa/tornado-6.5.4-cp39-abi3-win32.whl", hash = "sha256:1768110f2411d5cd281bac0a090f707223ce77fd110424361092859e089b38d1", size = 446481, upload-time = "2025-12-15T19:21:00.008Z" },
{ url = "https://files.pythonhosted.org/packages/d6/6d/c69be695a0a64fd37a97db12355a035a6d90f79067a3cf936ec2b1dc38cd/tornado-6.5.4-cp39-abi3-win_amd64.whl", hash = "sha256:fa07d31e0cd85c60713f2b995da613588aa03e1303d75705dca6af8babc18ddc", size = 446886, upload-time = "2025-12-15T19:21:01.287Z" },
{ url = "https://files.pythonhosted.org/packages/50/49/8dc3fd90902f70084bd2cd059d576ddb4f8bb44c2c7c0e33a11422acb17e/tornado-6.5.4-cp39-abi3-win_arm64.whl", hash = "sha256:053e6e16701eb6cbe641f308f4c1a9541f91b6261991160391bfc342e8a551a1", size = 445910, upload-time = "2025-12-15T19:21:02.571Z" },
{ url = "https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:487dc9cc380e29f58c7ab88f9e27cdeef04b2140862e5076a66fb6bb68bb1bfa", size = 445983, upload-time = "2026-03-10T21:30:44.28Z" },
{ url = "https://files.pythonhosted.org/packages/ab/5e/7625b76cd10f98f1516c36ce0346de62061156352353ef2da44e5c21523c/tornado-6.5.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:65a7f1d46d4bb41df1ac99f5fcb685fb25c7e61613742d5108b010975a9a6521", size = 444246, upload-time = "2026-03-10T21:30:46.571Z" },
{ url = "https://files.pythonhosted.org/packages/b2/04/7b5705d5b3c0fab088f434f9c83edac1573830ca49ccf29fb83bf7178eec/tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5", size = 447229, upload-time = "2026-03-10T21:30:48.273Z" },
{ url = "https://files.pythonhosted.org/packages/34/01/74e034a30ef59afb4097ef8659515e96a39d910b712a89af76f5e4e1f93c/tornado-6.5.5-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:435319e9e340276428bbdb4e7fa732c2d399386d1de5686cb331ec8eee754f07", size = 448192, upload-time = "2026-03-10T21:30:51.22Z" },
{ url = "https://files.pythonhosted.org/packages/be/00/fe9e02c5a96429fce1a1d15a517f5d8444f9c412e0bb9eadfbe3b0fc55bf/tornado-6.5.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3f54aa540bdbfee7b9eb268ead60e7d199de5021facd276819c193c0fb28ea4e", size = 448039, upload-time = "2026-03-10T21:30:53.52Z" },
{ url = "https://files.pythonhosted.org/packages/82/9e/656ee4cec0398b1d18d0f1eb6372c41c6b889722641d84948351ae19556d/tornado-6.5.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36abed1754faeb80fbd6e64db2758091e1320f6bba74a4cf8c09cd18ccce8aca", size = 447445, upload-time = "2026-03-10T21:30:55.541Z" },
{ url = "https://files.pythonhosted.org/packages/5a/76/4921c00511f88af86a33de770d64141170f1cfd9c00311aea689949e274e/tornado-6.5.5-cp39-abi3-win32.whl", hash = "sha256:dd3eafaaeec1c7f2f8fdcd5f964e8907ad788fe8a5a32c4426fbbdda621223b7", size = 448582, upload-time = "2026-03-10T21:30:57.142Z" },
{ url = "https://files.pythonhosted.org/packages/2c/23/f6c6112a04d28eed765e374435fb1a9198f73e1ec4b4024184f21faeb1ad/tornado-6.5.5-cp39-abi3-win_amd64.whl", hash = "sha256:6443a794ba961a9f619b1ae926a2e900ac20c34483eea67be4ed8f1e58d3ef7b", size = 448990, upload-time = "2026-03-10T21:30:58.857Z" },
{ url = "https://files.pythonhosted.org/packages/b7/c8/876602cbc96469911f0939f703453c1157b0c826ecb05bdd32e023397d4e/tornado-6.5.5-cp39-abi3-win_arm64.whl", hash = "sha256:2c9a876e094109333f888539ddb2de4361743e5d21eece20688e3e351e4990a6", size = 448016, upload-time = "2026-03-10T21:31:00.43Z" },
]
[[package]]
@@ -1792,11 +1786,11 @@ wheels = [
[[package]]
name = "whitenoise"
version = "6.11.0"
version = "6.12.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/15/95/8c81ec6b6ebcbf8aca2de7603070ccf37dbb873b03f20708e0f7c1664bc6/whitenoise-6.11.0.tar.gz", hash = "sha256:0f5bfce6061ae6611cd9396a8231e088722e4fc67bc13a111be74c738d99375f", size = 26432, upload-time = "2025-09-18T09:16:10.995Z" }
sdist = { url = "https://files.pythonhosted.org/packages/cb/2a/55b3f3a4ec326cd077c1c3defeee656b9298372a69229134d930151acd01/whitenoise-6.12.0.tar.gz", hash = "sha256:f723ebb76a112e98816ff80fcea0a6c9b8ecde835f8ddda25df7a30a3c2db6ad", size = 26841, upload-time = "2026-02-27T00:05:42.028Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6c/e9/4366332f9295fe0647d7d3251ce18f5615fbcb12d02c79a26f8dba9221b3/whitenoise-6.11.0-py3-none-any.whl", hash = "sha256:b2aeb45950597236f53b5342b3121c5de69c8da0109362aee506ce88e022d258", size = 20197, upload-time = "2025-09-18T09:16:09.754Z" },
{ url = "https://files.pythonhosted.org/packages/db/eb/d5583a11486211f3ebd4b385545ae787f32363d453c19fffd81106c9c138/whitenoise-6.12.0-py3-none-any.whl", hash = "sha256:fc5e8c572e33ebf24795b47b6a7da8da3c00cff2349f5b04c02f28d0cc5a3cc2", size = 20302, upload-time = "2026-02-27T00:05:40.086Z" },
]
[[package]]
+862 -3
View File
@@ -1,5 +1,864 @@
import { cunninghamConfig } from '@gouvfr-lasuite/ui-kit';
import { getThemesFromGlobals } from '@gouvfr-lasuite/cunningham-tokens';
export default {
...cunninghamConfig,
export const commonTokenOverrides = {
components: {
favicon: {
'png-light': '/assets/favicon-light.png',
'png-dark': '/assets/favicon-dark.png',
},
modal: {
'width-small': '342px',
},
tooltip: {
padding: '4px 8px',
},
button: {
'medium-height': '40px',
'medium-text-height': '40px',
'border-radius': '4px',
'border-radius--active': '4px',
'border-radius--focus': '4px',
},
datagrid: {
'header--color': 'ref(contextuals.content.semantic.neutral.primary)',
'header--size': '12px',
'header--weight': '500',
'body--background-color-hover':
'ref(contextuals.background.semantic.neutral.tertiary)',
},
'forms-checkbox': {
'font-size': 'ref(globals.font.sizes.sm)',
},
badge: {
'font-size': 'ref(globals.font.sizes.xs)',
'border-radius': '12px',
'padding-inline': 'ref(globals.spacings.xs)',
'padding-block': 'ref(globals.spacings.2xs)',
accent: {
'background-color':
'ref(contextuals.background.semantic.brand.secondary)',
color: 'ref(contextuals.content.semantic.brand.secondary)',
},
neutral: {
'background-color':
'ref(contextuals.background.semantic.neutral.secondary)',
color: 'ref(contextuals.content.semantic.neutral.secondary)',
},
danger: {
'background-color':
'ref(contextuals.background.semantic.error.secondary)',
color: 'ref(contextuals.content.semantic.error.secondary)',
},
success: {
'background-color':
'ref(contextuals.background.semantic.success.secondary)',
color: 'ref(contextuals.content.semantic.success.secondary)',
},
warning: {
'background-color':
'ref(contextuals.background.semantic.warning.secondary)',
color: 'ref(contextuals.content.semantic.warning.secondary)',
},
info: {
'background-color':
'ref(contextuals.background.semantic.info.secondary)',
color: 'ref(contextuals.content.semantic.info.secondary)',
},
},
},
};
export const commonGlobals = {
components: {
'la-gaufre': false,
'home-proconnect': false,
logo: {
src: '',
alt: '',
widthHeader: '',
widthFooter: '',
},
},
font: {
sizes: {
xs: '0.75rem',
sm: '0.875rem',
md: '1rem',
lg: '1.125rem',
ml: '0.938rem',
xl: '1.25rem',
t: '0.6875rem',
s: '0.75rem',
h1: '2rem',
h2: '1.75rem',
h3: '1.5rem',
h4: '1.375rem',
h5: '1.25rem',
h6: '1.125rem',
'xl-alt': '5rem',
'lg-alt': '4.5rem',
'md-alt': '4rem',
'sm-alt': '3.5rem',
'xs-alt': '3rem',
},
weights: {
thin: 100,
extrabold: 800,
black: 900,
},
families: {
accent: 'Marianne, Inter, Roboto Flex Variable, sans-serif',
base: 'Marianne, Inter, Roboto Flex Variable, sans-serif',
},
},
spacings: {
'0': '0',
none: '0',
auto: 'auto',
bx: '2.2rem',
full: '100%',
'4xs': '0.125rem',
'3xs': '0.25rem',
'2xs': '0.375rem',
xs: '0.5rem',
sm: '0.75rem',
base: '1rem',
md: '1.5rem',
lg: '2rem',
xl: '2.5rem',
xxl: '3rem',
'2xl': '3rem',
xxxl: '3.5rem',
'3xl': '3.5rem',
'4xl': '4rem',
'5xl': '4.5rem',
'6xl': '6rem',
'7xl': '7.5rem',
},
breakpoints: {
xxs: '320px',
xs: '480px',
mobile: '768px',
tablet: '1024px',
},
};
export const whiteLabelGlobals = {
colors: {
'logo-1-light': '#4844AD',
'logo-2-light': '#4844AD',
'logo-1-dark': '#BEC5F0',
'logo-2-dark': '#BEC5F0',
'brand-050': '#EEF1FA',
'brand-100': '#DDE2F5',
'brand-150': '#CED3F1',
'brand-200': '#BEC5F0',
'brand-250': '#AFB5F1',
'brand-300': '#A0A5F6',
'brand-350': '#8F94FD',
'brand-400': '#8184FC',
'brand-450': '#7576EE',
'brand-500': '#6969DF',
'brand-550': '#5E5CD0',
'brand-600': '#534FC2',
'brand-650': '#4844AD',
'brand-700': '#3E3B98',
'brand-750': '#36347D',
'brand-800': '#2D2F5F',
'brand-850': '#262848',
'brand-900': '#1C1E32',
'brand-950': '#11131F',
'gray-000': '#FFFFFF',
'gray-025': '#F8F8F9',
'gray-050': '#F0F0F3',
'gray-100': '#E2E2EA',
'gray-150': '#D3D4E0',
'gray-200': '#C5C6D5',
'gray-250': '#B7B7CB',
'gray-300': '#A9A9BF',
'gray-350': '#9C9CB2',
'gray-400': '#8F8FA4',
'gray-450': '#828297',
'gray-500': '#75758A',
'gray-550': '#69697D',
'gray-600': '#5D5D70',
'gray-650': '#515164',
'gray-700': '#454558',
'gray-750': '#3A3A4C',
'gray-800': '#2F303D',
'gray-850': '#25252F',
'gray-900': '#1B1B23',
'gray-950': '#111114',
'gray-1000': '#000000',
'info-050': '#EAF2F9',
'info-100': '#D5E4F3',
'info-150': '#BFD7F0',
'info-200': '#A7CAEE',
'info-250': '#8DBDEF',
'info-300': '#6EB0F2',
'info-350': '#50A2F5',
'info-400': '#3593F4',
'info-450': '#1185ED',
'info-500': '#0077DE',
'info-550': '#0069CF',
'info-600': '#005BC0',
'info-650': '#0D4EAA',
'info-700': '#124394',
'info-750': '#163878',
'info-800': '#192F5A',
'info-850': '#192541',
'info-900': '#141B2D',
'info-950': '#0C111C',
'success-050': '#E8F1EA',
'success-100': '#CFE4D4',
'success-150': '#BAD9C1',
'success-200': '#A2CFAD',
'success-250': '#86C597',
'success-300': '#6CBA83',
'success-350': '#4FB070',
'success-400': '#40A363',
'success-450': '#309556',
'success-500': '#1E884A',
'success-550': '#027B3E',
'success-600': '#016D31',
'success-650': '#006024',
'success-700': '#005317',
'success-750': '#0D4511',
'success-800': '#11380E',
'success-850': '#132A11',
'success-900': '#101E0F',
'success-950': '#091209',
'warning-050': '#F8F0E9',
'warning-100': '#F1E0D3',
'warning-150': '#ECD0BC',
'warning-200': '#E8C0A4',
'warning-250': '#E8AE8A',
'warning-300': '#EB9970',
'warning-350': '#E98456',
'warning-400': '#E57036',
'warning-450': '#DA5E18',
'warning-500': '#CB5000',
'warning-550': '#BC4200',
'warning-600': '#AD3300',
'warning-650': '#9E2300',
'warning-700': '#882011',
'warning-750': '#731E16',
'warning-800': '#58201A',
'warning-850': '#401D18',
'warning-900': '#2E1714',
'warning-950': '#1D0F0D',
'error-050': '#F9EFEC',
'error-100': '#F4DFD9',
'error-150': '#F0CEC6',
'error-200': '#EEBCB2',
'error-250': '#EEA99D',
'error-300': '#EF9486',
'error-350': '#F37C6E',
'error-400': '#F65F53',
'error-450': '#F0463D',
'error-500': '#E82322',
'error-550': '#D7010E',
'error-600': '#C00100',
'error-650': '#AA0000',
'error-700': '#910C06',
'error-750': '#731E16',
'error-800': '#58201A',
'error-850': '#401D18',
'error-900': '#2E1714',
'error-950': '#1D0F0D',
'red-050': '#FAEFEE',
'red-100': '#F4DEDD',
'red-150': '#F1CDCB',
'red-200': '#EFBBBA',
'red-250': '#EEA8A8',
'red-300': '#F09394',
'red-350': '#F37B7E',
'red-400': '#EF6569',
'red-450': '#E94A55',
'red-500': '#DA3B49',
'red-550': '#CA2A3C',
'red-600': '#BB1330',
'red-650': '#A90021',
'red-700': '#910A13',
'red-750': '#731E16',
'red-800': '#58201A',
'red-850': '#411D18',
'red-900': '#2E1714',
'red-950': '#1D0F0D',
'orange-050': '#F8F0E9',
'orange-100': '#F1E0D3',
'orange-150': '#ECD0BD',
'orange-200': '#EABFA6',
'orange-250': '#EBAC90',
'orange-300': '#EC9772',
'orange-350': '#E5845A',
'orange-400': '#D6774D',
'orange-450': '#C86A40',
'orange-500': '#B95D33',
'orange-550': '#AB5025',
'orange-600': '#9D4315',
'orange-650': '#8F3600',
'orange-700': '#812900',
'orange-750': '#6C2511',
'orange-800': '#572017',
'orange-850': '#401D18',
'orange-900': '#2E1714',
'orange-950': '#1D0F0D',
'brown-050': '#F6F0E8',
'brown-100': '#F1E0D3',
'brown-150': '#EBD0BA',
'brown-200': '#E2C0A6',
'brown-250': '#D4B398',
'brown-300': '#C6A58B',
'brown-350': '#B8987E',
'brown-400': '#AA8B71',
'brown-450': '#9D7E65',
'brown-500': '#8F7158',
'brown-550': '#82654C',
'brown-600': '#765841',
'brown-650': '#694C35',
'brown-700': '#5D412A',
'brown-750': '#51361E',
'brown-800': '#452A13',
'brown-850': '#392008',
'brown-900': '#29180A',
'brown-950': '#1B0F08',
'yellow-050': '#F3F0E7',
'yellow-100': '#E9E2CF',
'yellow-150': '#E1D4B7',
'yellow-200': '#D9C599',
'yellow-250': '#D2B677',
'yellow-300': '#CAA756',
'yellow-350': '#C2972E',
'yellow-400': '#B98900',
'yellow-450': '#AB7B00',
'yellow-500': '#9D6E00',
'yellow-550': '#916100',
'yellow-600': '#855400',
'yellow-650': '#784700',
'yellow-700': '#6C3A00',
'yellow-750': '#5F2E00',
'yellow-800': '#512302',
'yellow-850': '#3E1D10',
'yellow-900': '#2D1711',
'yellow-950': '#1D0F0D',
'green-050': '#E6F1E9',
'green-100': '#CFE4D5',
'green-150': '#B8D8C1',
'green-200': '#A0CFAE',
'green-250': '#84C59A',
'green-300': '#65BA86',
'green-350': '#45B173',
'green-400': '#23A562',
'green-450': '#029755',
'green-500': '#008948',
'green-550': '#017B3B',
'green-600': '#006E2E',
'green-650': '#006022',
'green-700': '#005314',
'green-750': '#0D4510',
'green-800': '#11380E',
'green-850': '#132A11',
'green-900': '#101E0F',
'green-950': '#091209',
'blue-1-050': '#EBF1F9',
'blue-1-100': '#D6E4F4',
'blue-1-150': '#C1D7F0',
'blue-1-200': '#AACAEF',
'blue-1-250': '#8FBCEF',
'blue-1-300': '#7CAFEB',
'blue-1-350': '#68A1E4',
'blue-1-400': '#5B94D6',
'blue-1-450': '#4E86C7',
'blue-1-500': '#4279B9',
'blue-1-550': '#356CAC',
'blue-1-600': '#28609E',
'blue-1-650': '#1B5390',
'blue-1-700': '#0B4783',
'blue-1-750': '#0F3C6E',
'blue-1-800': '#133059',
'blue-1-850': '#152641',
'blue-1-900': '#121C2D',
'blue-1-950': '#0B111C',
'blue-2-050': '#E7F3F4',
'blue-2-100': '#CEE7E9',
'blue-2-150': '#B2DCE0',
'blue-2-200': '#91D1D7',
'blue-2-250': '#68C7D0',
'blue-2-300': '#43BBC5',
'blue-2-350': '#00AFBA',
'blue-2-400': '#01A0AA',
'blue-2-450': '#00929D',
'blue-2-500': '#00848F',
'blue-2-550': '#007682',
'blue-2-600': '#016874',
'blue-2-650': '#005B67',
'blue-2-700': '#004E5A',
'blue-2-750': '#00424E',
'blue-2-800': '#003642',
'blue-2-850': '#002A38',
'blue-2-900': '#061E28',
'blue-2-950': '#071219',
'purple-050': '#F7F0F6',
'purple-100': '#EEE0EE',
'purple-150': '#E7D1E7',
'purple-200': '#DBBFE4',
'purple-250': '#D3AEE2',
'purple-300': '#CB99E1',
'purple-350': '#C188D9',
'purple-400': '#B47BCB',
'purple-450': '#A66EBD',
'purple-500': '#9961AF',
'purple-550': '#8B55A1',
'purple-600': '#7E4894',
'purple-650': '#723C87',
'purple-700': '#633376',
'purple-750': '#552A65',
'purple-800': '#452551',
'purple-850': '#35213D',
'purple-900': '#261A2C',
'purple-950': '#17111C',
'pink-050': '#F8EFF4',
'pink-100': '#F0DFEA',
'pink-150': '#EACEDF',
'pink-200': '#E9BBD1',
'pink-250': '#E9A7C2',
'pink-300': '#E095B4',
'pink-350': '#D685A8',
'pink-400': '#C7799B',
'pink-450': '#B86C8D',
'pink-500': '#AA5F80',
'pink-550': '#9C5374',
'pink-600': '#8E4767',
'pink-650': '#813B5B',
'pink-700': '#732E4F',
'pink-750': '#632643',
'pink-800': '#521F38',
'pink-850': '#3E1C2B',
'pink-900': '#2D171F',
'pink-950': '#1C0E12',
'black-000': '#1B1B2300',
'black-050': '#1B1B230D',
'black-100': '#1B1B231A',
'black-150': '#1B1B2326',
'black-200': '#1B1B2333',
'black-250': '#1B1B2340',
'black-300': '#1B1B234D',
'black-350': '#1B1B2359',
'black-400': '#1B1B2366',
'black-450': '#1B1B2373',
'black-500': '#1B1B2380',
'black-550': '#1B1B238C',
'black-600': '#1B1B2399',
'black-650': '#1B1B23A6',
'black-700': '#1B1B23B2',
'black-750': '#1B1B23BF',
'black-800': '#1B1B23CC',
'black-850': '#1B1B23D9',
'black-900': '#1B1B23E5',
'black-950': '#111114F2',
'white-000': '#F8F8F900',
'white-050': '#F8F8F90D',
'white-100': '#F8F8F91A',
'white-150': '#F8F8F926',
'white-200': '#F8F8F933',
'white-250': '#F8F8F940',
'white-300': '#F8F8F94D',
'white-350': '#F8F8F959',
'white-400': '#F8F8F966',
'white-450': '#F8F8F973',
'white-500': '#F8F8F980',
'white-550': '#F8F8F98C',
'white-600': '#F8F8F999',
'white-650': '#F8F8F9A6',
'white-700': '#F8F8F9B2',
'white-750': '#F8F8F9BF',
'white-800': '#F8F8F9CC',
'white-850': '#F8F8F9D9',
'white-900': '#F8F8F9E5',
'white-950': '#F8F8F9F2',
'white-975': '#F8F8F9F9',
},
...commonGlobals,
font: {
...commonGlobals.font,
families: {
base: 'Hanken Grotesk, Inter, Roboto Flex Variable, sans-serif',
accent: 'Hanken Grotesk, Inter, Roboto Flex Variable, sans-serif',
},
},
};
export const dsfrGlobals = {
...commonGlobals,
components: {
...commonGlobals.components,
'la-gaufre': true,
'home-proconnect': true,
logo: {
src: '/assets/logo-gouv.svg',
widthHeader: '110px',
widthFooter: '220px',
alt: 'Gouvernement Logo',
},
},
colors: {
'logo-1': '#2845C1',
'logo-2': '#C83F49',
'brand-050': '#EDF0FF',
'brand-100': '#DAE2FF',
'brand-150': '#C8D3FF',
'brand-200': '#B5C4FF',
'brand-250': '#A2B6FF',
'brand-300': '#90A7FF',
'brand-350': '#7E98FF',
'brand-400': '#6C89FE',
'brand-450': '#5C7AF7',
'brand-500': '#4C6CEF',
'brand-550': '#3E5DE7',
'brand-600': '#304DDF',
'brand-650': '#2845C1',
'brand-700': '#223E9E',
'brand-750': '#1F367D',
'brand-800': '#1B2E5F',
'brand-850': '#172446',
'brand-900': '#121B30',
'brand-950': '#0C111A',
'gray-100': '#DFE2EA',
'gray-150': '#CFD5DE',
'gray-200': '#C1C7D3',
'gray-250': '#B2B9C7',
'gray-300': '#A4ABBC',
'gray-350': '#969EB0',
'gray-400': '#8891A4',
'gray-450': '#7B8498',
'gray-500': '#6D778C',
'gray-550': '#626A80',
'gray-600': '#555E74',
'gray-650': '#4A5267',
'gray-700': '#3F4759',
'gray-750': '#363B4C',
'gray-800': '#2B303D',
'gray-850': '#222631',
'gray-900': '#181B24',
'gray-950': '#0F1117',
'gray-1000': '#000000',
'info-050': '#E7F2FF',
'info-100': '#CFE5FF',
'info-150': '#B7D7FF',
'info-200': '#A0CAFE',
'info-250': '#8CBCF9',
'info-300': '#77AEF4',
'info-350': '#63A0EE',
'info-400': '#5092E7',
'info-450': '#4185DC',
'info-500': '#3677CC',
'info-550': '#2F6ABB',
'info-600': '#265EAA',
'info-650': '#28528F',
'info-700': '#274775',
'info-750': '#243C5E',
'info-800': '#20314A',
'info-850': '#1B2637',
'info-900': '#141C27',
'info-950': '#0D1118',
'success-050': '#DEF7E6',
'success-100': '#BAEECF',
'success-150': '#A5E2C0',
'success-200': '#95D4B3',
'success-250': '#85C6A7',
'success-300': '#74B99B',
'success-350': '#65AB8F',
'success-400': '#579E84',
'success-450': '#4B9079',
'success-500': '#40836F',
'success-550': '#367664',
'success-600': '#2B695A',
'success-650': '#2C5A50',
'success-700': '#2A4D45',
'success-750': '#26403C',
'success-800': '#213430',
'success-850': '#1B2826',
'success-900': '#151D1C',
'success-950': '#0D1212',
'warning-050': '#FFEEDF',
'warning-100': '#FFDCBE',
'warning-150': '#FFCA9C',
'warning-200': '#FFB778',
'warning-250': '#FDA54F',
'warning-300': '#F59425',
'warning-350': '#E78613',
'warning-400': '#D7790C',
'warning-450': '#C86C08',
'warning-500': '#B85F03',
'warning-550': '#A75400',
'warning-600': '#984800',
'warning-650': '#814112',
'warning-700': '#6C3A19',
'warning-750': '#58321C',
'warning-800': '#452A1A',
'warning-850': '#352117',
'warning-900': '#261813',
'warning-950': '#170F0C',
'error-050': '#FFEDEB',
'error-100': '#FFDAD7',
'error-150': '#FFC7C2',
'error-200': '#FFB3AD',
'error-250': '#FF9F99',
'error-300': '#FF8984',
'error-350': '#FF706E',
'error-400': '#FB5759',
'error-450': '#F63A45',
'error-500': '#E32C39',
'error-550': '#CF202D',
'error-600': '#BD0F23',
'error-650': '#9D2227',
'error-700': '#812727',
'error-750': '#672624',
'error-800': '#512220',
'error-850': '#3D1C1B',
'error-900': '#2A1614',
'error-950': '#190E0D',
'red-050': '#FFEDEB',
'red-100': '#FFDAD7',
'red-150': '#FFC7C2',
'red-200': '#FFB3AD',
'red-250': '#FF9F99',
'red-300': '#FF8984',
'red-350': '#FF706E',
'red-400': '#FB5759',
'red-450': '#F63A45',
'red-500': '#E32C39',
'red-550': '#CF202D',
'red-600': '#BD0F23',
'red-650': '#9D2227',
'red-700': '#812727',
'red-750': '#672624',
'red-800': '#512220',
'red-850': '#3D1C1B',
'red-900': '#410003',
'red-950': '#190E0D',
'orange-050': '#FCEDEB',
'orange-100': '#F8DCD7',
'orange-150': '#F1CCC5',
'orange-200': '#EABCB4',
'orange-250': '#E2ACA2',
'orange-300': '#DA9C92',
'orange-350': '#D28C81',
'orange-400': '#CA7C70',
'orange-450': '#BE6E62',
'orange-500': '#AE6257',
'orange-550': '#9E564D',
'orange-600': '#8F4B42',
'orange-650': '#79443D',
'orange-700': '#643C37',
'orange-750': '#513430',
'orange-800': '#412B28',
'orange-850': '#312220',
'orange-900': '#231918',
'orange-950': '#150F0F',
'brown-050': '#F9EFEA',
'brown-100': '#F3DFD3',
'brown-150': '#EACFC1',
'brown-200': '#E2BFAE',
'brown-250': '#D8B19C',
'brown-300': '#D0A189',
'brown-350': '#C3937B',
'brown-400': '#B5866D',
'brown-450': '#A77A62',
'brown-500': '#996D57',
'brown-550': '#8B614D',
'brown-600': '#7C5542',
'brown-650': '#6A4C3C',
'brown-700': '#594236',
'brown-750': '#49382F',
'brown-800': '#3B2E28',
'brown-850': '#2D2420',
'brown-900': '#201A18',
'brown-950': '#13100F',
'yellow-050': '#FDF1C5',
'yellow-100': '#FBE18E',
'yellow-150': '#F4D261',
'yellow-200': '#EAC244',
'yellow-250': '#DFB41B',
'yellow-300': '#D1A516',
'yellow-350': '#C49711',
'yellow-400': '#B78A0C',
'yellow-450': '#A87D07',
'yellow-500': '#9B6F02',
'yellow-550': '#8D6300',
'yellow-600': '#7F5600',
'yellow-650': '#6E4C11',
'yellow-700': '#5D4219',
'yellow-750': '#4D371B',
'yellow-800': '#3D2E1A',
'yellow-850': '#2F2417',
'yellow-900': '#221A12',
'yellow-950': '#14100C',
'green-050': '#E7F9B3',
'green-100': '#D5EC98',
'green-150': '#C5DE86',
'green-200': '#B5D174',
'green-250': '#A5C464',
'green-300': '#95B755',
'green-350': '#85AA45',
'green-400': '#769D39',
'green-450': '#688F30',
'green-500': '#5A8228',
'green-550': '#4D7621',
'green-600': '#416919',
'green-650': '#3A5B20',
'green-700': '#324E22',
'green-750': '#2C4122',
'green-800': '#24351D',
'green-850': '#1D2919',
'green-900': '#161E13',
'green-950': '#0E120C',
'blue-1-050': '#E7F2FF',
'blue-1-100': '#CFE5FF',
'blue-1-150': '#B7D7FF',
'blue-1-200': '#A0CAFE',
'blue-1-250': '#8CBCF9',
'blue-1-300': '#77AEF4',
'blue-1-350': '#63A0EE',
'blue-1-400': '#5092E7',
'blue-1-450': '#4185DC',
'blue-1-500': '#3677CC',
'blue-1-550': '#2F6ABB',
'blue-1-600': '#265EAA',
'blue-1-650': '#28528F',
'blue-1-700': '#274775',
'blue-1-750': '#243C5E',
'blue-1-800': '#20314A',
'blue-1-850': '#1B2637',
'blue-1-900': '#141C27',
'blue-1-950': '#0D1118',
'blue-2-050': '#E2F4FD',
'blue-2-100': '#C4E8F8',
'blue-2-150': '#AADCF2',
'blue-2-200': '#93CFEB',
'blue-2-250': '#7CC2E2',
'blue-2-300': '#6CB4D6',
'blue-2-350': '#5CA7C9',
'blue-2-400': '#5099BC',
'blue-2-450': '#458BAE',
'blue-2-500': '#3A7EA0',
'blue-2-550': '#327191',
'blue-2-600': '#286483',
'blue-2-650': '#2B5770',
'blue-2-700': '#294A5E',
'blue-2-750': '#263E4D',
'blue-2-800': '#22323D',
'blue-2-850': '#1C272E',
'blue-2-900': '#151D21',
'blue-2-950': '#0E1114',
'purple-050': '#F5EEFF',
'purple-100': '#ECDCFF',
'purple-150': '#E2CBFF',
'purple-200': '#D9B9FF',
'purple-250': '#D0A7FF',
'purple-300': '#C894FE',
'purple-350': '#BE83FA',
'purple-400': '#B570F5',
'purple-450': '#AB5EF0',
'purple-500': '#A04BE8',
'purple-550': '#933CDB',
'purple-600': '#8530C8',
'purple-650': '#7033A5',
'purple-700': '#5D3185',
'purple-750': '#4C2C6A',
'purple-800': '#3C2652',
'purple-850': '#2D203C',
'purple-900': '#21182A',
'purple-950': '#130F19',
'pink-050': '#FFEBF6',
'pink-100': '#FFD8ED',
'pink-150': '#FCC4E3',
'pink-200': '#F7B2D9',
'pink-250': '#F29FCE',
'pink-300': '#ED8CC3',
'pink-350': '#E779B8',
'pink-400': '#E264AD',
'pink-450': '#D2579E',
'pink-500': '#C24B8E',
'pink-550': '#B0417F',
'pink-600': '#9F3670',
'pink-650': '#873560',
'pink-700': '#6F3250',
'pink-750': '#5A2C43',
'pink-800': '#472635',
'pink-850': '#351F29',
'pink-900': '#26171D',
'pink-950': '#170F12',
'black-000': '#181B2400',
'black-050': '#181B240D',
'black-100': '#181B241A',
'black-150': '#181B2426',
'black-200': '#181B2433',
'black-250': '#181B2440',
'black-300': '#181B244D',
'black-350': '#181B2459',
'black-400': '#181B2466',
'black-450': '#181B2473',
'black-500': '#181B2480',
'black-550': '#181B248C',
'black-600': '#181B2499',
'black-650': '#181B24A6',
'black-700': '#181B24B2',
'black-750': '#181B24BF',
'black-800': '#181B24CC',
'black-850': '#181B24D9',
'black-900': '#181B24E5',
'black-950': '#0F1117F2',
'white-000': '#F6F8F900',
'white-050': '#F6F8F90D',
'white-100': '#F6F8F91A',
'white-150': '#F6F8F926',
'white-200': '#F6F8F933',
'white-250': '#F6F8F940',
'white-300': '#F6F8F94D',
'white-350': '#F6F8F959',
'white-400': '#F6F8F966',
'white-450': '#F6F8F973',
'white-500': '#F6F8F980',
'white-550': '#F6F8F98C',
'white-600': '#F6F8F999',
'white-650': '#F6F8F9A6',
'white-700': '#F6F8F9B2',
'white-750': '#F6F8F9BF',
'white-800': '#F6F8F9CC',
'white-850': '#F6F8F9D9',
'white-900': '#F6F8F9E5',
'white-950': '#F6F8F9F2',
'white-975': '#F6F8F9F9',
},
};
const whiteLabelThemes = getThemesFromGlobals(whiteLabelGlobals, {
overrides: commonTokenOverrides,
});
const dsfrThemes = getThemesFromGlobals(dsfrGlobals, {
overrides: commonTokenOverrides,
});
if (dsfrThemes.dark) {
dsfrThemes.dark.globals.components.logo.src =
'/assets/logo-gouv-darkmode.svg';
dsfrThemes.dark.globals.colors['logo-1'] = '#95ABFF';
dsfrThemes.dark.globals.colors['logo-2'] = '#E78087';
}
console.log(dsfrThemes);
const themes = {
themes: {
default: whiteLabelThemes.light,
dark: whiteLabelThemes.dark,
dsfr: dsfrThemes.light,
'dsfr-dark': dsfrThemes.dark,
},
};
const config = { ...themes };
export default config;
+1
View File
@@ -1,5 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
+4 -3
View File
@@ -15,16 +15,17 @@
"test:watch": "jest --watch"
},
"dependencies": {
"@gouvfr-lasuite/cunningham-react": "4.2.0",
"@gouvfr-lasuite/cunningham-tokens": "^3.1.0",
"@gouvfr-lasuite/integration": "1.0.3",
"@gouvfr-lasuite/ui-kit": "^0.17.0",
"@gouvfr-lasuite/ui-kit": "0.19.10",
"@hookform/resolvers": "4.0.0",
"@openfun/cunningham-react": "3.2.3",
"@tanstack/react-query": "5.90.5",
"i18next": "25.6.0",
"i18next-browser-languagedetector": "8.2.0",
"lodash": "4.17.23",
"luxon": "3.7.2",
"next": "15.5.10",
"next": "15.5.14",
"react": "*",
"react-dom": "*",
"react-hook-form": "7.65.0",
@@ -47,7 +47,7 @@ describe('Page', () => {
render(<Page />, { wrapper: AppWrapper });
expect(mockedPush).toHaveBeenCalledWith('/teams/');
expect(mockedPush).toHaveBeenCalledWith('/mail-domains/');
});
it('checks Page rendering without team feature', () => {
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M4.04601 20.3381C3.04306 20.3381 2.28465 20.0812 1.77079 19.5673C1.25693 19.0597 1 18.3105 1 17.32V6.01815C1 5.02139 1.25693 4.26917 1.77079 3.7615C2.28465 3.25383 3.04306 3 4.04601 3H19.9447C20.9538 3 21.7154 3.25383 22.2292 3.7615C22.7431 4.26917 23 5.02139 23 6.01815V17.32C23 18.3105 22.7431 19.0597 22.2292 19.5673C21.7154 20.0812 20.9538 20.3381 19.9447 20.3381H4.04601ZM4.15745 18.5087H19.8425C20.2697 18.5087 20.5979 18.3972 20.8269 18.1743C21.056 17.9515 21.1705 17.614 21.1705 17.1621V6.16674C21.1705 5.72098 21.056 5.38666 20.8269 5.16378C20.5979 4.9409 20.2697 4.82946 19.8425 4.82946H4.15745C3.72407 4.82946 3.39285 4.9409 3.16378 5.16378C2.9409 5.38666 2.82946 5.72098 2.82946 6.16674V17.1621C2.82946 17.614 2.9409 17.9515 3.16378 18.1743C3.39285 18.3972 3.72407 18.5087 4.15745 18.5087ZM8.38286 18.8058V4.51372H10.1195V18.8058H8.38286ZM6.56268 8.22837H4.65893C4.49796 8.22837 4.35556 8.16646 4.23174 8.04263C4.11411 7.91881 4.0553 7.78261 4.0553 7.63402C4.0553 7.47305 4.11411 7.33376 4.23174 7.21613C4.35556 7.09849 4.49796 7.03968 4.65893 7.03968H6.56268C6.72984 7.03968 6.87224 7.09849 6.98987 7.21613C7.11369 7.33376 7.1756 7.47305 7.1756 7.63402C7.1756 7.78261 7.11369 7.91881 6.98987 8.04263C6.87224 8.16646 6.72984 8.22837 6.56268 8.22837ZM6.56268 10.7172H4.65893C4.49796 10.7172 4.35556 10.6584 4.23174 10.5407C4.11411 10.4169 4.0553 10.2745 4.0553 10.1136C4.0553 9.95877 4.11411 9.82257 4.23174 9.70494C4.35556 9.58731 4.49796 9.52849 4.65893 9.52849H6.56268C6.72984 9.52849 6.87224 9.58731 6.98987 9.70494C7.11369 9.82257 7.1756 9.95877 7.1756 10.1136C7.1756 10.2745 7.11369 10.4169 6.98987 10.5407C6.87224 10.6584 6.72984 10.7172 6.56268 10.7172ZM6.56268 13.1967H4.65893C4.49796 13.1967 4.35556 13.1379 4.23174 13.0203C4.11411 12.9026 4.0553 12.7664 4.0553 12.6117C4.0553 12.4507 4.11411 12.3114 4.23174 12.1938C4.35556 12.0761 4.49796 12.0173 4.65893 12.0173H6.56268C6.72984 12.0173 6.87224 12.0761 6.98987 12.1938C7.11369 12.3114 7.1756 12.4507 7.1756 12.6117C7.1756 12.7664 7.11369 12.9026 6.98987 13.0203C6.87224 13.1379 6.72984 13.1967 6.56268 13.1967Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -1,6 +1,13 @@
<svg width="32" height="33" viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.1196 5.07095L17.3203 5.98796C17.4154 6.00474 17.4706 6.10543 17.4328 6.19433C17.2714 6.57329 17.1581 6.80005 17.0539 6.96275C16.9667 7.09909 16.8998 7.16937 16.8295 7.22281C16.7557 7.27883 16.6168 7.3625 16.3363 7.44595C15.0129 7.83959 14.2592 9.23154 14.6528 10.5549C15.0465 11.8784 16.4384 12.6321 17.7618 12.2384C19.4239 11.744 20.5126 10.8341 21.2653 9.65807C21.5989 9.13681 21.842 8.60181 22.0329 8.15355C22.1128 7.966 22.1849 7.79074 22.2525 7.62637L22.254 7.62267C22.4259 7.20484 22.8671 6.96601 23.312 7.04447C24.401 7.23649 25.1671 7.65773 25.6102 8.30817C26.0604 8.95985 26.1888 9.83372 25.9956 10.9298L23.0029 27.9022C22.8084 29.0053 22.3888 29.7825 21.7441 30.2339C21.1065 30.6865 20.2432 30.8168 19.1541 30.6248L7.96177 28.6513C6.87274 28.4593 6.10253 28.0409 5.65114 27.3963C5.20677 26.7529 5.08183 25.8796 5.27634 24.7765L8.26903 7.80411C8.4623 6.70806 8.87836 5.9302 9.51723 5.47054C10.1631 5.01212 11.0306 4.87892 12.1196 5.07095ZM13.2049 19.4392C13.6814 20.1198 14.3259 20.5317 15.1384 20.675C15.9509 20.8182 16.6974 20.6516 17.378 20.175C18.0586 19.6985 18.4705 19.054 18.6138 18.2415C18.757 17.429 18.5904 16.6825 18.1138 16.0019C17.6373 15.3213 16.9927 14.9094 16.1803 14.7661C15.3678 14.6229 14.6213 14.7895 13.9407 15.2661C13.2601 15.7426 12.8482 16.3872 12.7049 17.1996C12.5616 18.0121 12.7283 18.7586 13.2049 19.4392ZM8.27886 25.0247C8.19255 25.5142 8.5194 25.981 9.00891 26.0673L19.0539 27.8385C19.5434 27.9248 20.0102 27.598 20.0966 27.1084L20.3049 25.9267C20.3787 25.5081 20.3389 25.1045 20.1853 24.7156C20.0317 24.3268 19.7927 24.0086 19.4683 23.761C18.7724 23.2448 18.0473 22.8218 17.2931 22.4921C16.5389 22.1624 15.7555 21.9259 14.9431 21.7826C14.1306 21.6394 13.3136 21.5937 12.4921 21.6456C11.6707 21.6975 10.8446 21.8469 10.0141 22.094C9.62458 22.2157 9.29116 22.433 9.01387 22.7458C8.73658 23.0587 8.56104 23.4244 8.48724 23.8429L8.27886 25.0247Z" fill="#000091"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M24.694 3.88339C24.7066 4.43554 24.2692 4.89337 23.7171 4.90599C23.4094 4.91303 22.8638 4.99795 22.3424 5.23172C21.8251 5.46364 21.4224 5.79861 21.219 6.25464C21.0916 6.54023 20.9867 6.79533 20.88 7.05474C20.8115 7.22114 20.7424 7.38931 20.6661 7.56844C20.4842 7.99554 20.2799 8.43826 20.0151 8.85213C19.4512 9.73317 18.6551 10.4143 17.3473 10.8033C16.818 10.9608 16.2612 10.6593 16.1037 10.1299C15.9463 9.60059 16.2478 9.04381 16.7771 8.88635C17.6126 8.63784 18.0184 8.26165 18.3305 7.774C18.5036 7.50353 18.6559 7.18409 18.826 6.78475C18.8824 6.65241 18.9442 6.50248 19.01 6.34276C19.1273 6.05827 19.2574 5.7427 19.3924 5.44002C19.8608 4.38982 20.7275 3.76397 21.5241 3.40676C22.3167 3.05141 23.1294 2.91891 23.6714 2.90652C24.2235 2.89389 24.6813 3.33125 24.694 3.88339Z" fill="#C9191E"/>
<path d="M12.9899 3.0145C12.4754 2.81379 12.221 2.23399 12.4217 1.71946C12.6224 1.20494 13.2022 0.950538 13.7167 1.15125C14.2217 1.34824 14.9402 1.75073 15.5634 2.35572C16.0249 2.80379 16.4723 3.40191 16.7083 4.14453C16.7414 4.2487 16.6503 4.34682 16.5427 4.32784L14.5057 3.96866C14.3783 3.9462 14.2631 3.88087 14.1703 3.79076C13.7603 3.39276 13.2766 3.12635 12.9899 3.0145Z" fill="#C9191E"/>
<path d="M17.1398 10.8419C17.077 10.8132 17.0162 10.7776 16.9585 10.7348C16.6026 10.4708 16.308 10.1964 16.0646 9.91118C16.0338 9.455 16.3205 9.02217 16.7771 8.88635C17.0849 8.7948 17.3346 8.68583 17.5424 8.56059C17.6949 8.74952 17.8921 8.93714 18.1499 9.12841C18.5324 9.41212 18.6542 9.91688 18.4695 10.3349C18.1431 10.5197 17.772 10.677 17.3473 10.8033C17.2782 10.8239 17.209 10.8365 17.1398 10.8419Z" fill="#C9191E"/>
<svg width="102" height="40" viewBox="0 0 102 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.9631 8.43425L21.1133 9.34236C21.2075 9.35898 21.2622 9.45868 21.2247 9.54672C21.0649 9.92201 20.9527 10.1466 20.8496 10.3077C20.7632 10.4427 20.6969 10.5123 20.6273 10.5652C20.5543 10.6207 20.4167 10.7036 20.1389 10.7862C18.8283 11.176 18.0819 12.5544 18.4717 13.865C18.8616 15.1756 20.24 15.922 21.5506 15.5321C23.1965 15.0426 24.2747 14.1414 25.02 12.9768C25.3504 12.4606 25.5911 11.9308 25.7802 11.4869C25.8593 11.3012 25.9307 11.1276 25.9977 10.9649L25.9992 10.9612C26.1693 10.5474 26.6063 10.3109 27.0469 10.3886C28.1253 10.5788 28.884 10.9959 29.3228 11.64C29.7686 12.2854 29.8958 13.1508 29.7044 14.2362L26.7407 31.0438C26.5481 32.1362 26.1326 32.9059 25.4942 33.3529C24.8628 33.8011 24.0078 33.9302 22.9294 33.74L11.8457 31.7857C10.7672 31.5955 10.0045 31.1812 9.55746 30.5428C9.11741 29.9057 8.99369 29.0409 9.1863 27.9485L12.1499 11.1409C12.3413 10.0555 12.7534 9.28516 13.386 8.82996C14.0257 8.37599 14.8847 8.24409 15.9631 8.43425ZM17.0378 22.663C17.5098 23.337 18.148 23.7449 18.9526 23.8868C19.7572 24.0287 20.4965 23.8636 21.1704 23.3917C21.8444 22.9198 22.2524 22.2815 22.3942 21.4769C22.5361 20.6724 22.3711 19.9331 21.8991 19.2591C21.4272 18.5851 20.789 18.1772 19.9844 18.0353C19.1798 17.8934 18.4405 18.0585 17.7665 18.5304C17.0925 19.0023 16.6846 19.6406 16.5428 20.4452C16.4009 21.2497 16.5659 21.989 17.0378 22.663ZM12.1597 28.1942C12.0742 28.679 12.3979 29.1413 12.8826 29.2267L22.8301 30.9808C23.3149 31.0662 23.7772 30.7425 23.8626 30.2578L24.069 29.0875C24.1421 28.673 24.1026 28.2733 23.9505 27.8882C23.7984 27.5032 23.5618 27.188 23.2405 26.9428C22.5513 26.4317 21.8333 26.0128 21.0864 25.6863C20.3395 25.3598 19.5638 25.1256 18.7592 24.9837C17.9546 24.8418 17.1456 24.7966 16.332 24.848C15.5185 24.8994 14.7006 25.0474 13.8781 25.292C13.4923 25.4125 13.1621 25.6277 12.8876 25.9375C12.613 26.2473 12.4391 26.6095 12.366 27.024L12.1597 28.1942Z" fill="#2845C1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.4153 7.25835C28.4278 7.80513 27.9947 8.25852 27.4479 8.27102C27.1432 8.27799 26.6029 8.36208 26.0866 8.59358C25.5744 8.82325 25.1755 9.15497 24.9741 9.60658C24.8479 9.88939 24.744 10.142 24.6384 10.3989C24.5706 10.5637 24.5021 10.7302 24.4266 10.9076C24.2464 11.3306 24.0442 11.769 23.7819 12.1788C23.2235 13.0513 22.4351 13.7259 21.14 14.1111C20.6158 14.267 20.0644 13.9685 19.9085 13.4443C19.7526 12.92 20.0512 12.3687 20.5754 12.2127C21.4027 11.9666 21.8046 11.5941 22.1137 11.1112C22.2851 10.8433 22.4359 10.527 22.6044 10.1315C22.6602 10.0005 22.7214 9.85201 22.7866 9.69384C22.9027 9.41211 23.0316 9.0996 23.1652 8.79986C23.6291 7.75986 24.4873 7.14008 25.2763 6.78635C26.0611 6.43445 26.866 6.30323 27.4027 6.29096C27.9494 6.27845 28.4028 6.71157 28.4153 7.25835Z" fill="#C83F49"/>
<path d="M16.8249 6.39789C16.3154 6.19914 16.0634 5.62496 16.2622 5.11543C16.461 4.6059 17.0351 4.35397 17.5447 4.55273C18.0448 4.74781 18.7562 5.14639 19.3734 5.74551C19.8305 6.18923 20.2735 6.78154 20.5072 7.51695C20.5399 7.6201 20.4498 7.71728 20.3432 7.69848L18.326 7.34279C18.1998 7.32055 18.0858 7.25585 17.9938 7.16661C17.5878 6.77248 17.1088 6.50865 16.8249 6.39789Z" fill="#C83F49"/>
<path d="M20.9346 14.1493C20.8723 14.1209 20.8121 14.0856 20.755 14.0432C20.4026 13.7818 20.1108 13.5101 19.8697 13.2276C19.8392 12.7759 20.1232 12.3472 20.5754 12.2127C20.8802 12.1221 21.1274 12.0142 21.3332 11.8901C21.4842 12.0772 21.6795 12.263 21.9349 12.4524C22.3136 12.7334 22.4342 13.2333 22.2513 13.6472C21.9281 13.8303 21.5606 13.986 21.14 14.1111C21.0716 14.1315 21.003 14.144 20.9346 14.1493Z" fill="#C83F49"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M70.4198 15.5822C71.2645 15.5822 72.0007 15.7275 72.6302 16.0144C72.6566 16.0264 72.6853 16.0329 72.7143 16.0329H76.6529C76.7622 16.0329 76.8509 16.1216 76.8509 16.2309V17.9566C76.8509 18.066 76.7622 18.1547 76.6529 18.1547H74.9582C74.8185 18.1547 74.7224 18.2957 74.7635 18.4292C74.8753 18.793 74.9322 19.1752 74.9322 19.5763C74.9322 20.3132 74.7526 20.9905 74.3916 21.6071C74.0457 22.2238 73.5337 22.7122 72.8569 23.0732C72.1799 23.4343 71.3675 23.6148 70.4198 23.6148C69.9907 23.6148 69.5888 23.5759 69.2138 23.4993C69.1523 23.4868 69.0878 23.5023 69.0418 23.545C68.9855 23.5972 68.9329 23.6505 68.8851 23.7057C68.6899 23.9161 68.5922 24.1413 68.592 24.3817C68.592 24.6072 68.6599 24.7879 68.7951 24.9233C68.9305 25.0436 69.141 25.1041 69.4266 25.1041H72.4507C73.4284 25.1041 74.2409 25.2621 74.8877 25.578C75.5496 25.8788 76.0379 26.3004 76.3538 26.842C76.6848 27.3835 76.8509 28 76.8509 28.692C76.8509 29.3087 76.7073 29.8801 76.4215 30.4066C76.1508 30.9331 75.7527 31.3926 75.2262 31.7837C74.7149 32.1898 74.098 32.4977 73.3762 32.7083C72.6541 32.9339 71.8561 33.0467 70.9836 33.0467C69.8104 33.0467 68.7947 32.9041 67.9373 32.6183C67.0951 32.3476 66.4404 31.9413 65.9742 31.3998C65.523 30.8584 65.2973 30.1964 65.2972 29.4144C65.2972 28.7074 65.5085 28.0827 65.9297 27.5412C66.1454 27.2715 66.4063 27.0132 66.7127 26.7671C66.8097 26.6892 66.8169 26.5428 66.7339 26.45C66.6244 26.3276 66.5289 26.195 66.448 26.0518C66.2676 25.7059 66.1772 25.3447 66.1772 24.9687C66.1773 24.3821 66.3578 23.863 66.7188 23.4117C66.8658 23.2294 67.0444 23.057 67.2537 22.8934C67.3604 22.8101 67.3642 22.6465 67.2619 22.5578C66.9468 22.2847 66.6828 21.9684 66.4712 21.6071C66.1103 20.9905 65.9297 20.3132 65.9297 19.5763C65.9297 18.8542 66.1102 18.1916 66.4712 17.5899C66.8322 16.9734 67.3434 16.4847 68.005 16.1238C68.682 15.7628 69.4872 15.5822 70.4198 15.5822ZM68.7065 27.2481C68.6601 27.2481 68.6149 27.2644 68.5798 27.2947C68.3541 27.4896 68.1547 27.6995 67.9828 27.9251C67.7572 28.256 67.6443 28.647 67.6443 29.0982C67.6443 29.4893 67.7725 29.8278 68.0282 30.1136C68.2989 30.4143 68.6747 30.6401 69.1558 30.7905C69.6523 30.956 70.2544 31.0391 70.9614 31.0391C71.6985 31.0391 72.3232 30.9488 72.8346 30.7683C73.3459 30.6028 73.7298 30.3545 73.9855 30.0236C74.2562 29.7078 74.3916 29.3385 74.3916 28.9173C74.3916 28.6017 74.3237 28.316 74.1885 28.0605C74.0532 27.8199 73.8273 27.6239 73.5116 27.4735C73.1957 27.323 72.7587 27.2481 72.2022 27.2481H68.7065ZM70.4421 17.4545C69.9759 17.4545 69.5695 17.553 69.2235 17.7485C68.8928 17.929 68.6371 18.1774 68.4566 18.4931C68.2913 18.809 68.2081 19.1702 68.2081 19.5763C68.2082 19.9823 68.2912 20.3508 68.4566 20.6816C68.6371 21.0125 68.8928 21.268 69.2235 21.4485C69.5695 21.629 69.9759 21.7193 70.4421 21.7193C70.9084 21.7193 71.3074 21.6291 71.6383 21.4485C71.9692 21.253 72.2176 20.9975 72.383 20.6816C72.5635 20.3508 72.6537 19.9823 72.6538 19.5763C72.6538 19.1551 72.5635 18.7858 72.383 18.4699C72.2175 18.1541 71.9692 17.9057 71.6383 17.7253C71.3074 17.5448 70.9084 17.4545 70.4421 17.4545Z" fill="#2845C1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M59.0663 15.5822C59.8483 15.5822 60.5554 15.7096 61.1871 15.9652C61.8188 16.2209 62.3606 16.5749 62.8118 17.0261C63.2781 17.4774 63.632 18.0118 63.8727 18.6285C64.1133 19.2451 64.2334 19.9143 64.2334 20.6362C64.2334 20.907 64.219 21.1706 64.1889 21.4263C64.1671 21.6115 64.1412 21.773 64.1114 21.9108C64.0925 21.9981 64.0138 22.0578 63.9245 22.0578H56.0046C55.8896 22.0578 55.7984 22.1555 55.8105 22.2698C55.8739 22.868 56.0185 23.384 56.2463 23.8179C56.5621 24.4195 57.0055 24.8709 57.577 25.1718C58.1486 25.4727 58.7959 25.6234 59.5179 25.6234C60.1948 25.6234 60.8042 25.488 61.3457 25.2173C61.8337 24.9798 62.2348 24.6727 62.5493 24.2964C62.6214 24.2101 62.7497 24.1925 62.8376 24.2626L64.3098 25.4363C64.3934 25.5029 64.4093 25.624 64.3426 25.7074C63.8329 26.3439 63.1869 26.8575 62.4056 27.2481C61.5633 27.6541 60.608 27.8574 59.5402 27.8574C58.2165 27.8574 57.088 27.5784 56.1554 27.0218C55.2229 26.4502 54.5087 25.6982 54.0123 24.7656C53.5159 23.833 53.2677 22.8175 53.2677 21.7193C53.2677 20.862 53.395 20.0647 53.6506 19.3277C53.9214 18.5906 54.305 17.9434 54.8015 17.3868C55.2978 16.8153 55.9073 16.372 56.6292 16.0561C57.3513 15.7402 58.1638 15.5822 59.0663 15.5822ZM58.9986 17.6353C58.3369 17.6354 57.758 17.7862 57.2617 18.087C56.7804 18.3878 56.4111 18.8311 56.1554 19.4177C56.0726 19.6028 56.0045 19.8025 55.9492 20.0163C55.9178 20.1378 56.012 20.2532 56.1375 20.2532H61.5891C61.7009 20.2532 61.7913 20.1606 61.7796 20.0493C61.7421 19.6925 61.6355 19.3543 61.4579 19.0347C61.2473 18.6136 60.9394 18.275 60.5334 18.0193C60.1272 17.7635 59.6154 17.6353 58.9986 17.6353Z" fill="#2845C1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M88.7228 15.5822C89.5048 15.5822 90.2119 15.7096 90.8436 15.9652C91.4753 16.2209 92.0171 16.5749 92.4683 17.0261C92.9346 17.4774 93.2885 18.0118 93.5292 18.6285C93.7698 19.2451 93.8899 19.9143 93.8899 20.6362C93.8899 20.907 93.8755 21.1706 93.8454 21.4263C93.8236 21.6115 93.7977 21.773 93.7679 21.9108C93.7491 21.9981 93.6703 22.0578 93.581 22.0578H85.6611C85.5461 22.0578 85.455 22.1555 85.4671 22.2698C85.5304 22.868 85.675 23.384 85.9028 23.8179C86.2187 24.4195 86.662 24.8709 87.2335 25.1718C87.8051 25.4727 88.4524 25.6234 89.1744 25.6234C89.8513 25.6234 90.4607 25.488 91.0022 25.2173C91.4902 24.9798 91.8913 24.6727 92.2058 24.2964C92.2779 24.2101 92.4062 24.1925 92.4941 24.2626L93.9664 25.4363C94.0499 25.5029 94.0659 25.624 93.9991 25.7074C93.4894 26.3439 92.8434 26.8575 92.0621 27.2481C91.2198 27.6542 90.2646 27.8574 89.1967 27.8574C87.8729 27.8574 86.7445 27.5784 85.8119 27.0218C84.8794 26.4502 84.1652 25.6982 83.6688 24.7656C83.1724 23.833 82.9242 22.8175 82.9242 21.7193C82.9242 20.862 83.0515 20.0647 83.3071 19.3277C83.5779 18.5906 83.9616 17.9434 84.458 17.3868C84.9543 16.8153 85.5638 16.372 86.2858 16.0561C87.0078 15.7402 87.8203 15.5822 88.7228 15.5822ZM88.6551 17.6353C87.9934 17.6354 87.4146 17.7862 86.9182 18.087C86.4369 18.3878 86.0676 18.8311 85.8119 19.4177C85.7291 19.6028 85.661 19.8025 85.6057 20.0163C85.5743 20.1378 85.6685 20.2532 85.794 20.2532H91.2456C91.3574 20.2532 91.4478 20.1606 91.4361 20.0493C91.3986 19.6925 91.292 19.3543 91.1144 19.0347C90.9038 18.6136 90.5959 18.275 90.1899 18.0193C89.7837 17.7635 89.2719 17.6353 88.6551 17.6353Z" fill="#2845C1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M45.1829 11.6104C46.8676 11.6105 48.191 12.0248 49.1537 12.8522C50.1314 13.6795 50.6207 14.7998 50.6208 16.2137C50.6208 17.1613 50.395 17.9811 49.9438 18.673C49.5076 19.35 48.8829 19.8772 48.0706 20.2532C48.045 20.2648 48.0353 20.295 48.0514 20.318L52.7942 27.0941C52.8861 27.2254 52.7922 27.4058 52.632 27.4058H49.8695C49.803 27.4058 49.7409 27.3723 49.7043 27.3168L45.4859 20.9267C45.4297 20.8416 45.3338 20.7925 45.2318 20.7943C45.2155 20.7946 45.1992 20.7948 45.1829 20.7948H43.5754C43.466 20.7948 43.3773 20.8835 43.3773 20.9928V27.2077C43.3773 27.3171 43.2887 27.4058 43.1793 27.4058H40.7999C40.6905 27.4058 40.6018 27.3171 40.6018 27.2077V11.8085C40.6018 11.6991 40.6905 11.6104 40.7999 11.6104H45.1829ZM43.3773 18.2951C43.3773 18.4045 43.466 18.4931 43.5754 18.4931H45.3183C46.1002 18.4931 46.7017 18.2898 47.1228 17.8839C47.544 17.4627 47.7553 16.8903 47.7553 16.1683C47.7552 15.4915 47.544 14.9497 47.1228 14.5436C46.7017 14.1377 46.1001 13.9353 45.3183 13.9353H43.5754C43.466 13.9353 43.3773 14.024 43.3773 14.1333V18.2951Z" fill="#2845C1"/>
<path d="M80.7927 27.2077C80.7927 27.3171 80.7041 27.4058 80.5947 27.4058H78.5093C78.3999 27.4058 78.3112 27.3171 78.3112 27.2077V16.2309C78.3112 16.1216 78.3999 16.0329 78.5093 16.0329H80.5947C80.7041 16.0329 80.7927 16.1216 80.7927 16.2309V27.2077Z" fill="#2845C1"/>
<path d="M60.4572 14.1456C60.42 14.1975 60.3601 14.2283 60.2962 14.2283H58.3846C58.2293 14.2283 58.1344 14.0578 58.2163 13.9258L60.2944 10.5764C60.3305 10.5182 60.3942 10.4828 60.4627 10.4828H62.6969C62.8582 10.4828 62.9519 10.6652 62.8579 10.7962L60.4572 14.1456Z" fill="#2845C1"/>
<path d="M79.5742 10.3474C79.875 10.3474 80.1459 10.4224 80.3866 10.5727C80.6423 10.7232 80.8382 10.9263 80.9736 11.182C81.124 11.4227 81.1989 11.6935 81.1989 11.9944C81.1989 12.2951 81.1239 12.5733 80.9736 12.8289C80.8382 13.0847 80.6423 13.2878 80.3866 13.4382C80.1459 13.5736 79.875 13.6413 79.5742 13.6413C79.2734 13.6413 78.9953 13.5736 78.7396 13.4382C78.4839 13.2878 78.2808 13.0847 78.1304 12.8289C77.98 12.5733 77.905 12.2951 77.905 11.9944C77.905 11.5431 78.063 11.1594 78.3789 10.8435C78.7098 10.5127 79.1081 10.3475 79.5742 10.3474Z" fill="#2845C1"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 12 KiB

+112 -12
View File
@@ -1,4 +1,4 @@
import { ComponentPropsWithRef } from 'react';
import { ComponentPropsWithRef, HTMLElementType } from 'react';
import styled from 'styled-components';
import { CSSProperties, RuleSet } from 'styled-components/dist/types';
@@ -11,11 +11,13 @@ import {
import { hideEffect, showEffect } from './Effect';
export interface BoxProps {
as?: keyof HTMLElementTagNameMap;
as?: HTMLElementType;
$align?: CSSProperties['alignItems'];
$background?: CSSProperties['background'];
$border?: CSSProperties['border'];
$color?: CSSProperties['color'];
$css?: string | RuleSet<object>;
$cursor?: CSSProperties['cursor'];
$direction?: CSSProperties['flexDirection'];
$display?: CSSProperties['display'];
$effect?: 'show' | 'hide';
@@ -24,6 +26,7 @@ export interface BoxProps {
$hasTransition?: boolean | 'slow';
$height?: CSSProperties['height'];
$justify?: CSSProperties['justifyContent'];
$opacity?: CSSProperties['opacity'];
$overflow?: CSSProperties['overflow'];
$margin?: MarginPadding;
$maxHeight?: CSSProperties['maxHeight'];
@@ -36,28 +39,44 @@ export interface BoxProps {
$shrink?: CSSProperties['flexShrink'];
$transition?: CSSProperties['transition'];
$width?: CSSProperties['width'];
$wrap?: CSSProperties['flexWrap'];
$zIndex?: CSSProperties['zIndex'];
$wrap?: CSSProperties['flexWrap'];
// Theming props
$layer?: 'background' | 'content' | 'border';
$theme?:
| 'brand'
| 'error'
| 'gray'
| 'info'
| 'success'
| 'warning'
| 'neutral'
| 'contextual'
| 'disabled'
| (string & {});
$scope?: 'surface' | 'semantic' | 'palette' | (string & {});
$variation?: 'primary' | 'secondary' | 'tertiary' | (string & {});
$withThemeBG?: boolean;
$withThemeBorder?: boolean;
$withThemeInherited?: boolean;
}
export type BoxType = ComponentPropsWithRef<typeof Box>;
export const Box = styled('div')<BoxProps>`
display: flex;
flex-direction: column;
${({ $align }) => $align && `align-items: ${$align};`}
${({ $background }) => $background && `background: ${$background};`}
${({ $color }) => $color && `color: ${$color};`}
${({ $direction }) => $direction && `flex-direction: ${$direction};`}
${({ $display }) => $display && `display: ${$display};`}
${({ $cursor }) => $cursor && `cursor: ${$cursor};`}
${({ $direction }) => `flex-direction: ${$direction || 'column'};`}
${({ $display, as }) =>
`display: ${$display || (as?.match('span|input') ? 'inline-flex' : 'flex')};`}
${({ $flex }) => $flex && `flex: ${$flex};`}
${({ $gap }) => $gap && `gap: ${$gap};`}
${({ $height }) => $height && `height: ${$height};`}
${({ $hasTransition }) =>
$hasTransition && $hasTransition === 'slow'
? `transition: all 0.5s ease-in-out;`
? `transition: all 0.5s var(--c--globals--transitions--ease-out);`
: $hasTransition
? `transition: all 0.3s ease-in-out;`
? `transition: all var(--c--globals--transitions--duration) var(--c--globals--transitions--ease-out);`
: ''}
${({ $justify }) => $justify && `justify-content: ${$justify};`}
${({ $margin }) => $margin && stylesMargin($margin)}
@@ -65,11 +84,92 @@ export const Box = styled('div')<BoxProps>`
${({ $minHeight }) => $minHeight && `min-height: ${$minHeight};`}
${({ $maxWidth }) => $maxWidth && `max-width: ${$maxWidth};`}
${({ $minWidth }) => $minWidth && `min-width: ${$minWidth};`}
${({ $opacity }) => $opacity && `opacity: ${$opacity};`}
${({ $overflow }) => $overflow && `overflow: ${$overflow};`}
${({ $padding }) => $padding && stylesPadding($padding)}
${({ $position }) => $position && `position: ${$position};`}
${({ $radius }) => $radius && `border-radius: ${$radius};`}
${({ $shrink }) => $shrink && `flex-shrink: ${$shrink};`}
${({
$layer = 'border',
$theme = 'brand',
$variation = 'primary',
$scope = 'semantic',
$border,
$withThemeBorder,
$withThemeInherited,
}) => {
if ($border) {
return `border: ${$border};`;
}
if (!$layer || !$scope || !$theme || !$withThemeBorder) {
return '';
}
if ($withThemeInherited) {
return `border: inherit;`;
}
return `border: 1px solid var(--c--contextuals--${$layer}--${$scope}${$theme ? `--${$theme}` : ''}${$variation ? `--${$variation}` : ''});`;
}}
${({
$layer = 'background',
$theme = 'brand',
$variation = 'primary',
$scope = 'semantic',
$background,
$withThemeBG,
$withThemeInherited,
}) => {
if ($background) {
return `background: ${$background};`;
}
if (!$layer || !$scope || !$theme || !$withThemeBG) {
return '';
}
if ($withThemeInherited) {
return `background: inherit;`;
}
return `background: var(--c--contextuals--${$layer}--${$scope}${$theme ? `--${$theme}` : ''}${$variation ? `--${$variation}` : ''});`;
}}
${({
$layer = 'content',
$theme = 'neutral',
$variation = 'primary',
$scope = 'semantic',
$color,
$withThemeBG,
$withThemeInherited,
}) => {
if ($color) {
return `color: ${$color};`;
}
if (!$layer || !$scope) {
return '';
}
// There is a special case when primary with background
if (
$withThemeBG &&
$layer === 'content' &&
$scope === 'semantic' &&
$variation === 'primary' &&
$theme
) {
$variation = `on-${$theme}`;
}
if ($withThemeInherited) {
return `color: inherit;`;
}
return `color: var(--c--contextuals--${$layer}--${$scope}${$theme ? `--${$theme}` : ''}${$variation ? `--${$variation}` : ''});`;
}}
${({ $transition }) => $transition && `transition: ${$transition};`}
${({ $width }) => $width && `width: ${$width};`}
${({ $wrap }) => $wrap && `flex-wrap: ${$wrap};`}
@@ -89,7 +189,7 @@ export const Box = styled('div')<BoxProps>`
return (
effect &&
`
transition: all 0.3s ease-in-out;
transition: all var(--c--globals--transitions--duration) var(--c--globals--transitions--ease-out);
${effect}
`
);
@@ -39,7 +39,7 @@ const BoxButton = forwardRef<HTMLDivElement, BoxButtonType>(
font-family: inherit;
color: ${props.disabled
? 'var(--c--theme--colors--greyscale-400) !important'
? 'var(--c--theme--colors--gray-400) !important'
: 'inherit'};
${$css || ''}
`}
@@ -12,7 +12,7 @@ export const Card = ({ children, ...props }: PropsWithChildren<BoxType>) => {
$background="white"
$radius="4px"
$css={`
border: 1px solid ${colorsTokens()['greyscale-050']};
border: 1px solid ${colorsTokens()['gray-050']};
`}
{...props}
>
@@ -40,6 +40,8 @@ interface DropButtonProps {
button: ReactNode;
isOpen?: boolean;
onOpenChange?: (isOpen: boolean) => void;
/** Accessible name for the trigger button (recommended for screen readers). */
ariaLabel?: string;
}
export const DropButton = ({
@@ -47,6 +49,7 @@ export const DropButton = ({
isOpen = false,
onOpenChange,
children,
ariaLabel,
}: PropsWithChildren<DropButtonProps>) => {
const [opacity, setOpacity] = useState(false);
const [isLocalOpen, setIsLocalOpen] = useState(isOpen);
@@ -75,7 +78,7 @@ export const DropButton = ({
<>
<GlobalStyle />
<DialogTrigger onOpenChange={onOpenChangeHandler} isOpen={isLocalOpen}>
<StyledButton>{button}</StyledButton>
<StyledButton aria-label={ariaLabel}>{button}</StyledButton>
<StyledPopover
style={{ opacity: opacity ? 1 : 0 }}
isOpen={isLocalOpen}
+8 -33
View File
@@ -1,41 +1,17 @@
import clsx from 'clsx';
import { css } from 'styled-components';
import { Text, TextType } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
type IconProps = TextType & {
iconName: string;
};
export const Icon = ({ iconName, ...textProps }: IconProps) => {
return (
<Text $isMaterialIcon {...textProps}>
{iconName}
</Text>
);
};
interface IconBGProps extends TextType {
iconName: string;
}
export const IconBG = ({ iconName, ...textProps }: IconBGProps) => {
const { colorsTokens } = useCunninghamTheme();
return (
<Text
$isMaterialIcon
$size="36px"
$theme="primary"
$variation="600"
$background={colorsTokens()['primary-050']}
$css={`
border: 1px solid ${colorsTokens()['primary-200']};
user-select: none;
`}
$radius="12px"
$padding="4px"
$margin="auto"
{...textProps}
aria-hidden="true"
className={clsx('material-icons', textProps.className)}
>
{iconName}
</Text>
@@ -48,16 +24,15 @@ type IconOptionsProps = TextType & {
export const IconOptions = ({ isHorizontal, ...props }: IconOptionsProps) => {
return (
<Text
<Icon
{...props}
$isMaterialIcon
$theme="primary"
aria-hidden="true"
iconName={isHorizontal ? 'more_horiz' : 'more_vert'}
$css={css`
color: var(--c--contextuals--content--semantic--brand--tertiary);
user-select: none;
${props.$css}
`}
>
{isHorizontal ? 'more_horiz' : 'more_vert'}
</Text>
/>
);
};
+24 -22
View File
@@ -12,32 +12,34 @@ export const Input = ({ label, error, required, ...props }: InputProps) => {
const { colorsTokens } = useCunninghamTheme();
return (
<Box $display="flex" $gap="4px">
<label
htmlFor={label}
style={{ fontWeight: 500, color: colorsTokens()['greyscale-900'] }}
>
{label} {required && '*'}
</label>
<Box>
<Box $display="flex" $gap="8px" $margin={{ bottom: 'sm' }}>
<label
htmlFor={label}
style={{ fontWeight: 500, color: colorsTokens()['gray-900'] }}
>
{label} {required && '*'}
</label>
<input
id={label}
aria-required={required}
required={required}
style={{
padding: '12px',
borderRadius: '4px',
fontSize: '14px',
border: `1px solid ${error ? colorsTokens()['error-500'] : 'var(--c--contextuals--border--semantic--neutral--tertiary)'}`,
background: colorsTokens()['gray-000'],
color: colorsTokens()['gray-550'],
}}
{...props}
/>
</Box>
{error && (
<Text $size="xs" $theme="danger" $variation="600">
<Text $size="xs" $theme="error" $variation="secondary">
{error}
</Text>
)}
<input
id={label}
aria-required={required}
required={required}
style={{
padding: '12px',
borderRadius: '4px',
fontSize: '14px',
border: `1px solid ${error ? colorsTokens()['danger-500'] : colorsTokens()['greyscale-400']}`,
background: colorsTokens()['greyscale-050'],
color: colorsTokens()['greyscale-900'],
}}
{...props}
/>
</Box>
);
};
@@ -0,0 +1,89 @@
import React from 'react';
import { Box, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
export type LocalInputProps = React.InputHTMLAttributes<HTMLInputElement> & {
label: string;
right: React.ReactNode;
error?: string;
required?: boolean;
id: string;
};
export const LocalInput = ({
label,
right,
error,
required,
id,
...inputProps
}: LocalInputProps) => {
const { colorsTokens } = useCunninghamTheme();
const borderColor = error
? colorsTokens()['error-500']
: 'var(--c--contextuals--border--semantic--neutral--tertiary)';
return (
<Box>
<Box $margin={{ bottom: 'sm' }}>
<label
htmlFor={id}
style={{ fontWeight: 500, color: colorsTokens()['gray-900'] }}
>
{label} {required && '*'}
</label>
</Box>
<div
style={{
display: 'flex',
flexDirection: 'row',
flexWrap: 'nowrap',
alignItems: 'stretch',
border: `1px solid ${borderColor}`,
borderRadius: 4,
overflow: 'hidden',
background: colorsTokens()['gray-000'],
}}
>
<input
id={id}
aria-required={required}
required={required}
style={{
flex: '1 1 0',
minWidth: 0,
padding: '12px',
border: 'none',
borderRadius: 0,
fontSize: 14,
background: 'transparent',
color: colorsTokens()['gray-550'],
outline: 'none',
}}
{...inputProps}
/>
<div
style={{
display: 'inline-flex',
alignItems: 'center',
flexShrink: 0,
background: 'transparent',
}}
>
{right}
</div>
</div>
{error && (
<Text
$size="xs"
$theme="error"
$variation="secondary"
$margin={{ top: '2xs' }}
>
{error}
</Text>
)}
</Box>
);
};
@@ -1,7 +1,7 @@
import {
Modal as CunninghamModal,
ModalProps,
} from '@openfun/cunningham-react';
} from '@gouvfr-lasuite/cunningham-react';
import React, { useEffect } from 'react';
// Define a wrapper component that extends ModalProps to accept the same props as the Modal
+14 -6
View File
@@ -1,4 +1,4 @@
import { Tooltip } from '@openfun/cunningham-react';
import { Tooltip } from '@gouvfr-lasuite/cunningham-react';
import { useTranslation } from 'react-i18next';
import { Box, Icon } from '@/components';
@@ -16,20 +16,28 @@ const TagContent = ({ status, showIcon }: TagContentProps) => {
const { colorsTokens } = useCunninghamTheme();
const { t } = useTranslation();
const translatedStatus = {
pending: t('pending'),
enabled: t('enabled'),
disabled: t('disabled'),
action_required: t('action required'),
failed: t('failed'),
};
const textColor = {
pending: colorsTokens()['info-600'],
enabled: colorsTokens()['success-600'],
disabled: colorsTokens()['greyscale-600'],
disabled: colorsTokens()['gray-600'],
action_required: colorsTokens()['warning-600'],
failed: colorsTokens()['danger-600'],
failed: colorsTokens()['error-600'],
};
const backgroundColor = {
pending: colorsTokens()['info-100'],
enabled: colorsTokens()['success-100'],
disabled: colorsTokens()['greyscale-100'],
disabled: colorsTokens()['gray-100'],
action_required: colorsTokens()['warning-100'],
failed: colorsTokens()['danger-100'],
failed: colorsTokens()['error-100'],
};
return (
@@ -46,7 +54,7 @@ const TagContent = ({ status, showIcon }: TagContentProps) => {
text-transform: capitalize;
`}
>
{t(status).replace('_', ' ')}
{translatedStatus[status]}
{showIcon &&
(status === 'enabled' ? (
<Icon
+10 -44
View File
@@ -5,70 +5,36 @@ import { tokens } from '@/cunningham';
import { Box, BoxProps } from './Box';
const { sizes } = tokens.themes.default.theme.font;
const { sizes } = tokens.themes.default.globals.font;
type TextSizes = keyof typeof sizes;
export interface TextProps extends BoxProps {
as?: keyof Pick<
HTMLElementTagNameMap,
'p' | 'span' | 'div' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
>;
$elipsis?: boolean;
$isMaterialIcon?: boolean;
as?: 'p' | 'span' | 'div' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
$ellipsis?: boolean;
$weight?: CSSProperties['fontWeight'];
$textAlign?: CSSProperties['textAlign'];
$textTransform?: CSSProperties['textTransform'];
$size?: TextSizes | (string & {});
$theme?:
| 'primary'
| 'secondary'
| 'info'
| 'success'
| 'warning'
| 'danger'
| 'greyscale';
$variation?:
| 'text'
| '000'
| '100'
| '200'
| '300'
| '400'
| '500'
| '600'
| '700'
| '800'
| '900'
| '1000';
}
export type TextType = ComponentPropsWithRef<typeof Text>;
export const TextStyled = styled(Box)<TextProps>`
${({ $textAlign }) => $textAlign && `text-align: ${$textAlign};`}
${({ $textTransform }) =>
$textTransform && `text-transform: ${$textTransform};`}
${({ $weight }) => $weight && `font-weight: ${$weight};`}
${({ $size }) =>
$size &&
`font-size: ${$size in sizes ? sizes[$size as TextSizes] : $size};`}
${({ $theme, $variation }) =>
`color: var(--c--theme--colors--${$theme}-${$variation});`}
${({ $color }) => $color && `color: ${$color};`}
${({ $elipsis }) =>
$elipsis &&
${({ $ellipsis }) =>
$ellipsis &&
`white-space: nowrap; overflow: hidden; text-overflow: ellipsis;`}
`;
const Text = forwardRef<HTMLElement, ComponentPropsWithRef<typeof TextStyled>>(
({ className, $isMaterialIcon, ...props }, ref) => {
return (
<TextStyled
ref={ref}
as="span"
$theme="greyscale"
$variation="text"
className={`${className || ''}${$isMaterialIcon ? ' material-icons' : ''}`}
{...props}
/>
);
(props, ref) => {
return <TextStyled ref={ref} as="span" {...props} />;
},
);
@@ -5,6 +5,7 @@ export * from './Effect';
export * from './DropButton';
export * from './Icon';
export * from './Input';
export * from './LocalInput';
export * from './Link';
export * from './LogoGouv';
export * from './Tag';
@@ -1,7 +1,7 @@
import {
Modal as CunninghamModal,
ModalProps,
} from '@openfun/cunningham-react';
} from '@gouvfr-lasuite/cunningham-react';
import React, { useEffect } from 'react';
import { HorizontalSeparator } from '@/components';
@@ -33,6 +33,8 @@ export const CustomModal: React.FC<
<HorizontalSeparator $withPadding={true}></HorizontalSeparator>
<div>
{children}
<HorizontalSeparator $withPadding={true}></HorizontalSeparator>
{/* modal content */}
{/*<div className={style.content}>{children}</div>*/}
@@ -23,11 +23,16 @@
border: none;
font-size: 34px;
cursor: pointer;
color: var(--c--theme--colors--greyscale-700);
color: var(--c--theme--colors--gray-700);
}
.content {
padding: var(--c--theme--spacings--md);
padding: 0 !important;
}
.modalCustom .c__modal__content {
padding: 0 !important;
}
.c__modal__footer {
@@ -42,7 +47,7 @@
padding: 0 var(--c--theme--spacings--md);
padding-top: 0;
align-items: center;
border-top: 1px solid var(--c--theme--colors--greyscale-200);
border-top: 1px solid var(--c--theme--colors--gray-200);
}
.progressBar {
@@ -63,9 +68,9 @@
width: auto;
flex-grow: 1;
border-radius: 4px;
background-color: var(--c--theme--colors--primary-200);
background-color: var(--c--theme--colors--brand-200);
&.active {
background-color: var(--c--theme--colors--primary-800);
background-color: var(--c--theme--colors--brand-800);
}
}
@@ -26,7 +26,7 @@ export const HorizontalSeparator = ({
$background={
variant === SeparatorVariant.DARK
? '#e5e5e533'
: colorsTokens()['greyscale-100']
: colorsTokens()['gray-100']
}
/>
);
@@ -1,8 +1,6 @@
import { PropsWithChildren } from 'react';
import { css } from 'styled-components';
import { useCunninghamTheme } from '@/cunningham';
import { Box } from '../Box';
type Props = {
@@ -13,15 +11,14 @@ export const SeparatedSection = ({
showSeparator = true,
children,
}: PropsWithChildren<Props>) => {
const theme = useCunninghamTheme();
const colors = theme.colorsTokens();
return (
<Box
$css={css`
width: 100%;
${showSeparator &&
css`
border-bottom: 1px solid ${colors?.['greyscale-200']};
border-bottom: 1px solid
var(--c--contextuals--border--surface--primary);
`}
`}
>
@@ -1,54 +1,109 @@
import * as React from 'react';
import { ReactNode } from 'react';
import { Tab, TabList, TabPanel, Tabs } from 'react-aria-components';
import { Button } from '@gouvfr-lasuite/cunningham-react';
import clsx from 'clsx';
import React, { ReactNode, useState } from 'react';
import { Box } from '@/components';
import { Icon } from '@/components';
import style from './custom-tabs.module.scss';
type TabsOption = {
ariaLabel?: string;
export type TabData = {
id: string;
label: string;
iconName?: string;
id?: string;
icon?: ReactNode;
subtext?: string;
content: ReactNode;
};
type Props = {
tabs: TabsOption[];
export type TabsProps = {
theme?: 'brand' | 'neutral';
orientation?: 'horizontal' | 'vertical';
tabs: TabData[];
defaultSelectedTab?: string;
fullWidth?: boolean;
};
export const CustomTabs = ({ tabs }: Props) => {
export const CustomTabs = ({
orientation = 'horizontal',
theme = 'brand',
tabs,
defaultSelectedTab,
fullWidth = false,
}: TabsProps) => {
const [selectedTabId, setSelectedTabId] = useState(
defaultSelectedTab ?? tabs[0]?.id,
);
const activeTab =
tabs.find((tab) => tab.id === selectedTabId) ?? tabs[0] ?? null;
if (tabs.length === 0 || !activeTab) {
return null;
}
return (
<div className={style.customTabsContainer}>
<Tabs>
<TabList>
<div
className={clsx(style.root, {
[style.fullWidth]: fullWidth,
[style.neutral]: theme === 'neutral',
[style.orientationVertical]: orientation === 'vertical',
})}
>
<div
style={{
display: 'flex',
}}
>
<div
style={{
display: 'inline-flex',
width: 'auto',
flexDirection: orientation === 'horizontal' ? 'row' : 'column',
alignItems: 'center',
borderRadius: 8,
backgroundColor:
'var(--c--contextuals--background--surface--primary)',
backdropFilter: 'blur(10px)',
border: '1px solid var(--c--contextuals--border--surface--primary)',
padding: 4,
boxShadow: 'rgba(0, 0, 0, 0.05) 0px 2px 4px 0px',
gap: 4,
}}
>
{tabs.map((tab) => {
const id = tab.id ?? tab.label;
const isSelected = tab.id === activeTab?.id;
return (
<Tab key={id} aria-label={tab.ariaLabel} id={id}>
<Box $direction="row" $gap="5px">
{tab.iconName && (
<span className="material-icons" aria-hidden="true">
{tab.iconName}
<Button
key={tab.id}
onClick={() => setSelectedTabId(tab.id)}
variant={isSelected ? 'secondary' : 'tertiary'}
color={isSelected ? 'brand' : 'neutral'}
size="small"
aria-pressed={isSelected}
icon={
<Icon
iconName={tab.iconName ?? ''}
$color={isSelected ? 'brand' : 'neutral'}
/>
}
>
{tab.icon && !tab.iconName && tab.icon}
<span>
<span className="react-aria-Tab__title">{tab.label}</span>
{tab.subtext && orientation === 'vertical' && (
<span className="react-aria-Tab__subtext">
{tab.subtext}
</span>
)}
{tab.label}
</Box>
</Tab>
</span>
</Button>
);
})}
</TabList>
{tabs.map((tab) => {
const id = tab.id ?? tab.label;
return (
<TabPanel key={id} id={id}>
{tab.content}
</TabPanel>
);
})}
</Tabs>
</div>
</div>
{activeTab && (
<div style={{ marginTop: 8, width: '100%' }}>{activeTab.content}</div>
)}
</div>
);
};
@@ -1,12 +1,18 @@
.customTabsContainer {
display: flex;
width: 100%;
margin-top: 30px;
.root {
&.fullWidth {
:global(.react-aria-Tabs) {
width: 100%;
}
:global(.react-aria-Tab) {
flex: 1;
display: flex;
justify-content: center;
}
}
:global(.react-aria-Tabs) {
width: 100%;
display: flex;
flex: 1;
margin: auto;
&[data-orientation='horizontal'] {
flex-direction: column;
@@ -15,36 +21,39 @@
:global(.react-aria-TabList) {
display: flex;
width: 100%;
gap: 25px;
}
:global(.react-aria-Tab) {
display: flex;
padding: 0 10px 10px 10px;
padding: 10px;
cursor: pointer;
font-weight: 500;
max-width: 300px;
color: var(--c--contextuals--content--semantic--brand--tertiary);
outline: none;
position: relative;
color: var(--c--theme--colors--secondary-900);
transition: color 200ms;
--border-color: transparent;
forced-color-adjust: none;
display: flex;
align-items: center;
gap: var(--c--globals--spacings--sm);
border-bottom: 1px solid var(--c--contextuals--border--surface--primary);
&[data-hovered] {
background-color: #fff;
color: var(--text-color-hover);
background-color: var(
--c--contextuals--background--semantic--overlay--primary
);
}
&[data-selected] {
font-weight: 500;
color: var(--c--theme--colors--primary-text);
border-bottom: 2px solid var(--c--theme--colors--primary-text);
padding-bottom: 9px;
border-bottom: 2px solid
var(--c--contextuals--content--semantic--brand--tertiary);
}
&[data-disabled] {
color: var(--text-color-disabled);
color: var(--c--theme--colors--gray-400);
&[data-selected] {
--border-color: var(--text-color-disabled);
--border-color: var(--c--theme--colors--gray-400);
}
}
@@ -53,17 +62,72 @@
position: absolute;
inset: 4px;
border-radius: 4px;
border: 2px solid var(--focus-ring-color);
border: 2px solid var(--c--theme--colors--brand-600);
}
}
:global(.react-aria-TabPanel) {
margin-top: 15px;
margin-top: 4px;
padding: 10px;
border-radius: 4px;
outline: none;
&[data-focus-visible] {
outline: 2px solid var(--focus-ring-color);
outline: 2px solid var(--c--theme--colors--brand-600);
}
}
&.neutral {
:global(.react-aria-Tab) {
color: var(--c--contextuals--content--semantic--neutral--tertiary);
&[data-selected] {
border-color: var(
--c--contextuals--content--semantic--neutral--tertiary
);
}
}
}
&.orientationVertical {
:global(.react-aria-Tabs) {
width: 100%;
}
:global(.react-aria-TabList) {
width: 100%;
flex-direction: column;
gap: 10px;
}
:global(.react-aria-Tab) {
color: var(--c--contextuals--content--semantic--neutral--primary);
border-radius: 4px;
border: none !important;
font-weight: 500;
letter-spacing: -0.01em;
font-size: var(--c--globals--font--sizes--sm);
line-height: var(--c--globals--font--sizes--md);
&[data-hovered] {
background-color: var(
--c--contextuals--background--semantic--overlay--primary
);
}
:global(.react-aria-Tab__subtext) {
display: block;
font-weight: 400;
line-height: var(--c--globals--font--sizes--md);
font-size: var(--c--globals--font--sizes--xs);
color: var(--c--contextuals--content--semantic--neutral--secondary);
}
&[data-selected] {
font-weight: 700;
padding-bottom: 10px;
background-color: var(
--c--contextuals--background--semantic--overlay--primary
);
border: none;
}
}
}
}
@@ -1,4 +1,4 @@
import { CunninghamProvider } from '@openfun/cunningham-react';
import { CunninghamProvider } from '@gouvfr-lasuite/ui-kit';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
@@ -27,13 +27,13 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
const { theme } = useCunninghamTheme();
return (
<QueryClientProvider client={queryClient}>
<ReactQueryDevtools />
<CunninghamProvider theme={theme}>
<CunninghamProvider theme={theme}>
<QueryClientProvider client={queryClient}>
<ReactQueryDevtools />
<ConfigProvider>
<Auth>{children}</Auth>
</ConfigProvider>
</CunninghamProvider>
</QueryClientProvider>
</QueryClientProvider>
</CunninghamProvider>
);
}
@@ -1,4 +1,4 @@
import { Loader } from '@openfun/cunningham-react';
import { Loader } from '@gouvfr-lasuite/cunningham-react';
import { useRouter } from 'next/router';
import { PropsWithChildren, useEffect } from 'react';
@@ -1,4 +1,4 @@
import { Button } from '@openfun/cunningham-react';
import { Button } from '@gouvfr-lasuite/cunningham-react';
import { useTranslation } from 'react-i18next';
import { useAuthStore } from '@/core/auth';
@@ -8,7 +8,12 @@ export const ButtonLogin = () => {
const { logout } = useAuthStore();
return (
<Button onClick={logout} color="primary-text" aria-label={t('Logout')}>
<Button
onClick={logout}
color="brand"
variant="tertiary"
aria-label={t('Logout')}
>
{t('Logout')}
</Button>
);
@@ -1,20 +1,46 @@
import { Button } from '@gouvfr-lasuite/cunningham-react';
import { UserMenu } from '@gouvfr-lasuite/ui-kit';
import { useTranslation } from 'react-i18next';
import { Box, Text } from '@/components';
import { baseApiUrl } from '@/api';
import { useAuthStore } from '@/core/auth';
import { LanguagePicker } from '@/features/language';
interface UserMenuUser {
email: string;
full_name: string;
organization?: string;
}
export const UserInfo = () => {
const { t } = useTranslation();
const { authenticated, userData } = useAuthStore();
const userName = userData?.name || userData?.email || t('No Username');
const organizationName = userData?.organization?.name || '';
const { authenticated, userData, logout } = useAuthStore();
const user: UserMenuUser = {
email: userData?.email ?? '',
full_name: userData?.name ?? '',
organization:
typeof userData?.organization === 'string'
? userData.organization
: (userData?.organization?.name ?? ''),
};
return authenticated ? (
<Box $direction="column" $align="left">
<Text $theme="primary">
{userName}
{organizationName ? ` | ${organizationName}` : ''}
</Text>
</Box>
) : null;
if (!authenticated) {
return null;
}
if (!userData) {
return (
<Button
color="brand"
variant="tertiary"
onClick={() =>
window.location.assign(new URL('authenticate/', baseApiUrl()).href)
}
>
{t('Sign in')}
</Button>
);
}
return <UserMenu user={user} logout={logout} actions={<LanguagePicker />} />;
};
@@ -1,4 +1,4 @@
import { Loader } from '@openfun/cunningham-react';
import { Loader } from '@gouvfr-lasuite/cunningham-react';
import { PropsWithChildren, useEffect } from 'react';
import { Box } from '@/components';
@@ -23,7 +23,7 @@
* Select
**/
--c--components--forms-select--value-color--disabled: var(
--c--theme--colors--greyscale-400
--c--theme--colors--gray-400
);
/**
@@ -1,6 +1,6 @@
@import url('@openfun/cunningham-react/icons');
@import url('@openfun/cunningham-react/style');
@import url('@openfun/cunningham-react/fonts');
@import url('@gouvfr-lasuite/cunningham-react/icons');
@import url('@gouvfr-lasuite/cunningham-react/style');
@import url('@gouvfr-lasuite/cunningham-react/fonts');
@import url('./cunningham-tokens.css');
@import url('./cunningham-custom-tokens.css');
@import url('../assets/fonts/Marianne/Marianne-font.css');
@@ -9,7 +9,7 @@
.c__field,
.c__select,
.c__datagrid {
font-family: var(--c--theme--font--families--base);
font-family: var(--c--globals--font--families--base);
}
.c__field {
@@ -24,16 +24,16 @@
}
.labelled-box label {
color: var(--c--theme--colors--greyscale-500);
color: var(--c--contextuals--content--semantic--neutral--primary);
top: 1px;
}
.labelled-box--disabled label {
color: var(--c--components--forms-labelledbox--label-color--small-disabled);
color: var(--c--contextuals--content--semantic--neutral--tertiary);
}
.labelled-box label.placeholder {
top: 12px;
top: 11px;
}
.c__field :not(.c__textarea__wrapper, div) .labelled-box label.placeholder {
@@ -48,11 +48,10 @@
.c__input__wrapper,
.c__textarea__wrapper {
width: 100%;
transition: all var(--c--theme--transitions--duration)
var(--c--theme--transitions--ease-out);
transition: all var(--c--globals--transitions--duration)
var(--c--globals--transitions--ease-out);
gap: 0.5rem;
color: var(--c--theme--colors--greyscale-600);
background-color: var(--c--theme--colors--greyscale-050);
color: var(--c--globals--colors--gray-600);
}
@media screen and (width <= 768px) {
@@ -93,7 +92,7 @@
}
.c__input__wrapper--disabled .c__input {
color: var(--c--components--forms-input--value-color--disabled);
color: var(--c--contextuals--content--semantic--neutral--tertiary);
}
.c__input__wrapper--error .c__input {
@@ -148,8 +147,8 @@ input:-webkit-autofill:focus {
}
.c__select__wrapper {
transition: all var(--c--theme--transitions--duration)
var(--c--theme--transitions--ease-out);
transition: all var(--c--globals--transitions--duration)
var(--c--globals--transitions--ease-out);
min-height: var(--c--components--forms-select--height);
height: auto;
}
@@ -174,8 +173,8 @@ input:-webkit-autofill:focus {
}
.c__select__menu__item {
transition: all var(--c--theme--transitions--duration)
var(--c--theme--transitions--ease-out);
transition: all var(--c--globals--transitions--duration)
var(--c--globals--transitions--ease-out);
}
.c__select--disabled .c__select__wrapper label,
@@ -239,17 +238,17 @@ input:-webkit-autofill:focus {
.c__datagrid__table__container > table tbody tr {
border: none;
border-top: 1px var(--c--theme--colors--greyscale-100) solid;
border-bottom: 1px var(--c--theme--colors--greyscale-100) solid;
border-top: 1px solid var(--c--contextuals--border--surface--primary);
border-bottom: 1px solid var(--c--contextuals--border--surface--primary);
}
.c__datagrid__table__container > table tbody tr:hover {
background-color: var(--c--theme--colors--greyscale-050);
background-color: var(--c--contextuals--background--surface--secondary);
}
.c__datagrid__table__container > table tbody {
background-color: var(--c--components--datagrid--body--background-color);
color: var(--c--theme--colors--greyscale-900);
color: var(--c--globals--colors--gray-900);
}
.c__datagrid__table__container > table {
@@ -286,7 +285,7 @@ input:-webkit-autofill:focus {
background-color: var(
--c--components--datagrid--pagination--background-color-active
);
color: var(--c--theme--colors--greyscale-800);
color: var(--c--globals--colors--gray-800);
}
.c__pagination__list .c__button--tertiary-text:disabled {
@@ -309,22 +308,22 @@ input:-webkit-autofill:focus {
}
.c__date-picker__wrapper {
transition: all var(--c--theme--transitions--duration)
var(--c--theme--transitions--ease-out);
transition: all var(--c--globals--transitions--duration)
var(--c--globals--transitions--ease-out);
}
.c__date-picker:not(.c__date-picker--disabled):hover .c__date-picker__wrapper {
box-shadow: var(--c--theme--colors--primary-500) 0 0 0 2px;
box-shadow: var(--c--globals--colors--primary-500) 0 0 0 2px;
}
.c__date-picker.c__date-picker--invalid:not(.c__date-picker--disabled):hover
.c__date-picker__wrapper {
box-shadow: var(--c--theme--colors--danger-300) 0 0 0 2px;
box-shadow: var(--c--globals--colors--danger-300) 0 0 0 2px;
}
.c__date-picker__wrapper button[aria-label='Clear date'],
.c__date-picker.c__date-picker--invalid .c__date-picker__wrapper * {
color: var(--c--theme--colors--danger-300);
color: var(--c--globals--colors--danger-300);
}
/**
@@ -345,7 +344,7 @@ input:-webkit-autofill:focus {
}
.c__checkbox.c__checkbox--disabled .c__field__text {
color: var(--c--theme--colors--greyscale-600);
color: var(--c--globals--colors--gray-600);
}
.c__switch.c__checkbox--disabled .c__switch__rail {
@@ -353,11 +352,11 @@ input:-webkit-autofill:focus {
}
.c__checkbox.c__checkbox--disabled .c__checkbox__label {
color: var(--c--theme--colors--greyscale-400);
color: var(--c--globals--colors--gray-400);
}
.c__radio input::before {
box-shadow: inset 1em 1em var(--c--theme--colors--primary-600);
box-shadow: inset 1em 1em var(--c--globals--colors--primary-600);
}
/**
@@ -551,8 +550,8 @@ input:-webkit-autofill:focus {
}
.c__button--with-icon--right {
padding: 0.7rem var(--c--theme--spacings--t) 0.7rem
var(--c--theme--spacings--s);
padding: 0.7rem var(--c--globals--spacings--t) 0.7rem
var(--c--globals--spacings--s);
}
/**
@@ -567,37 +566,28 @@ input:-webkit-autofill:focus {
box-shadow: none;
}
.c__modal__close button {
padding: 0;
font-size: 88px;
width: 28px !important;
height: 28px;
}
.c__modal__close button .material-icons {
padding: 0;
font-size: 24px;
color: var(--c--theme--colors--greyscale-600);
}
.c__modal__close .c__button {
padding: 0 !important;
}
.c__modal--full .c__modal__content {
overflow-y: auto;
}
.c__modal__content {
color: var(--c--theme--colors--greyscale-600) !important;
font-size: var(--c--globals--font--sizes--sm);
color: var(
--c--contextuals--content--semantic--neutral--secondary
) !important;
}
.c__modal__content__margin {
padding: var(--c--globals--spacings--md);
padding-bottom: 0;
}
.c__modal__title {
padding: var(--c--theme--spacings--md);
padding: var(--c--globals--spacings--md);
padding-bottom: 0;
margin-bottom: 0;
text-align: left;
font-size: 1.125rem;
font-size: var(--c--globals--font--sizes--lg) !important;
}
@media screen and (width <= 420px) {
@@ -614,7 +604,7 @@ input:-webkit-autofill:focus {
.c__modal__footer {
margin-top: 0;
background-color: transparent !important;
padding: var(--c--theme--spacings--md);
padding: var(--c--globals--spacings--md);
}
@media screen and (width <= 576px) {
@@ -638,8 +628,8 @@ input:-webkit-autofill:focus {
}
.c__modal__title {
font-size: var(--c--theme--font--sizes--xs);
padding: var(--c--theme--spacings--base);
font-size: var(--c--globals--font--sizes--xs);
padding: var(--c--globals--spacings--base);
margin-bottom: 0;
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -3,17 +3,27 @@ import { create } from 'zustand';
import { tokens } from './cunningham-tokens';
type Tokens = typeof tokens.themes.default;
type ColorsTokens = Tokens['theme']['colors'];
type FontSizesTokens = Tokens['theme']['font']['sizes'];
type SpacingsTokens = Tokens['theme']['spacings'];
type ComponentTokens = Tokens['components'];
type Tokens = typeof tokens.themes.default &
Partial<(typeof tokens.themes)[keyof typeof tokens.themes]>;
type ColorsTokens = Tokens['globals']['colors'];
type FontSizesTokens = Tokens['globals']['font']['sizes'];
type SpacingsTokens = Tokens['globals']['spacings'];
type ComponentTokens = Partial<
| (Tokens['components'] & Tokens['globals']['components'])
| Record<string, unknown>
> &
Record<string, unknown>;
export const removeQuotes = (str: string) => {
return str.replace(/^['"]|['"]$/g, '');
};
export type Theme = keyof typeof tokens.themes;
interface AuthStore {
theme: string;
setTheme: (theme: Theme) => void;
themeTokens: () => Partial<Tokens['theme']>;
themeTokens: () => Partial<Tokens['globals']>;
colorsTokens: () => Partial<ColorsTokens>;
fontSizesTokens: () => Partial<FontSizesTokens>;
spacingsTokens: () => Partial<SpacingsTokens>;
@@ -29,11 +39,11 @@ export const useCunninghamTheme = create<AuthStore>((set, get) => {
return {
theme: 'dsfr',
themeTokens: () => currentTheme().theme,
colorsTokens: () => currentTheme().theme.colors,
themeTokens: () => currentTheme().globals,
colorsTokens: () => currentTheme().globals?.colors ?? {},
componentTokens: () => currentTheme().components,
spacingsTokens: () => currentTheme().theme.spacings,
fontSizesTokens: () => currentTheme().theme.font.sizes,
spacingsTokens: () => currentTheme().globals?.spacings ?? {},
fontSizesTokens: () => currentTheme().globals?.font?.sizes ?? {},
setTheme: (theme: Theme) => {
set({ theme });
},
@@ -13,7 +13,7 @@ const BlueStripe = styled.div`
position: absolute;
height: 2px;
width: 100%;
background: var(--c--theme--colors--primary-600);
background: var(--c--theme--colors--brand-600);
top: 0;
`;
@@ -82,7 +82,7 @@ export const Footer = () => {
gap:0.2rem;
transition: box-shadow 0.3s;
&:hover {
box-shadow: 0px 2px 0 0 var(--c--theme--colors--greyscale-text);
box-shadow: 0px 2px 0 0 var(--c--theme--colors--gray-text);
}
`}
>
@@ -98,7 +98,7 @@ export const Footer = () => {
$padding={{ top: 'tiny' }}
$css="
flex-wrap: wrap;
border-top: 1px solid var(--c--theme--colors--greyscale-200);
border-top: 1px solid var(--c--theme--colors--gray-200);
column-gap: 1rem;
row-gap: .5rem;
"
@@ -1,116 +1,84 @@
import { Button } from '@openfun/cunningham-react';
import Image from 'next/image';
import { useTranslation } from 'react-i18next';
import { default as IconRegie } from '@/assets/logo-regie.svg?url';
import { Icon, StyledLink, Text } from '@/components/';
import { ButtonLogin } from '@/core/auth';
import { StyledLink } from '@/components/';
import { UserInfo } from '@/core/auth/UserInfo';
import { useCunninghamTheme } from '@/cunningham';
import { LanguagePicker } from '@/features/language';
import { useLeftPanelStore } from '@/features/left-panel';
import { TogglePanelButton } from '@/features/left-panel/components/TogglePanelButton';
import { LaGaufre } from './LaGaufre';
export const HEADER_HEIGHT = '52px';
export const Header = () => {
const { t } = useTranslation();
const theme = useCunninghamTheme();
const { isPanelOpen, togglePanel } = useLeftPanelStore();
const spacings = theme.spacingsTokens();
const colors = theme.colorsTokens();
const { isPanelOpen } = useLeftPanelStore();
return (
<header
className=""
style={{
display: 'flex',
position: 'fixed',
top: 0,
position: 'sticky',
top: '0px',
left: 0,
right: 0,
zIndex: 1000,
flexDirection: 'row',
padding: '10px',
alignItems: 'center',
justifyContent: 'space-between',
height: 'var(--header-height)',
minHeight: 'var(--header-height)',
padding: `0 ${spacings['base']}`,
backgroundColor: colors['greyscale-000'],
borderBottom: `1px solid ${colors['greyscale-200']}`,
background:
'linear-gradient(180deg, rgba(255, 255, 255, 0.75) 0%, rgba(255, 255, 255, 0.00) 100%)',
backdropFilter: 'blur(1px)',
}}
>
<Button
className="md:hidden"
size="medium"
onClick={() => togglePanel()}
aria-label={t('Open the header menu')}
color="tertiary-text"
icon={
<Icon
$variation="800"
$theme="primary"
iconName={isPanelOpen ? 'close' : 'menu'}
/>
}
/>
<StyledLink href="/">
<div
style={{
display: 'flex',
alignItems: 'center',
borderRadius: '8px',
backgroundColor:
'var(--c--contextuals--background--surface--primary)',
backdropFilter: 'blur(10px)',
border: '1px solid var(--c--contextuals--border--surface--primary)',
padding: '4px',
flexDirection: 'row',
boxShadow: '0 2px 4px 0 rgba(0, 0, 0, 0.05)',
}}
>
<TogglePanelButton />
<div
style={{
display: 'flex',
alignItems: 'center',
gap: spacings['3xs'],
overflow: 'hidden',
maxWidth: isPanelOpen ? '0' : '120px',
opacity: isPanelOpen ? '0' : '1',
transition: 'max-width 0.3s ease, opacity 0.2s ease 0.3s',
}}
>
<Image priority src={IconRegie} alt={t('La régie Logo')} width={25} />
<div
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
gap: spacings['xs'],
}}
>
<Text
as="h2"
style={{ color: '#000091', zIndex: 1, fontSize: '1.30rem' }}
>
{t('La Régie')}
</Text>
<Text
style={{
padding: '1px 8px',
fontSize: '11px',
fontWeight: 'bold',
color: colors['primary-500'],
borderRadius: '12px',
backgroundColor: colors['primary-200'],
}}
>
BETA
</Text>
</div>
</div>
</StyledLink>
<div className="md:hidden">
<div
style={{ display: 'flex', flexDirection: 'row', gap: spacings['sm'] }}
>
<LaGaufre />
<StyledLink href="/">
<div style={{ display: 'flex', alignItems: 'center' }}>
<Image priority src={IconRegie} alt="Régie Logo" height={34} />
</div>
</StyledLink>
</div>
</div>
<div className="hidden md:block">
<div className="">
<div
style={{
display: 'flex',
display: 'inline-flex',
alignItems: 'center',
gap: spacings['sm'],
borderRadius: '8px',
backgroundColor:
'var(--c--contextuals--background--surface--primary)',
border: '1px solid var(--c--contextuals--border--surface--primary)',
padding: '4px',
gap: '4px',
flexDirection: 'row',
boxShadow: '0 2px 4px 0 rgba(0, 0, 0, 0.05)',
}}
>
<UserInfo />
<ButtonLogin />
<LanguagePicker />
<LaGaufre />
</div>
</div>
@@ -1,25 +1,114 @@
import { Gaufre } from '@gouvfr-lasuite/integration';
import '@gouvfr-lasuite/integration/dist/css/gaufre.css';
// import { Gaufre } from '@gouvfr-lasuite/integration';
import { Button } from '@gouvfr-lasuite/cunningham-react';
import Script from 'next/script';
import React from 'react';
import { createGlobalStyle } from 'styled-components';
import { useEffect } from 'react';
const GaufreStyle = createGlobalStyle`
.lasuite-gaufre-btn{
box-shadow: inset 0 0 0 0 !important;
declare global {
interface Window {
_lasuite_widget?: unknown[];
}
`;
}
export const LaGaufre = () => {
useEffect(() => {
const wrapper = document.querySelector('[data-gaufre-button-wrapper]');
const button = wrapper?.querySelector('button') as HTMLButtonElement;
if (button && !button.id) {
button.id = 'gaufre_button';
button.setAttribute('aria-expanded', 'false');
}
const applyZIndex = () => {
const shadowHost = document.querySelector(
'#lasuite-widget-lagaufre-shadow',
);
const wrapper = shadowHost?.shadowRoot?.querySelector(
'#wrapper',
) as HTMLElement;
if (wrapper) {
wrapper.style.zIndex = '1000000000';
}
};
setTimeout(applyZIndex, 500);
}, []);
return (
<>
<div data-gaufre-button-wrapper>
<Button
variant="tertiary"
className="!w-10 !h-10 !p-0 !min-w-0"
aria-label="Les services de LaSuite"
aria-expanded="false"
>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<path
fill="var(--c--contextuals--content--semantic--brand--tertiary)"
id="square"
d="M2.7959 0.5C3.26483 0.5 3.49956 0.49985 3.68848 0.564453C4.03934 0.684581 4.31542 0.960658 4.43555 1.31152C4.50015 1.50044 4.5 1.73517 4.5 2.2041V2.7959C4.5 3.26483 4.50015 3.49956 4.43555 3.68848C4.31542 4.03934 4.03934 4.31542 3.68848 4.43555C3.49956 4.50015 3.26483 4.5 2.7959 4.5H2.2041C1.73517 4.5 1.50044 4.50015 1.31152 4.43555C0.960658 4.31542 0.684581 4.03934 0.564453 3.68848C0.49985 3.49956 0.5 3.26483 0.5 2.7959V2.2041C0.5 1.73517 0.49985 1.50044 0.564453 1.31152C0.684581 0.960658 0.960658 0.684581 1.31152 0.564453C1.50044 0.49985 1.73517 0.5 2.2041 0.5H2.7959Z"
/>
</defs>
<use href="#square" transform="translate(0, 0)" />
<use href="#square" transform="translate(6.5, 0)" />
<use href="#square" transform="translate(13, 0)" />
<use href="#square" transform="translate(0, 6.5)" />
<use href="#square" transform="translate(6.5, 6.5)" />
<use href="#square" transform="translate(13, 6.5)" />
<use href="#square" transform="translate(0, 13)" />
<use href="#square" transform="translate(6.5, 13)" />
<use href="#square" transform="translate(13, 13)" />
</svg>
</Button>
</div>
<Script
src="https://integration.lasuite.numerique.gouv.fr/api/v1/gaufre.js"
src="https://static.suite.anct.gouv.fr/widgets/lagaufre.js"
strategy="lazyOnload"
id="lasuite-gaufre-script"
onLoad={() => {
const button = document.getElementById('gaufre_button');
if (button) {
globalThis.window._lasuite_widget =
globalThis.window._lasuite_widget || [];
globalThis.window._lasuite_widget.push([
'lagaufre',
'init',
{
api: 'https://lasuite.numerique.gouv.fr/api/services',
label: 'Services de la Suite numérique',
closeLabel: 'Fermer le menu',
headerLabel: 'À propos',
backgroundColor: '#fff',
background:
'linear-gradient(#f1f2fd, rgba(255, 255, 255, 1) 20.54%, #FFF 0%',
headerLogo:
'https://lasuite.numerique.gouv.fr/assets/lasuite.svg',
headerUrl: 'https://lasuite.numerique.gouv.fr',
loadingText: 'Chargement…',
newWindowLabelSuffix: ' (nouvelle fenêtre)',
fontFamily: 'Marianne',
buttonElement: button,
viewMoreLabel: 'Voir plus',
viewLessLabel: 'Voir moins',
position: () => {
return {
backgroundColor: '#fff',
position: 'fixed',
top: 65,
right: 20,
};
},
},
]);
}
}}
/>
<GaufreStyle />
<Gaufre variant="small" />
</>
);
};
@@ -1,63 +1,29 @@
import { Select } from '@openfun/cunningham-react';
import { LanguagePicker as LanguagePickerUi } from '@gouvfr-lasuite/ui-kit';
import { Settings } from 'luxon';
import { useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import styled from 'styled-components';
import { Box, Text } from '@/components/';
import { LANGUAGES_ALLOWED } from '@/i18n/conf';
const SelectStyled = styled(Select)<{ $isSmall?: boolean }>`
flex-shrink: 0;
width: auto;
.c__select__wrapper {
min-height: 2rem;
height: auto;
border-color: transparent;
padding: 0 0.15rem 0 0.45rem;
border-radius: 1px;
.labelled-box .labelled-box__children {
padding-right: 2rem;
.c_select__render .typo-text {
${({ $isSmall }) => $isSmall && `display: none;`}
}
}
&:hover {
box-shadow: none !important;
}
}
`;
export const LanguagePicker = () => {
const { t, i18n } = useTranslation();
const { i18n } = useTranslation();
const { preload: languages } = i18n.options;
Settings.defaultLocale = i18n.language;
const optionsPicker = useMemo(() => {
return (languages || []).map((lang) => ({
value: lang,
label: lang,
label: LANGUAGES_ALLOWED[lang],
render: () => (
<Box
className="c_select__render"
$direction="row"
$gap="0.7rem"
$align="center"
$css="text-transform: uppercase;"
>
<Text
$isMaterialIcon
$size="1rem"
$theme="primary"
$weight="bold"
$variation="800"
>
translate
</Text>
<Text $theme="primary" $weight="500" $variation="800">
<Text $theme="brand" $weight="500" $variation="800">
{LANGUAGES_ALLOWED[lang]}
</Text>
</Box>
@@ -71,33 +37,33 @@ export const LanguagePicker = () => {
}
}, [i18n.language]);
/**
* @description prevent select div to receive focus on keyboard navigation so the focus goes directly to inner button
* @see https://github.com/suitenumerique/people/pull/379
*/
useEffect(() => {
if (!document) {
return;
}
document
.querySelector('.c__select-language-picker .c__select__wrapper')
?.setAttribute('tabindex', '-1');
}, []);
return (
<SelectStyled
label={t('Language')}
showLabelWhenSelected={false}
clearable={false}
hideLabel
defaultValue={i18n.language}
className="c_select__no_bg"
options={optionsPicker}
onChange={(e) => {
i18n.changeLanguage(e.target.value as string).catch((err) => {
<LanguagePickerUi
languages={optionsPicker}
size="small"
onChange={(selected) => {
type LanguageOption = { value?: string };
let code: string | undefined;
if (typeof selected === 'string') {
code = selected;
} else if (
typeof selected === 'object' &&
selected !== null &&
typeof (selected as LanguageOption).value === 'string'
) {
code = (selected as LanguageOption).value;
}
if (!code) {
return;
}
i18n.changeLanguage(code).catch((err) => {
console.error('Error changing language', err);
});
}}
compact
/>
);
};
@@ -1,16 +1,15 @@
import Image from 'next/image';
import { usePathname } from 'next/navigation';
import { useCallback, useEffect } from 'react';
import { createGlobalStyle } from 'styled-components';
import { default as IconRegie } from '@/assets/logo-regie.svg?url';
import { StyledLink } from '@/components/';
import { SeparatedSection } from '@/components/separators';
import { ButtonLogin } from '@/core';
import { UserInfo } from '@/core/auth/UserInfo';
import { LanguagePicker } from '@/features/language';
import { useLeftPanelStore } from '../stores';
import { LeftPanelContent } from './LeftPanelContent';
import { LeftPanelHeader } from './LeftPanelHeader';
import { LeftPanelItems } from './LeftPanelItems';
const MobileLeftPanelStyle = createGlobalStyle`
@@ -21,11 +20,15 @@ const MobileLeftPanelStyle = createGlobalStyle`
const LeftPanelStyle = createGlobalStyle`
.LeftPanel {
height: calc(100vh - var(--header-height));
position: sticky;
top: 0;
padding: 12px;
height: 100vh;
width: 300px;
min-width: 300px;
overflow: hidden;
border-right: 1px solid var(--c--theme--colors--greyscale-200']};
background-color: var(--c--contextuals--background--surface--tertiary);
border-right: 1px solid var(--c--contextuals--border--surface--primary);
}
`;
@@ -34,29 +37,63 @@ export const LeftPanel = () => {
const pathname = usePathname();
const toggle = useCallback(() => {
const closePanel = useCallback(() => {
togglePanel(false);
}, [togglePanel]);
useEffect(() => {
toggle();
}, [pathname, toggle]);
if (
typeof globalThis.window !== 'undefined' &&
globalThis.window.matchMedia('(min-width: 768px)').matches
) {
togglePanel(true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (
typeof globalThis.window !== 'undefined' &&
!globalThis.window.matchMedia('(min-width: 768px)').matches
) {
closePanel();
}
}, [pathname, closePanel]);
return (
<>
<div className="hidden md:block">
<div
className="hidden md:block"
style={{
width: isPanelOpen ? undefined : 0,
minWidth: isPanelOpen ? undefined : 0,
overflow: 'hidden',
transition: 'min-width 0.2s ease, width 0.2s ease',
}}
>
<LeftPanelStyle />
<div className="LeftPanel">
<div className="LeftPanel" style={{ minWidth: isPanelOpen ? 300 : 0 }}>
<div
style={{
flex: '0 0 auto',
}}
></div>
<StyledLink href="/">
<div
style={{
display: 'flex',
alignItems: 'center',
marginBottom: '1rem',
}}
>
<Image priority src={IconRegie} alt="Régie" height={40} />
</div>
</StyledLink>
<menu
style={{
margin: '0',
gap: 'var(--c--theme--spacings--xs)',
padding: '0 10px',
padding: '0',
}}
>
<LeftPanelItems />
@@ -71,11 +108,18 @@ export const LeftPanel = () => {
style={{
zIndex: '999',
width: '100vw',
height: 'calc(100dvh - var(--header-height))',
borderRight: '1px solid var(--c--theme--colors--greyscale-200)',
height: '100vh',
position: 'fixed',
inset: 0,
display: 'flex',
justifyContent: 'center',
alignItems: 'flex-start',
padding: '12px',
backgroundColor:
'var(--c--contextuals--background--surface--tertiary)',
transform: `translateX(${isPanelOpen ? '0' : '-100dvw'})`,
backgroundColor: 'var(--c--theme--colors--greyscale-000)',
transition: 'transform 0.2s ease-out',
overflowY: 'auto',
}}
className="md:hidden"
>
@@ -83,13 +127,17 @@ export const LeftPanel = () => {
data-testid="left-panel-mobile"
style={{
width: '100%',
padding: '0 10px',
justifyContent: 'center',
marginTop: '60px',
justifyContent: 'flex-start',
gap: 'var(--c--theme--spacings--base)',
}}
className="md:hidden"
>
<LeftPanelHeader />
<StyledLink href="/">
<div>
<Image priority src={IconRegie} alt="Régie" height={40} />
</div>
</StyledLink>
<LeftPanelItems />
<SeparatedSection showSeparator={false}>
<div
@@ -99,11 +147,7 @@ export const LeftPanel = () => {
flexDirection: 'column',
gap: 'var(--c--theme--spacings--sm)',
}}
>
<UserInfo />
<ButtonLogin />
<LanguagePicker />
</div>
></div>
</SeparatedSection>
</div>
</div>
@@ -1,57 +0,0 @@
import { Button } from '@openfun/cunningham-react';
import { useRouter } from 'next/navigation';
import { PropsWithChildren } from 'react';
import { Icon, SeparatedSection } from '@/components';
// import { useAuthStore } from '@/core/auth/useAuthStore';
import { useLeftPanelStore } from '../stores';
export const LeftPanelHeader = ({ children }: PropsWithChildren) => {
const router = useRouter();
// const auth = useAuthStore();
const { togglePanel } = useLeftPanelStore();
const goToHome = () => {
router.push('/');
togglePanel();
};
// const createNewDoc = () => {
// createDoc({ title: t('Untitled document') });
// };
return (
<>
<div>
<SeparatedSection>
<div>
<Button
onClick={goToHome}
size="medium"
color="tertiary-text"
icon={<Icon $variation="800" $theme="primary" iconName="house" />}
/>
{/* {auth.authenticated && (
<Button
onClick={searchModal.open}
size="medium"
color="tertiary-text"
icon={
<Icon $variation="800" $theme="primary" iconName="search" />
}
/>
)}*/}
</div>
{/* {auth.authenticated && (
<Button onClick={createNewDoc}>{t('New doc')}</Button>
)}*/}
</SeparatedSection>
{children}
</div>
{/* {searchModal.isOpen && (
<DocSearchModal {...searchModal} size={ModalSize.LARGE} />
)}*/}
</>
);
};
@@ -1,28 +1,23 @@
import { Button } from '@openfun/cunningham-react';
import { Button } from '@gouvfr-lasuite/cunningham-react';
import { usePathname, useRouter } from 'next/navigation';
import { useTranslation } from 'react-i18next';
import { Icon, Text } from '@/components';
import { SeparatedSection } from '@/components/separators';
import { useAuthStore } from '@/core/auth';
import { useCunninghamTheme } from '@/cunningham';
import { useLeftPanelStore } from '@/features/left-panel';
export const LeftPanelItems = () => {
const { t } = useTranslation();
const { closePanel } = useLeftPanelStore();
const { colorsTokens } = useCunninghamTheme();
const { userData } = useAuthStore();
const colors = colorsTokens();
const router = useRouter();
const pathname = usePathname();
const defaultQueries = [
{
icon: 'group',
label: t('Teams'),
label: t('Groups'),
targetQuery: '/teams',
abilities: userData?.abilities?.teams.can_view,
},
@@ -36,7 +31,12 @@ export const LeftPanelItems = () => {
const onSelectQuery = (query: string) => {
router.push(query);
closePanel();
if (
typeof globalThis.window !== 'undefined' &&
!globalThis.window.matchMedia('(min-width: 768px)').matches
) {
closePanel();
}
};
return (
@@ -47,7 +47,7 @@ export const LeftPanelItems = () => {
gap: 'var(--c--theme--spacings--2xs)',
}}
>
{defaultQueries.map((query, index) => {
{defaultQueries.map((query) => {
if (!query.abilities) {
return null;
}
@@ -57,34 +57,32 @@ export const LeftPanelItems = () => {
pathname.startsWith(`${query.targetQuery}/`);
return (
<SeparatedSection
<Button
color="neutral"
variant="tertiary"
style={{
backgroundColor: isActive
? 'var(--c--contextuals--background--semantic--overlay--primary)'
: '',
fontWeight: isActive ? 'bold' : '500',
marginBottom: '4px',
}}
key={query.label}
showSeparator={defaultQueries.length - 1 > index}
>
<Button
data-testid={`${query.label} button`}
aria-label={`${query.label} button`}
onClick={() => onSelectQuery(query.targetQuery)}
style={{
gap: 'var(--c--theme--spacings--xs)',
margin: '10px 0',
cursor: 'pointer',
border: 'none',
backgroundColor: isActive
? colors['greyscale-100']
: colors['greyscale-000'],
fontWeight: isActive ? 700 : undefined,
}}
>
data-testid={`${query.label} button`}
aria-label={`${query.label} button`}
aria-current={isActive ? 'page' : undefined}
onClick={() => onSelectQuery(query.targetQuery)}
icon={
<Icon
$variation={isActive ? '1000' : '700'}
$width="16"
$height="16"
$theme="neutral"
iconName={query.icon}
/>
<Text $variation={isActive ? '1000' : '700'} $size="sm">
{query.label}
</Text>
</Button>
</SeparatedSection>
}
>
<Text $size="sm">{query.label}</Text>
</Button>
);
})}
</div>
@@ -0,0 +1,21 @@
import { Button } from '@gouvfr-lasuite/cunningham-react';
import { useTranslation } from 'react-i18next';
import IconPanelMenu from '@/assets/icons/icon-panel-menu.svg';
import { useLeftPanelStore } from '@/features/left-panel';
export const TogglePanelButton = () => {
const { t } = useTranslation();
const { isPanelOpen, togglePanel } = useLeftPanelStore();
return (
<Button
size="medium"
onClick={() => togglePanel()}
aria-label={isPanelOpen ? t('Close the menu') : t('Open the menu')}
color="neutral"
variant="tertiary"
icon={<IconPanelMenu width={24} height={24} aria-hidden />}
/>
);
};
@@ -1 +1,2 @@
export * from './LeftPanel';
export * from './TogglePanelButton';
@@ -1,4 +1,4 @@
import { Input } from '@openfun/cunningham-react';
import { Input } from '@gouvfr-lasuite/cunningham-react';
interface InputUserEmailProps {
label: string;
@@ -1,4 +1,4 @@
import { Input } from '@openfun/cunningham-react';
import { Input } from '@gouvfr-lasuite/cunningham-react';
interface InputUserEmailProps {
label: string;
@@ -1,4 +1,4 @@
import { Button } from '@openfun/cunningham-react';
import { Button } from '@gouvfr-lasuite/cunningham-react';
import { Box, Text } from '@/components';
import { InputUserEmail, InputUserPassword } from '@/features/login';
@@ -82,7 +82,7 @@ export const LoginForm = ({
)}
<Box $padding="tiny">
<Button color="primary" type="submit" fullWidth>
<Button color="brand" type="submit" fullWidth>
{labelSignIn}
</Button>
</Box>
@@ -1,14 +1,19 @@
import { Button } from '@openfun/cunningham-react';
import React, { useEffect, useRef, useState } from 'react';
import {
Button,
VariantType,
useToastProvider,
} from '@gouvfr-lasuite/cunningham-react';
import { DropdownMenu, useDropdownMenu } from '@gouvfr-lasuite/ui-kit';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { IconOptions, Text } from '@/components';
import { Box, Icon, IconOptions } from '@/components';
import { useUpdateMailDomainAccess } from '@/features/mail-domains/access-management';
import { MailDomain, Role } from '../../domains/types';
import { Access } from '../types';
import { ModalDelete } from './ModalDelete';
import { ModalRole } from './ModalRole';
interface AccessActionProps {
access: Access;
@@ -22,28 +27,17 @@ export const AccessAction = ({
mailDomain,
}: AccessActionProps) => {
const { t } = useTranslation();
const [isModalRoleOpen, setIsModalRoleOpen] = useState(false);
const { isOpen, setIsOpen } = useDropdownMenu();
const [isModalDeleteOpen, setIsModalDeleteOpen] = useState(false);
const [isDropOpen, setIsDropOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const { toast } = useToastProvider();
// Ferme le menu si clic en dehors
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(e.target as Node)
) {
setIsDropOpen(false);
}
};
if (isDropOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isDropOpen]);
const { mutate: updateMailDomainAccess } = useUpdateMailDomainAccess({
onSuccess: () => {
toast(t('The role has been updated'), VariantType.SUCCESS, {
duration: 4000,
});
},
});
if (currentRole === Role.VIEWER) {
return null;
@@ -55,104 +49,115 @@ export const AccessAction = ({
access.can_set_role_to.length > 0;
const canDelete = mailDomain.abilities.delete;
const localizedRoles = {
[Role.ADMIN]: t('Administrator'),
[Role.VIEWER]: t('Viewer'),
[Role.OWNER]: t('Owner'),
};
const menuOptions = [
...(canUpdateRole
? (access.can_set_role_to ?? []).map((role) => ({
label: localizedRoles[role],
callback: () => {
setIsOpen(false);
updateMailDomainAccess({
slug: mailDomain.slug,
accessId: access.id,
role,
});
},
}))
: []),
...(canDelete
? [
...(canUpdateRole ? [{ type: 'separator' as const }] : []),
{
label: t('Remove from domain'),
callback: () => {
setIsOpen(false);
setIsModalDeleteOpen(true);
},
variant: 'danger' as const,
},
]
: []),
];
const roleLabel = localizedRoles[access.role];
if (!canUpdateRole && !canDelete) {
return null;
return (
<Button variant="tertiary" color="neutral" size="small" disabled>
{roleLabel}
</Button>
);
}
if (canUpdateRole) {
return (
<>
<Box $display="inline-flex">
<DropdownMenu
options={menuOptions}
isOpen={isOpen}
onOpenChange={setIsOpen}
>
<Button
variant="tertiary"
color="brand"
size="small"
onClick={() => setIsOpen(!isOpen)}
icon={<Icon iconName="expand_more" $size="sm" $color="brand" />}
iconPosition="right"
>
{roleLabel}
</Button>
</DropdownMenu>
</Box>
{isModalDeleteOpen && canDelete && (
<ModalDelete
access={access}
currentRole={currentRole}
onClose={() => setIsModalDeleteOpen(false)}
mailDomain={mailDomain}
/>
)}
</>
);
}
return (
<>
<div
style={{ position: 'relative', display: 'inline-block' }}
ref={dropdownRef}
>
<button
onClick={() => setIsDropOpen((prev) => !prev)}
aria-label={t('Open the access options modal')}
style={{
background: 'none',
border: 'none',
cursor: 'pointer',
padding: 4,
}}
<Box $display="inline-flex" $direction="row" $align="center" $gap="xs">
<Button variant="tertiary" color="neutral" size="small" disabled>
{roleLabel}
</Button>
<DropdownMenu
options={menuOptions}
isOpen={isOpen}
onOpenChange={setIsOpen}
>
<IconOptions />
</button>
{isDropOpen && (
<div
role="menu"
tabIndex={0}
<button
type="button"
aria-label={t('Open the access options modal')}
onClick={() => setIsOpen(!isOpen)}
style={{
position: 'absolute',
top: '100%',
right: 0,
zIndex: 1000,
background: 'white',
border: '1px solid #ccc',
borderRadius: 4,
padding: '0.5rem',
minWidth: '210px',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
}}
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => {
if (e.key === 'Escape' || e.key === 'Enter') {
setIsDropOpen(false);
}
background: 'none',
border: 'none',
cursor: 'pointer',
padding: 4,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{canUpdateRole && (
<Button
aria-label={t(
'Open the modal to update the role of this access',
)}
onClick={() => {
setIsModalRoleOpen(true);
setIsDropOpen(false);
}}
color="primary-text"
size="small"
fullWidth
icon={
<span className="material-icons" aria-hidden="true">
edit
</span>
}
>
<Text $theme="primary">{t('Update role')}</Text>
</Button>
)}
{canDelete && (
<Button
aria-label={t('Open the modal to delete this access')}
onClick={() => {
setIsModalDeleteOpen(true);
setIsDropOpen(false);
}}
color="primary-text"
size="small"
fullWidth
icon={
<span className="material-icons" aria-hidden="true">
delete
</span>
}
>
<Text $theme="primary">{t('Remove from domain')}</Text>
</Button>
)}
</div>
)}
</div>
<IconOptions />
</button>
</DropdownMenu>
</Box>
{isModalRoleOpen && canUpdateRole && (
<ModalRole
access={access}
currentRole={currentRole}
onClose={() => setIsModalRoleOpen(false)}
slug={mailDomain.slug}
/>
)}
{isModalDeleteOpen && canDelete && (
<ModalDelete
access={access}
@@ -1,5 +1,5 @@
import { SortModel, usePagination } from '@gouvfr-lasuite/cunningham-react';
import { QuickSearchItemTemplate, UserRow } from '@gouvfr-lasuite/ui-kit';
import { SortModel, usePagination } from '@openfun/cunningham-react';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -136,18 +136,12 @@ export const AccessesList = ({
setPagesCount(data?.count ? Math.ceil(data.count / pageSize) : 0);
}, [data?.count, pageSize, setPagesCount]);
const localizedRoles = {
[Role.ADMIN]: t('Administrator'),
[Role.VIEWER]: t('Viewer'),
[Role.OWNER]: t('Owner'),
};
return (
<>
<SeparatedSection />
{invitationsAccesses && invitationsAccesses.length > 0 && (
<Box
$margin={{ bottom: 'xl', top: 'md' }}
$margin={{ bottom: 'md', top: 'md' }}
$padding={{ horizontal: 'md' }}
>
<Text $size="small" $margin={{ bottom: 'md' }} $weight="600">
@@ -159,7 +153,11 @@ export const AccessesList = ({
<QuickSearchItemTemplate
key={access.id}
left={
<Box $direction="row" className="c__share-member-item">
<Box
$direction="row"
$margin={{ bottom: '10px' }}
className="c__share-member-item"
>
<UserRow
key={access.user.email}
fullName={undefined}
@@ -169,7 +167,7 @@ export const AccessesList = ({
<Text
$size="small"
$margin={{ left: '4px' }}
$color={colorsTokens()['greyscale-500']}
$color={colorsTokens()['gray-500']}
>
{t('on pending')}
</Text>
@@ -177,7 +175,6 @@ export const AccessesList = ({
}
/>
<Box $direction="row" $align="center">
<Text>{localizedRoles[access.role]}</Text>
<InvitationAction
mailDomain={mailDomain}
access={access}
@@ -204,7 +201,11 @@ export const AccessesList = ({
<QuickSearchItemTemplate
key={access.id}
left={
<Box $direction="row" className="c__share-member-item">
<Box
$direction="row"
$margin={{ bottom: '10px' }}
className="c__share-member-item"
>
<UserRow
key={access.user.email}
fullName={access.user.name}
@@ -215,7 +216,6 @@ export const AccessesList = ({
}
/>
<Box $direction="row" $align="center">
<Text>{localizedRoles[access.role]}</Text>
<AccessAction
mailDomain={mailDomain}
access={access}
@@ -1,6 +1,8 @@
import { Radio, RadioGroup } from '@openfun/cunningham-react';
import { Button } from '@gouvfr-lasuite/cunningham-react';
import { DropdownMenu, useDropdownMenu } from '@gouvfr-lasuite/ui-kit';
import { useTranslation } from 'react-i18next';
import { Icon } from '@/components';
import { Role } from '@/features/mail-domains/domains/types';
const ORDERED_ROLES = [Role.VIEWER, Role.ADMIN, Role.OWNER];
@@ -25,34 +27,66 @@ export const ChooseRole = ({
new Set([roleAccess, ...availableRoles]),
);
const translations = {
const translations: Record<Role, string> = {
[Role.VIEWER]: t('Viewer'),
[Role.ADMIN]: t('Administrator'),
[Role.OWNER]: t('Owner'),
};
return (
<RadioGroup>
{ORDERED_ROLES.filter((role) => rolesToDisplay.includes(role)).map(
(role) => {
let disableRadio = disabled;
if (role === Role.OWNER) {
disableRadio = disableRadio || currentRole !== Role.OWNER;
}
const { isOpen, setIsOpen } = useDropdownMenu();
return (
<Radio
key={role}
label={translations[role]}
value={role}
name="role"
onChange={(evt) => setRole(evt.target.value as Role)}
defaultChecked={roleAccess === role}
disabled={disableRadio}
/>
);
const orderedRolesToDisplay = ORDERED_ROLES.filter((role) =>
rolesToDisplay.includes(role),
);
const selectedRole = orderedRolesToDisplay.includes(roleAccess)
? roleAccess
: orderedRolesToDisplay[0];
const menuOptions =
orderedRolesToDisplay.map((role) => {
const isOwnerDisabled = role === Role.OWNER && currentRole !== Role.OWNER;
const isOptionDisabled = disabled || isOwnerDisabled;
return {
label: translations[role],
callback: () => {
if (isOptionDisabled) {
return;
}
setIsOpen(false);
setRole(role);
},
)}
</RadioGroup>
isDisabled: isOptionDisabled,
};
}) ?? [];
const selectedLabel = selectedRole ? translations[selectedRole] : '';
return (
<DropdownMenu
options={menuOptions}
isOpen={isOpen}
onOpenChange={setIsOpen}
>
<Button
variant="tertiary"
color="brand"
size="small"
disabled={disabled || orderedRolesToDisplay.length === 0}
aria-haspopup="menu"
aria-expanded={isOpen}
icon={<Icon iconName="expand_more" $size="sm" $color="brand" />}
iconPosition="right"
onClick={() => {
if (disabled || orderedRolesToDisplay.length === 0) {
return;
}
setIsOpen(!isOpen);
}}
>
{selectedLabel}
</Button>
</DropdownMenu>
);
};
@@ -2,11 +2,12 @@ import {
Button,
VariantType,
useToastProvider,
} from '@openfun/cunningham-react';
import React, { useEffect, useRef, useState } from 'react';
} from '@gouvfr-lasuite/cunningham-react';
import { DropdownMenu, useDropdownMenu } from '@gouvfr-lasuite/ui-kit';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { IconOptions, Text } from '@/components';
import { Box, Icon } from '@/components';
import { MailDomain, Role } from '../../domains/types';
import { useDeleteMailDomainInvitation } from '../api';
@@ -24,9 +25,8 @@ export const InvitationAction = ({
mailDomain,
}: InvitationActionProps) => {
const { t } = useTranslation();
const { isOpen, setIsOpen } = useDropdownMenu();
const { toast } = useToastProvider();
const [isDropOpen, setIsDropOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const { mutate: deleteMailDomainInvitation } = useDeleteMailDomainInvitation({
onSuccess: () => {
@@ -36,99 +36,63 @@ export const InvitationAction = ({
},
});
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(e.target as Node)
) {
setIsDropOpen(false);
}
};
if (isDropOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isDropOpen]);
if (currentRole === Role.VIEWER) {
return null;
}
const canDelete = currentRole === Role.OWNER || currentRole === Role.ADMIN;
const localizedRoles = {
[Role.ADMIN]: t('Administrator'),
[Role.VIEWER]: t('Viewer'),
[Role.OWNER]: t('Owner'),
};
const roleLabel = localizedRoles[access.role];
const menuOptions = canDelete
? [
{
label: t('Delete invitation'),
callback: () => {
setIsOpen(false);
deleteMailDomainInvitation({
slug: mailDomain.slug,
invitationId: access.id,
});
},
variant: 'danger' as const,
},
]
: [];
if (!canDelete) {
return null;
return (
<Button variant="tertiary" color="neutral" size="small" disabled>
{roleLabel}
</Button>
);
}
return (
<div
style={{ position: 'relative', display: 'inline-block' }}
ref={dropdownRef}
>
<button
onClick={() => setIsDropOpen((prev) => !prev)}
aria-label={t('Open the invitation options modal')}
style={{
background: 'none',
border: 'none',
cursor: 'pointer',
padding: 4,
}}
type="button"
<Box $display="inline-flex">
<DropdownMenu
options={menuOptions}
isOpen={isOpen}
onOpenChange={setIsOpen}
>
<IconOptions />
</button>
{isDropOpen && (
<div
role="menu"
tabIndex={0}
style={{
position: 'absolute',
top: '100%',
right: 0,
zIndex: 1000,
background: 'white',
border: '1px solid #ccc',
borderRadius: 4,
padding: '0.5rem',
minWidth: '210px',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
}}
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => {
if (e.key === 'Escape' || e.key === 'Enter') {
setIsDropOpen(false);
}
}}
<Button
variant="tertiary"
color="brand"
size="small"
onClick={() => setIsOpen(!isOpen)}
icon={<Icon iconName="expand_more" $size="sm" $color="brand" />}
iconPosition="right"
aria-label={t('Open the invitation options modal')}
>
{canDelete && (
<Button
aria-label={t('Delete this invitation')}
onClick={() => {
deleteMailDomainInvitation({
slug: mailDomain.slug,
invitationId: access.id,
});
setIsDropOpen(false);
}}
color="primary-text"
size="small"
fullWidth
icon={
<span className="material-icons" aria-hidden="true">
delete
</span>
}
>
<Text $theme="primary">{t('Delete invitation')}</Text>
</Button>
)}
</div>
)}
</div>
{roleLabel}
</Button>
</DropdownMenu>
</Box>
);
};
@@ -3,7 +3,7 @@ import {
ModalSize,
VariantType,
useToastProvider,
} from '@openfun/cunningham-react';
} from '@gouvfr-lasuite/cunningham-react';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -114,7 +114,7 @@ export const ModalDomainAccessesManagement = ({
<Modal
isOpen
leftActions={
<Button color="secondary" fullWidth onClick={onClose}>
<Button color="neutral" variant="secondary" fullWidth onClick={onClose}>
{t('Cancel')}
</Button>
}
@@ -123,7 +123,7 @@ export const ModalDomainAccessesManagement = ({
hideCloseButton
rightActions={
<Button
color="primary"
color="brand"
fullWidth
disabled={!selectedMembers.length}
onClick={() => void handleValidate()}
@@ -147,12 +147,12 @@ export const ModalDomainAccessesManagement = ({
selectedMembers={selectedMembers}
/>
{selectedMembers.length > 0 && (
<Box $margin={{ top: 'small' }}>
<Box $margin={{ top: 'small' }} $flex="1">
<Text as="h4" $textAlign="left" $margin={{ bottom: 'tiny' }}>
{t('Choose a role')}
{t('Choose a role : ')}
</Text>
<ChooseRole
roleAccess={currentRole}
roleAccess={role}
disabled={false}
availableRoles={[Role.VIEWER, Role.ADMIN, Role.OWNER]}
currentRole={currentRole}
@@ -3,7 +3,7 @@ import {
ModalSize,
VariantType,
useToastProvider,
} from '@openfun/cunningham-react';
} from '@gouvfr-lasuite/cunningham-react';
import { t } from 'i18next';
import { useRouter } from 'next/navigation';
@@ -66,14 +66,19 @@ export const ModalDelete = ({
closeOnClickOutside
hideCloseButton
leftActions={
<Button color="secondary" fullWidth onClick={() => onClose()}>
<Button
color="neutral"
variant="secondary"
fullWidth
onClick={() => onClose()}
>
{t('Cancel')}
</Button>
}
onClose={onClose}
rightActions={
<Button
color="primary"
color="brand"
fullWidth
onClick={() => {
removeMailDomainAccess({
@@ -131,8 +136,8 @@ export const ModalDelete = ({
$padding="big"
$direction="row"
$gap="0.5rem"
$background={colorsTokens()['primary-150']}
$theme="primary"
$background={colorsTokens()['gray-050']}
$theme="neutral"
>
<IconUser width={20} height={20} aria-hidden="true" />
<Text>{access.user.name}</Text>
@@ -3,13 +3,13 @@ import {
ModalSize,
VariantType,
useToastProvider,
} from '@openfun/cunningham-react';
} from '@gouvfr-lasuite/cunningham-react';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { APIError } from '@/api';
import { Box, Text } from '@/components';
import { Modal } from '@/components/Modal';
import { CustomModal } from '@/components/modal/CustomModal';
import { useCreateMailDomainAccess } from '@/features/mail-domains/access-management';
import { useCreateInvitation } from '@/features/mail-domains/access-management/api';
import {
@@ -125,10 +125,10 @@ export const ModalDomainAccessesManagement = ({
};
return (
<Modal
<CustomModal
isOpen
leftActions={
<Button color="secondary" fullWidth onClick={onClose}>
<Button color="neutral" variant="secondary" fullWidth onClick={onClose}>
{t('Cancel')}
</Button>
}
@@ -157,30 +157,38 @@ export const ModalDomainAccessesManagement = ({
/>
{selectedMembers.length > 0 && (
<Box $margin={{ top: 'small' }}>
<Text as="h4" $textAlign="left" $margin={{ bottom: 'tiny' }}>
{t('Choose a role')}
</Text>
<ChooseRole
roleAccess={currentRole}
disabled={false}
availableRoles={[Role.VIEWER, Role.ADMIN, Role.OWNER]}
currentRole={currentRole}
setRole={setRole}
/>
<Box $direction="row" $align="baseline" $gap="4px">
<Text
as="span"
$size="small"
$weight="bold"
$textAlign="left"
$margin={{ bottom: 'tiny' }}
>
{t('Choose a role:')}
</Text>
<ChooseRole
roleAccess={role}
disabled={false}
availableRoles={[Role.VIEWER, Role.ADMIN, Role.OWNER]}
currentRole={currentRole}
setRole={setRole}
/>
</Box>
<Box $align="end">
<Button
color="primary"
color="brand"
size="medium"
disabled={!selectedMembers.length}
onClick={() => void handleValidate()}
>
{t('OK')}
{t('Send invitation')}
</Button>
</Box>
</Box>
)}
</Box>
<AccessesList mailDomain={mailDomain} currentRole={currentRole} />
</Modal>
</CustomModal>
);
};
@@ -1,102 +0,0 @@
import {
Button,
ModalSize,
VariantType,
useToastProvider,
} from '@openfun/cunningham-react';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, TextErrors } from '@/components';
import { Modal } from '@/components/Modal';
import { useUpdateMailDomainAccess } from '@/features/mail-domains/access-management';
import { Role } from '../../domains';
import { Access } from '../types';
import { ChooseRole } from './ChooseRole';
interface ModalRoleProps {
access: Access;
currentRole: Role;
onClose: () => void;
slug: string;
}
export const ModalRole = ({
access,
currentRole,
onClose,
slug,
}: ModalRoleProps) => {
const { t } = useTranslation();
const [localRole, setLocalRole] = useState(access.role);
const { toast } = useToastProvider();
const {
mutate: updateMailDomainAccess,
error: errorUpdate,
isError: isErrorUpdate,
isPending,
} = useUpdateMailDomainAccess({
onSuccess: () => {
toast(t('The role has been updated'), VariantType.SUCCESS, {
duration: 4000,
});
onClose();
},
});
return (
<Modal
isOpen={true}
leftActions={
<Button
color="secondary"
fullWidth
onClick={() => onClose()}
disabled={isPending}
>
{t('Cancel')}
</Button>
}
onClose={() => onClose()}
closeOnClickOutside
hideCloseButton
rightActions={
<Button
color="primary"
fullWidth
onClick={() => {
updateMailDomainAccess({
role: localRole,
slug,
accessId: access.id,
});
}}
disabled={isPending}
>
{t('Validate')}
</Button>
}
size={ModalSize.MEDIUM}
title={t('Update the role')}
>
<Box aria-label={t('Radio buttons to update the roles')}>
{isErrorUpdate && (
<TextErrors
$margin={{ bottom: 'small' }}
causes={errorUpdate.cause}
/>
)}
<ChooseRole
roleAccess={access.role}
availableRoles={access.can_set_role_to}
currentRole={currentRole}
disabled={false}
setRole={setLocalRole}
/>
</Box>
</Modal>
);
};
@@ -29,73 +29,81 @@ describe('ChooseRole', () => {
jest.clearAllMocks();
});
it('renders available roles correctly when we are Administrator', () => {
it('renders available roles correctly when we are Administrator', async () => {
const user = userEvent.setup();
renderChooseRole();
expect(screen.getByLabelText('Viewer')).toBeInTheDocument();
expect(screen.getByLabelText('Administrator')).toBeInTheDocument();
expect(
screen.getByRole('button', { name: 'Administrator' }),
).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'Administrator' }));
expect(
screen.getByRole('menuitem', { name: 'Viewer' }),
).toBeInTheDocument();
expect(
screen.getByRole('menuitem', { name: 'Administrator' }),
).toBeInTheDocument();
});
it('renders available roles correctly when we are owner', () => {
it('renders available roles correctly when we are owner', async () => {
const user = userEvent.setup();
renderChooseRole({
currentRole: Role.OWNER,
roleAccess: Role.OWNER,
});
expect(screen.getByLabelText('Viewer')).toBeInTheDocument();
expect(screen.getByLabelText('Administrator')).toBeInTheDocument();
expect(screen.getByLabelText('Owner')).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'Owner' }));
expect(
screen.getByRole('menuitem', { name: 'Viewer' }),
).toBeInTheDocument();
expect(
screen.getByRole('menuitem', { name: 'Administrator' }),
).toBeInTheDocument();
expect(screen.getByRole('menuitem', { name: 'Owner' })).toBeInTheDocument();
});
it('sets default role checked correctly', () => {
renderChooseRole({ currentRole: Role.ADMIN });
const adminRadio: HTMLInputElement = screen.getByLabelText('Administrator');
const viewerRadio: HTMLInputElement = screen.getByLabelText('Viewer');
expect(adminRadio).toBeChecked();
expect(viewerRadio).not.toBeChecked();
const trigger = screen.getByRole('button', { name: 'Administrator' });
expect(trigger).toBeInTheDocument();
expect(trigger).toHaveTextContent('Administrator');
});
it('calls setRole when a new role is selected', async () => {
const user = userEvent.setup();
renderChooseRole();
await user.click(screen.getByLabelText('Viewer'));
await user.click(screen.getByRole('button', { name: 'Administrator' }));
await user.click(screen.getByRole('menuitem', { name: 'Viewer' }));
await waitFor(() => {
expect(mockSetRole).toHaveBeenCalledWith(Role.VIEWER);
});
});
it('disables radio buttons when disabled prop is true', () => {
it('disables trigger button when disabled prop is true', () => {
renderChooseRole({ disabled: true });
const viewerRadio: HTMLInputElement = screen.getByLabelText('Viewer');
const adminRadio: HTMLInputElement = screen.getByLabelText('Administrator');
expect(viewerRadio).toBeDisabled();
expect(adminRadio).toBeDisabled();
const trigger = screen.getByRole('button', { name: 'Administrator' });
expect(trigger).toBeDisabled();
});
it('disables owner radio button if current role is not owner', () => {
it('disables owner menu option when current role is not owner', async () => {
const user = userEvent.setup();
renderChooseRole({
availableRoles: [Role.VIEWER, Role.ADMIN, Role.OWNER],
currentRole: Role.ADMIN,
});
const ownerRadio = screen.getByLabelText('Owner');
expect(ownerRadio).toBeDisabled();
await user.click(screen.getByRole('button', { name: 'Administrator' }));
const ownerOption = screen.getByRole('menuitem', { name: 'Owner' });
expect(ownerOption).toHaveAttribute('aria-disabled', 'true');
});
it('removes duplicates from availableRoles', () => {
it('removes duplicates from availableRoles', async () => {
const user = userEvent.setup();
renderChooseRole({
availableRoles: [Role.VIEWER, Role.ADMIN, Role.VIEWER],
currentRole: Role.ADMIN,
});
const radios = screen.getAllByRole('radio');
expect(radios.length).toBe(2); // Only two unique roles should be rendered
await user.click(screen.getByRole('button', { name: 'Administrator' }));
const menuItems = screen.getAllByRole('menuitem');
expect(menuItems).toHaveLength(2);
});
it('renders and checks owner role correctly when currentRole is owner', () => {
@@ -104,30 +112,19 @@ describe('ChooseRole', () => {
roleAccess: Role.OWNER,
availableRoles: [Role.OWNER, Role.VIEWER, Role.ADMIN],
});
const ownerRadio: HTMLInputElement = screen.getByLabelText('Owner');
expect(ownerRadio).toBeInTheDocument();
expect(ownerRadio).toBeChecked();
const trigger = screen.getByRole('button', { name: 'Owner' });
expect(trigger).toBeInTheDocument();
expect(trigger).toHaveTextContent('Owner');
});
it('renders no roles if availableRoles is empty', () => {
it('renders only current role when availableRoles is empty', async () => {
const user = userEvent.setup();
renderChooseRole({
availableRoles: [],
currentRole: Role.ADMIN,
});
const radios = screen.queryAllByRole('radio');
expect(radios.length).toBe(1); // Only the current role should be rendered
});
it.failing('sets aria-checked attribute correctly for selected roles', () => {
renderChooseRole({ currentRole: Role.ADMIN });
const adminRadio = screen.getByLabelText('Administrator');
const viewerRadio = screen.getByLabelText('Viewer');
expect(adminRadio).toHaveAttribute('aria-checked', 'true');
expect(viewerRadio).toHaveAttribute('aria-checked', 'false');
await user.click(screen.getByRole('button', { name: 'Administrator' }));
const menuItems = screen.getAllByRole('menuitem');
expect(menuItems).toHaveLength(1);
});
});
@@ -1,4 +1,7 @@
import { VariantType, useToastProvider } from '@openfun/cunningham-react';
import {
VariantType,
useToastProvider,
} from '@gouvfr-lasuite/cunningham-react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import fetchMock from 'fetch-mock';
@@ -15,8 +18,8 @@ jest.mock('next/navigation', () => ({
useRouter: jest.fn(),
}));
jest.mock('@openfun/cunningham-react', () => ({
...jest.requireActual('@openfun/cunningham-react'),
jest.mock('@gouvfr-lasuite/cunningham-react', () => ({
...jest.requireActual('@gouvfr-lasuite/cunningham-react'),
useToastProvider: jest.fn(),
}));
@@ -1,146 +0,0 @@
import { VariantType, useToastProvider } from '@openfun/cunningham-react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import fetchMock from 'fetch-mock';
import React from 'react';
import { AppWrapper } from '@/tests/utils';
import { Role } from '../../../domains';
import { useWhoAmI } from '../../hooks/useWhoAmI';
import { Access } from '../../types';
import { ModalRole } from '../ModalRole';
jest.mock('@openfun/cunningham-react', () => ({
...jest.requireActual('@openfun/cunningham-react'),
useToastProvider: jest.fn(),
}));
jest.mock('../../hooks/useWhoAmI');
describe('ModalRole', () => {
const access: Access = {
id: '1-1-1-1-1-1',
role: Role.ADMIN,
user: {
id: '2-1-1-1-1-1',
name: 'username1',
email: 'user1@test.com',
},
can_set_role_to: [Role.VIEWER, Role.ADMIN],
};
const mockOnClose = jest.fn();
const mockToast = jest.fn();
const renderModalRole = (
isLastOwner = false,
isOtherOwner = false,
props?: Partial<React.ComponentProps<typeof ModalRole>>,
) => {
(useToastProvider as jest.Mock).mockReturnValue({ toast: mockToast });
(useWhoAmI as jest.Mock).mockReturnValue({
isLastOwner,
isOtherOwner,
});
return render(
<ModalRole
access={access}
currentRole={props?.currentRole ?? Role.ADMIN}
onClose={mockOnClose}
slug="domain-slug"
/>,
{ wrapper: AppWrapper },
);
};
beforeEach(() => {
jest.clearAllMocks();
fetchMock.restore();
});
it('renders the modal with all elements', () => {
renderModalRole();
expect(screen.getByText('Update the role')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Cancel/i })).toBeInTheDocument();
expect(
screen.getByRole('button', { name: /Validate/i }),
).toBeInTheDocument();
expect(
screen.getByLabelText('Radio buttons to update the roles'),
).toBeInTheDocument();
});
it('calls the close function when Cancel is clicked', async () => {
renderModalRole();
const cancelButton = screen.getByRole('button', { name: /Cancel/i });
await userEvent.click(cancelButton);
await waitFor(() => {
expect(mockOnClose).toHaveBeenCalledTimes(1);
});
});
it('updates the role and closes the modal when Validate is clicked', async () => {
fetchMock.patch(`end:mail-domains/domain-slug/accesses/1-1-1-1-1-1/`, {
status: 200,
body: {
id: '1-1-1-1-1-1',
role: Role.VIEWER,
},
});
renderModalRole();
const validateButton = screen.getByRole('button', { name: /Validate/i });
await userEvent.click(validateButton);
await waitFor(() => {
expect(fetchMock.calls().length).toBe(1);
});
expect(fetchMock.lastCall()?.[0]).toContain(
'/mail-domains/domain-slug/accesses/1-1-1-1-1-1/',
);
await waitFor(() => {
expect(mockOnClose).toHaveBeenCalledTimes(1);
});
expect(mockToast).toHaveBeenCalledWith(
'The role has been updated',
VariantType.SUCCESS,
{ duration: 4000 },
);
});
it('shows error message when update fails', async () => {
fetchMock.patch(`end:mail-domains/domain-slug/accesses/1-1-1-1-1-1/`, {
status: 400,
body: {
cause: ['Error updating role'],
},
});
renderModalRole();
const validateButton = screen.getByRole('button', { name: /Validate/i });
await userEvent.click(validateButton);
await waitFor(() => {
expect(fetchMock.calls().length).toBe(1);
});
expect(screen.getByText('Error updating role')).toBeInTheDocument();
});
it('displays the available roles and ensures no duplicates', () => {
renderModalRole();
const radioButtons = screen.getAllByRole('radio');
expect(radioButtons.length).toBe(2); // Only two roles: Viewer and Admin
expect(screen.getByLabelText('Administrator')).toBeInTheDocument();
expect(screen.getByLabelText('Viewer')).toBeInTheDocument();
});
});
@@ -1,4 +1,3 @@
export * from './AccessesList';
export * from './ChooseRole';
export * from './ModalRole';
export * from './ModalDelete';
@@ -1,4 +1,4 @@
import { Button, Input, Tooltip } from '@openfun/cunningham-react';
import { Button, Input, Tooltip } from '@gouvfr-lasuite/cunningham-react';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -35,7 +35,7 @@ export function AliasesView({ mailDomain }: { mailDomain: MailDomain }) {
return (
<>
<div aria-label="Aliases panel" className="container">
<div aria-label="Aliases panel">
<h3 style={{ fontWeight: 700, fontSize: '18px', marginBottom: 'base' }}>
{t('Aliases')}
</h3>
@@ -83,7 +83,7 @@ export function AliasesView({ mailDomain }: { mailDomain: MailDomain }) {
<div
className="hidden md:flex"
style={{
background: colors['greyscale-200'],
background: colors['gray-200'],
height: '32px',
width: '1px',
}}
@@ -1,11 +1,10 @@
import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';
import {
Button,
Loader,
ModalSize,
VariantType,
useToastProvider,
} from '@openfun/cunningham-react';
} from '@gouvfr-lasuite/cunningham-react';
import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';
import React, { useState } from 'react';
import { Controller, FormProvider, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
@@ -16,7 +15,7 @@ import {
Box,
HorizontalSeparator,
Icon,
Input,
LocalInput,
Text,
TextErrors,
} from '@/components';
@@ -40,7 +39,7 @@ export const ModalCreateAlias = ({
const [step] = useState(0);
const [isSubmitting, setIsSubmitting] = useState(false);
const [destinations, setDestinations] = useState<string[]>([]);
const [newDestination, setNewDestination] = useState('');
const [newDestination, setNewDestination] = useState<string>('');
const [destinationError, setDestinationError] = useState<string | null>(null);
type AliasFormData = {
@@ -63,7 +62,7 @@ export const ModalCreateAlias = ({
});
const addDestination = () => {
const trimmed = newDestination.trim();
const trimmed = newDestination?.toString().trim();
if (!trimmed) {
setDestinationError(t('Please enter an email address'));
return;
@@ -91,15 +90,6 @@ export const ModalCreateAlias = ({
setDestinations(destinations.filter((_, i) => i !== index));
};
const handleDestinationKeyPress = (
event: React.KeyboardEvent<HTMLInputElement>,
) => {
if (event.key === 'Enter') {
event.preventDefault();
addDestination();
}
};
const { mutate: createAlias } = useCreateAlias({
mailDomainSlug: mailDomain.slug,
});
@@ -228,83 +218,112 @@ export const ModalCreateAlias = ({
<Text $size="md" $weight="bold">
{t('Alias configuration')}
</Text>
<Text $theme="greyscale" $variation="600">
<Text $theme="gray" $variation="600">
{t(
'An alias allows you to redirect emails to one or more addresses.',
)}
</Text>
</Box>
<Box
$padding="md"
style={{
position: 'relative',
alignItems: 'end',
gap: '20px',
flexDirection: 'row',
alignContent: 'flex-end',
}}
>
<Box $padding="md">
<Controller
name="local_part"
control={methods.control}
render={({ field }) => (
<Box $align="center">
<Input
{...field}
label={t('Name of the alias')}
required
placeholder={t('contact')}
/>
</Box>
render={({ field, fieldState }) => (
<LocalInput
{...field}
id="alias_local_part"
label={t('Name of the alias')}
right={
<span
style={{
padding: '12px 12px 12px 0',
fontSize: 14,
color: 'var(--c--theme--colors--gray-550)',
fontWeight: 500,
}}
>
@{mailDomain.name}
</span>
}
required
error={fieldState.error?.message}
/>
)}
/>
<Box
style={{
display: 'flex',
position: 'absolute',
top: '58px',
left: '210px',
}}
>
<Text className="mb-8" $weight="500">
@{mailDomain.name}
</Text>
</Box>
</Box>
<HorizontalSeparator $withPadding={true} />
<Box $padding={{ horizontal: 'md' }}>
<Box $margin={{ top: 'base', bottom: 'base' }} $gap="12px">
<Text $size="sm" $weight="500">
{t('Destination email addresses')}
</Text>
<Box $gap="4px">
<Box $direction="row" $gap="8px" $align="end">
<Box style={{ flex: 1 }}>
<Input
value={newDestination}
<Box $direction="column" $gap="8px">
<Box $margin={{ bottom: 'xxs' }}>
<label
htmlFor="newDestination"
style={{
fontWeight: 500,
color:
'var(--c--contextuals--text--semantic--neutral--primary)',
}}
>
{t('Add destination email')}
</label>
</Box>
<div
style={{
display: 'inline-flex',
alignItems: 'center',
width: '100%',
flexDirection: 'row',
flexWrap: 'nowrap',
border: `1px solid var(--c--contextuals--border--semantic--neutral--tertiary)`,
borderRadius: 4,
overflow: 'hidden',
paddingRight: '4px',
}}
>
<input
id="newDestination"
aria-required="true"
required
onChange={(e) => {
setNewDestination(e.target.value);
setDestinationError(null);
}}
onKeyPress={handleDestinationKeyPress}
label={t('Add destination email')}
placeholder=""
onKeyUp={(e) => {
if (e.key === 'Enter') {
addDestination();
}
}}
style={{
flex: '1 1 0',
minWidth: 0,
padding: '12px',
border: 'none',
borderRadius: 0,
fontSize: 14,
background: 'transparent',
color:
'var(--c--contextuals--text--semantic--neutral--tertiary)',
outline: 'none',
}}
/>
</Box>
<Button
type="button"
onClick={addDestination}
disabled={!newDestination.trim()}
>
{t('Add destination')}
</Button>
<Button
type="button"
size="small"
onClick={addDestination}
disabled={!newDestination.trim()}
tabIndex={-1}
icon={<Icon iconName="add" $color="currentColor" />}
>
{t('Add destination')}
</Button>
</div>
</Box>
{destinationError && (
<Text $theme="warning" $size="sm">
<Text $theme="error" $variation="secondary" $size="sm">
{destinationError}
</Text>
)}
@@ -334,6 +353,7 @@ export const ModalCreateAlias = ({
>
<th
style={{
paddingLeft: '12px',
textAlign: 'left',
fontWeight: 500,
fontSize: '14px',
@@ -343,8 +363,9 @@ export const ModalCreateAlias = ({
</th>
<th
style={{
paddingBottom: '12px',
paddingRight: '12px',
textAlign: 'right',
fontWeight: 500,
width: '80px',
}}
>
@@ -357,7 +378,6 @@ export const ModalCreateAlias = ({
<tr
key={index}
style={{
paddingBottom: '12px',
borderBottom:
index < destinations.length - 1
? '1px solid var(--c--contextuals--border--surface--primary)'
@@ -365,12 +385,16 @@ export const ModalCreateAlias = ({
}}
>
<td>
<Text $size="sm">{destination}</Text>
<Text $size="sm" $padding={{ left: '12px' }}>
{destination}
</Text>
</td>
<td style={{ textAlign: 'right' }}>
<td style={{ textAlign: 'right', padding: '12px' }}>
<Button
type="button"
color="tertiary"
size="small"
color="neutral"
variant="tertiary"
onClick={() => removeDestination(index)}
aria-label={t('Remove destination')}
icon={<Icon iconName="delete" />}
@@ -388,7 +412,7 @@ export const ModalCreateAlias = ({
</FormProvider>
),
leftAction: (
<Button color="secondary" onClick={closeModal}>
<Button color="neutral" variant="secondary" onClick={closeModal}>
{t('Cancel')}
</Button>
),
@@ -424,14 +448,6 @@ export const ModalCreateAlias = ({
closeOnClickOutside
>
{steps[step].content}
{isSubmitting && (
<Box $align="center" $padding="md">
<Loader />
<Text $theme="greyscale" $variation="600" $margin={{ top: 'sm' }}>
{t('Creating alias...')}
</Text>
</Box>
)}
</CustomModal>
</div>
);
@@ -4,7 +4,7 @@ import {
ModalSize,
VariantType,
useToastProvider,
} from '@openfun/cunningham-react';
} from '@gouvfr-lasuite/cunningham-react';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -13,7 +13,7 @@ import {
Box,
HorizontalSeparator,
Icon,
Input,
LocalInput,
Text,
TextErrors,
} from '@/components';
@@ -309,15 +309,6 @@ export const ModalEditAlias = ({
}
};
const handleDestinationKeyPress = (
event: React.KeyboardEvent<HTMLInputElement>,
) => {
if (event.key === 'Enter') {
event.preventDefault();
void addDestination();
}
};
const { mutate: createAlias } = useCreateAlias({
mailDomainSlug: mailDomain.slug,
});
@@ -342,41 +333,32 @@ export const ModalEditAlias = ({
<Text $size="md" $weight="bold">
{t('Alias configuration')}
</Text>
<Text $theme="greyscale" $variation="600">
<Text $theme="gray" $variation="600">
{t('Manage the destination email addresses for this alias.')}
</Text>
</Box>
<Box
$padding="md"
style={{
position: 'relative',
alignItems: 'end',
gap: '20px',
flexDirection: 'row',
alignContent: 'flex-end',
}}
>
<Box>
<Input
value={aliasGroup.local_part}
label={t('Name of the alias')}
disabled
readOnly
/>
</Box>
<Box
style={{
display: 'flex',
position: 'absolute',
top: '58px',
left: '210px',
}}
>
<Text className="mb-8" $weight="500">
@{mailDomain.name}
</Text>
</Box>
<Box $padding="md">
<LocalInput
id="edit-alias-local-part"
label={t('Name of the alias')}
value={aliasGroup.local_part}
right={
<span
style={{
padding: '12px 12px 12px 0',
fontSize: 14,
color: 'var(--c--theme--colors--gray-550)',
fontWeight: 500,
}}
>
@{mailDomain.name}
</span>
}
required={false}
readOnly
disabled
/>
</Box>
<HorizontalSeparator $withPadding={true} />
@@ -388,43 +370,81 @@ export const ModalEditAlias = ({
</Text>
<Box $gap="4px">
<Box $direction="row" $gap="8px" $align="end">
<Box style={{ flex: 1 }}>
<Input
<Box $direction="column" $gap="8px">
<Box $margin={{ bottom: 'xxs' }}>
<Text $size="sm" $weight="500">
{t('Add destination email')}
</Text>
</Box>
<div
style={{
display: 'inline-flex',
alignItems: 'center',
width: '100%',
flexDirection: 'row',
flexWrap: 'nowrap',
border:
'1px solid var(--c--contextuals--border--semantic--neutral--tertiary)',
borderRadius: 4,
overflow: 'hidden',
paddingRight: '4px',
}}
>
<input
id="edit-new-destination"
value={newDestination}
onChange={(e) => {
setNewDestination(e.target.value);
setDestinationError(undefined);
}}
onKeyPress={handleDestinationKeyPress}
error={destinationError}
label={t('Add destination email')}
onKeyUp={(e) => {
if (e.key === 'Enter') {
void addDestination();
}
}}
style={{
flex: '1 1 0',
minWidth: 0,
padding: '12px',
border: 'none',
borderRadius: 0,
fontSize: 14,
background: 'transparent',
color:
'var(--c--contextuals--text--semantic--neutral--primary)',
outline: 'none',
}}
placeholder={t('john.appleseed@example.fr')}
/>
</Box>
<Button
type="submit"
form={FORM_ID}
disabled={!newDestination.trim() || isSubmitting}
>
{isSubmitting ? t('Adding...') : t('Add')}
</Button>
<Button
type="submit"
size="small"
form={FORM_ID}
disabled={!newDestination.trim() || isSubmitting}
tabIndex={-1}
icon={<Icon iconName="add" $color="currentColor" />}
>
{isSubmitting ? t('Adding...') : t('Add destination')}
</Button>
</div>
</Box>
{destinationError && (
<Text $theme="error" $variation="secondary" $size="sm">
{destinationError}
</Text>
)}
</Box>
{/* Tableau des destinations */}
{destinations.length > 0 && (
<Box
$margin={{ top: 'md' }}
$css={`
table tbody tr {
transition: background-color 0.2s ease;
cursor: pointer;
}
table tbody tr:hover {
background-color: rgba(0, 0, 0, 0.04);
}
`}
style={{
border:
'1px solid var(--c--contextuals--border--surface--primary)',
borderRadius: '4px',
overflow: 'hidden',
}}
>
<table
style={{ width: '100%', borderCollapse: 'collapse' }}
@@ -439,6 +459,7 @@ export const ModalEditAlias = ({
>
<th
style={{
paddingLeft: '12px',
textAlign: 'left',
fontWeight: 500,
fontSize: '14px',
@@ -448,8 +469,9 @@ export const ModalEditAlias = ({
</th>
<th
style={{
paddingBottom: '12px',
paddingRight: '12px',
textAlign: 'right',
fontWeight: 500,
width: '80px',
}}
>
@@ -462,17 +484,23 @@ export const ModalEditAlias = ({
<tr
key={index}
style={{
paddingBottom: '12px',
marginBottom: '12px',
borderBottom:
index < destinations.length - 1
? '1px solid var(--c--contextuals--border--surface--primary)'
: 'none',
}}
>
<td style={{ paddingLeft: '12px' }}>
<Text $size="sm">{destination}</Text>
<td>
<Text $size="sm" $padding={{ left: '12px' }}>
{destination}
</Text>
</td>
<td style={{ textAlign: 'right' }}>
<td style={{ textAlign: 'right', padding: '12px' }}>
<Button
type="button"
color="tertiary"
size="small"
color="neutral"
variant="tertiary"
onClick={() => removeDestination(destination)}
aria-label={t('Remove destination')}
icon={<Icon iconName="delete" />}
@@ -491,7 +519,7 @@ export const ModalEditAlias = ({
</>
),
leftAction: (
<Button color="secondary" onClick={closeModal}>
<Button color="neutral" variant="secondary" onClick={closeModal}>
{t('Close')}
</Button>
),
@@ -500,12 +528,10 @@ export const ModalEditAlias = ({
{destinations.length > 0 && (
<Button
type="button"
color="danger"
color="error"
onClick={handleRemoveAllDestinations}
disabled={isSubmitting}
icon={
<Icon iconName="delete" $theme="greyscale" $variation="000" />
}
icon={<Icon iconName="delete" $theme="gray" $variation="000" />}
>
{t('Delete this alias')}
</Button>
@@ -534,7 +560,7 @@ export const ModalEditAlias = ({
{isSubmitting && (
<Box $align="center" $padding="md">
<Loader />
<Text $theme="greyscale" $variation="600" $margin={{ top: 'sm' }}>
<Text $theme="gray" $variation="600" $margin={{ top: 'sm' }}>
{t('Updating alias...')}
</Text>
</Box>
@@ -548,7 +574,8 @@ export const ModalEditAlias = ({
hideCloseButton
leftActions={
<Button
color="secondary"
color="neutral"
variant="secondary"
fullWidth
onClick={() => setConfirmModal({ ...confirmModal, isOpen: false })}
>
@@ -557,7 +584,7 @@ export const ModalEditAlias = ({
}
rightActions={
<Button
color="danger"
color="error"
fullWidth
onClick={confirmModal.onConfirm}
disabled={isSubmitting}
@@ -1,4 +1,4 @@
import { Button, DataGrid, SortModel } from '@openfun/cunningham-react';
import { Button, DataGrid, SortModel } from '@gouvfr-lasuite/cunningham-react';
import type { InfiniteData } from '@tanstack/react-query';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -7,6 +7,7 @@ import { Box, Text, TextErrors } from '@/components';
import { useAuthStore } from '@/core/auth';
import { Alias, AliasGroup } from '@/features/mail-domains/aliases/types';
import { MailDomain } from '@/features/mail-domains/domains';
import { sortData } from '@/utils/sortData';
import { useAliasesInfinite } from '../../api/useAliasesInfinite';
import { ModalEditAlias } from '../ModalEditAlias';
@@ -23,15 +24,6 @@ interface AliasesListViewProps {
querySearch: string;
}
type SortModelItem = {
field: string;
sort: 'asc' | 'desc' | null;
};
function formatSortModel(sortModel: SortModelItem) {
return sortModel.sort === 'desc' ? `-${sortModel.field}` : sortModel.field;
}
export function AliasesListView({
mailDomain,
querySearch,
@@ -42,9 +34,13 @@ export function AliasesListView({
null,
);
const [sortModel] = useState<SortModel>([]);
const [sortModel, setSortModel] = useState<SortModel>([
{
field: 'local_part',
sort: 'desc',
},
]);
const ordering = sortModel.length ? formatSortModel(sortModel[0]) : undefined;
const {
data,
isLoading,
@@ -54,7 +50,6 @@ export function AliasesListView({
isFetchingNextPage,
} = useAliasesInfinite({
mailDomainSlug: mailDomain.slug,
ordering,
}) as {
data: InfiniteData<MailDomainAliasesResponse, number> | undefined;
isLoading: boolean;
@@ -90,7 +85,7 @@ export function AliasesListView({
local_part: alias.local_part,
destinations: [alias.destination],
destinationIds,
count_destinations: 1,
count_destinations: [alias.destination].length,
});
}
});
@@ -100,18 +95,21 @@ export function AliasesListView({
}, [data, mailDomain]);
const filteredAliases = useMemo(() => {
if (!querySearch) {
return aliasGroups;
let result = aliasGroups;
if (querySearch) {
const lowerCaseSearch = querySearch.toLowerCase();
result = result.filter(
(alias) =>
alias.email.toLowerCase().includes(lowerCaseSearch) ||
alias.destinations.some((dest) =>
dest.toLowerCase().includes(lowerCaseSearch),
),
);
}
const lowerCaseSearch = querySearch.toLowerCase();
return aliasGroups.filter(
(alias) =>
alias.email.toLowerCase().includes(lowerCaseSearch) ||
alias.destinations.some((dest) =>
dest.toLowerCase().includes(lowerCaseSearch),
),
);
}, [querySearch, aliasGroups]);
return sortData(result, sortModel);
}, [querySearch, aliasGroups, sortModel]);
const loadMoreRef = useRef<HTMLDivElement | null>(null);
@@ -143,9 +141,11 @@ export function AliasesListView({
{error && <TextErrors causes={error.cause ?? []} />}
{!filteredAliases.length && (
<Text $align="center" $size="small" $padding={{ top: 'base' }}>
{t('No alias was created with this mail domain.')}
</Text>
<Box $align="center" $margin={{ top: 'base', bottom: 'base' }}>
<Text $size="small">
{t('No alias was created with this mail domain.')}
</Text>
</Box>
)}
{filteredAliases && filteredAliases.length ? (
@@ -155,20 +155,19 @@ export function AliasesListView({
rows={filteredAliases}
columns={[
{
field: 'email',
field: 'local_part',
headerName: `${t('Alias')}${filteredAliases.length}`,
renderCell: ({ row }) => (
<Text $weight="400" $theme="greyscale">
<Text $weight="400" $theme="gray">
{row.email}
</Text>
),
},
{
field: 'destinations',
field: 'count_destinations',
headerName: t('Destinations'),
enableSorting: false,
renderCell: ({ row }) => (
<Text $weight="500" $theme="greyscale">
<Text $weight="500" $theme="gray">
{row.count_destinations} destination
{row.count_destinations > 1 ? 's' : ''}
</Text>
@@ -189,7 +188,8 @@ export function AliasesListView({
<Box $direction="row" $gap="sm" $align="center">
{canEdit && (
<Button
color="tertiary"
color="brand"
variant="secondary"
onClick={() => setEditingAliasGroup(row)}
style={{
fontWeight: '500',
@@ -205,6 +205,8 @@ export function AliasesListView({
},
]}
isLoading={isLoading}
sortModel={sortModel}
onSortModelChange={setSortModel}
/>
{isFetchingNextPage && <div>{t('Loading more...')}</div>}
</>
@@ -1,4 +1,4 @@
<svg width="20" height="22" viewBox="0 0 20 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.833274 20.1996C0.649147 20.3682 0.358822 20.3058 0.307032 20.0616C0.269011 19.8823 0.25 19.6731 0.25 19.4341V11.1863C0.25 10.9274 0.269011 10.7057 0.307032 10.5214C0.357913 10.3057 0.595223 10.2539 0.76765 10.3933L6.08706 14.6925C6.29962 14.8643 6.30951 15.1849 6.10795 15.3695L0.833274 20.1996ZM2.56385 21.75C2.26512 21.75 1.99897 21.7201 1.76541 21.6603C1.75027 21.6564 1.7353 21.6523 1.7205 21.6482C1.43988 21.5697 1.40379 21.2299 1.61856 21.033L7.11023 15.9974C7.27385 15.8473 7.52281 15.8402 7.69478 15.9806L8.58475 16.7072C8.82374 16.8965 9.06817 17.0409 9.31802 17.1405C9.56787 17.2352 9.82044 17.2825 10.0757 17.2825C10.331 17.2825 10.5836 17.2352 10.8334 17.1405C11.0887 17.0409 11.3358 16.8965 11.5748 16.7072L12.4648 15.9806C12.6368 15.8402 12.8857 15.8473 13.0494 15.9974L18.541 21.033C18.7558 21.2299 18.7193 21.5689 18.4384 21.6464C18.4212 21.6512 18.4037 21.6558 18.386 21.6603C18.1579 21.7201 17.8945 21.75 17.5957 21.75H2.56385ZM19.8444 20.0616C19.7936 20.3011 19.5089 20.3668 19.3283 20.2014L14.0516 15.3695C13.8501 15.1849 13.86 14.8643 14.0725 14.6925L19.3944 10.3913C19.5625 10.2555 19.7948 10.3111 19.8444 10.5214C19.8879 10.7057 19.9096 10.9274 19.9096 11.1863V19.4341C19.9096 19.6731 19.8879 19.8823 19.8444 20.0616Z" fill="#000091"/>
<path d="M3.82451 11.1887L1.37227 9.18726C0.80273 8.72242 0.80273 7.85211 1.37227 7.38727L9.08957 1.08863C9.64277 0.637124 10.4373 0.637124 10.9905 1.08863L18.7078 7.38727C19.2773 7.85211 19.2773 8.72242 18.7078 9.18726L16.3352 11.1237V8.50847C16.3352 7.67831 15.6622 7.00533 14.832 7.00533H5.32766C4.49749 7.00533 3.82451 7.67831 3.82451 8.50847V11.1887Z" fill="#000091"/>
<svg width="20" height="22" viewBox="0 0 20 22" fill="#F6F8F9" xmlns="http://www.w3.org/2000/svg">
<path d="M0.833274 20.1996C0.649147 20.3682 0.358822 20.3058 0.307032 20.0616C0.269011 19.8823 0.25 19.6731 0.25 19.4341V11.1863C0.25 10.9274 0.269011 10.7057 0.307032 10.5214C0.357913 10.3057 0.595223 10.2539 0.76765 10.3933L6.08706 14.6925C6.29962 14.8643 6.30951 15.1849 6.10795 15.3695L0.833274 20.1996ZM2.56385 21.75C2.26512 21.75 1.99897 21.7201 1.76541 21.6603C1.75027 21.6564 1.7353 21.6523 1.7205 21.6482C1.43988 21.5697 1.40379 21.2299 1.61856 21.033L7.11023 15.9974C7.27385 15.8473 7.52281 15.8402 7.69478 15.9806L8.58475 16.7072C8.82374 16.8965 9.06817 17.0409 9.31802 17.1405C9.56787 17.2352 9.82044 17.2825 10.0757 17.2825C10.331 17.2825 10.5836 17.2352 10.8334 17.1405C11.0887 17.0409 11.3358 16.8965 11.5748 16.7072L12.4648 15.9806C12.6368 15.8402 12.8857 15.8473 13.0494 15.9974L18.541 21.033C18.7558 21.2299 18.7193 21.5689 18.4384 21.6464C18.4212 21.6512 18.4037 21.6558 18.386 21.6603C18.1579 21.7201 17.8945 21.75 17.5957 21.75H2.56385ZM19.8444 20.0616C19.7936 20.3011 19.5089 20.3668 19.3283 20.2014L14.0516 15.3695C13.8501 15.1849 13.86 14.8643 14.0725 14.6925L19.3944 10.3913C19.5625 10.2555 19.7948 10.3111 19.8444 10.5214C19.8879 10.7057 19.9096 10.9274 19.9096 11.1863V19.4341C19.9096 19.6731 19.8879 19.8823 19.8444 20.0616Z" fill="#F6F8F9"/>
<path d="M3.82451 11.1887L1.37227 9.18726C0.80273 8.72242 0.80273 7.85211 1.37227 7.38727L9.08957 1.08863C9.64277 0.637124 10.4373 0.637124 10.9905 1.08863L18.7078 7.38727C19.2773 7.85211 19.2773 8.72242 18.7078 9.18726L16.3352 11.1237V8.50847C16.3352 7.67831 15.6622 7.00533 14.832 7.00533H5.32766C4.49749 7.00533 3.82451 7.67831 3.82451 8.50847V11.1887Z" fill="#F6F8F9"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -1,4 +1,4 @@
import { VariantType } from '@openfun/cunningham-react';
import { VariantType } from '@gouvfr-lasuite/cunningham-react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import fetchMock from 'fetch-mock';
@@ -50,8 +50,8 @@ const mockMailDomain: MailDomain = {
};
const toast = jest.fn();
jest.mock('@openfun/cunningham-react', () => ({
...jest.requireActual('@openfun/cunningham-react'),
jest.mock('@gouvfr-lasuite/cunningham-react', () => ({
...jest.requireActual('@gouvfr-lasuite/cunningham-react'),
useToastProvider: () => ({
toast,
}),
@@ -80,8 +80,10 @@ describe('<MailDomainView />', () => {
wrapper: AppWrapper,
},
);
// Check if action required button is displayed
const actionButton = screen.getByTestId('actions_required');
// Check if action required button is displayed (desktop + mobile variants)
const actionButtons = screen.getAllByTestId('actions_required');
expect(actionButtons.length).toBeGreaterThan(0);
const actionButton = actionButtons[0];
expect(actionButton).toBeInTheDocument();
// Click the button and verify modal content
@@ -117,9 +119,10 @@ describe('<MailDomainView />', () => {
{ wrapper: AppWrapper },
);
// Check if action required button is displayed
const actionButton = screen.getByTestId('actions_required');
expect(actionButton).toBeInTheDocument();
// Check if action required button is displayed (desktop + mobile variants)
const actionButtons = screen.getAllByTestId('actions_required');
expect(actionButtons.length).toBeGreaterThan(0);
const actionButton = actionButtons[0];
// Click the button and verify modal content
fireEvent.click(actionButton);
@@ -17,7 +17,7 @@ export enum EnumMailDomainsOrdering {
}
export type MailDomainsParams = {
ordering: EnumMailDomainsOrdering;
ordering?: string | EnumMailDomainsOrdering;
};
type MailDomainsAPIParams = MailDomainsParams & {
@@ -1,45 +1,95 @@
import { Button } from '@openfun/cunningham-react';
import { DropdownMenu, useDropdownMenu } from '@gouvfr-lasuite/ui-kit';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Box } from '@/components';
import { Box, Icon, IconOptions } from '@/components';
import { ModalDomainAccessesManagement } from '@/features/mail-domains/access-management/components/ModalDomainAccessesManagement';
import { MailDomain, Role } from '@/features/mail-domains/domains';
type DropdownMenuWithDisabled = React.ComponentType<
React.ComponentProps<typeof DropdownMenu> & { isDisabled?: boolean }
>;
const DropdownMenuAny = DropdownMenu as DropdownMenuWithDisabled;
type MailDomainAccessesActionProps = {
mailDomain: MailDomain;
currentRole: Role;
onConfigureDomain?: () => void;
};
export const MailDomainAccessesAction = ({
mailDomain,
currentRole,
}: {
mailDomain: MailDomain;
currentRole: Role;
}) => {
onConfigureDomain,
}: MailDomainAccessesActionProps) => {
const { t } = useTranslation();
const { isOpen, setIsOpen } = useDropdownMenu();
const [isModalAccessOpen, setIsModalAccessOpen] = useState(false);
const openAccessManagement = () => {
setIsOpen(false);
setIsModalAccessOpen(true);
};
const openConfigureDomain = () => {
setIsOpen(false);
onConfigureDomain?.();
};
const options = [
...(mailDomain?.abilities.post
? [
{
label: t('Access management'),
icon: <Icon iconName="manage_accounts" $size="sm" />,
callback: openAccessManagement,
},
]
: []),
...(onConfigureDomain
? [
{
label: t('Configure domain'),
icon: <Icon iconName="settings" $size="sm" />,
callback: openConfigureDomain,
},
]
: []),
];
if (!mailDomain?.abilities.post && !onConfigureDomain) {
return null;
}
const isDisabled = options.length === 0;
return (
<>
<Box $direction="row" $justify="flex-end" $align="center">
<Box $display="flex" $direction="row" $align="center">
{mailDomain?.abilities.post && (
<Button
style={{
border: 'none',
fontWeight: '500',
marginBottom: '0px',
}}
color="primary-text"
aria-label={t('Add a new access in {{name}} domain', {
name: mailDomain?.name,
})}
onClick={() => {
setIsModalAccessOpen(true);
}}
>
{t('Access management')}
</Button>
)}
</Box>
<DropdownMenuAny
options={options}
isOpen={isOpen}
onOpenChange={setIsOpen}
isDisabled={isDisabled}
>
<button
type="button"
aria-label={t('Open domain options menu')}
onClick={() => setIsOpen(!isOpen)}
style={{
background: 'none',
border: 'none',
cursor: 'pointer',
padding: 4,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<IconOptions isHorizontal={true} />
</button>
</DropdownMenuAny>
</Box>
{isModalAccessOpen && mailDomain && (
<ModalDomainAccessesManagement
@@ -0,0 +1,38 @@
import Image from 'next/image';
import { Box } from '@/components';
import MailDomainsLogoUrl from '@/features/mail-domains/assets/mail-domains-logo.svg?url';
type MailDomainLogoCircleProps = {
size?: number;
};
export const MailDomainLogoCircle = ({
size = 24,
}: MailDomainLogoCircleProps) => {
return (
<Box
$css={`
display: flex;
align-items: center;
justify-content: center;
padding: 6px;
width: ${size}px;
height: ${size}px;
border-radius: 48px;
border: 1px solid var(--c--contextuals--border--semantic--overlay--primary);
background: var(--c--contextuals--background--palette--brand--primary);
position: relative;
`}
aria-hidden="true"
>
<Image
alt=""
src={MailDomainsLogoUrl}
fill
style={{ objectFit: 'contain', padding: '4px', paddingTop: '3px' }}
aria-hidden
/>
</Box>
);
};
@@ -1,16 +1,15 @@
import { Button } from '@openfun/cunningham-react';
import { Button } from '@gouvfr-lasuite/cunningham-react';
import { useRouter } from 'next/navigation';
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import { Box, CustomTabs, Tag, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { AliasesView } from '@/features/mail-domains/aliases';
import { useAliases } from '@/features/mail-domains/aliases/api/useAliases';
import MailDomainsLogo from '@/features/mail-domains/assets/mail-domains-logo.svg';
import {
MailDomain,
MailDomainAccessesAction,
MailDomainLogoCircle,
ModalRequiredActionDomain,
Role,
} from '@/features/mail-domains/domains';
@@ -29,7 +28,6 @@ export const MailDomainView = ({
onMailDomainUpdate,
}: Props) => {
const { t } = useTranslation();
const { colorsTokens } = useCunninghamTheme();
const router = useRouter();
const [showModal, setShowModal] = React.useState(false);
@@ -64,77 +62,101 @@ export const MailDomainView = ({
$padding={{ horizontal: 'md' }}
$background="white"
$justify="space-between"
$gap="8px"
$align="center"
$radius="4px"
$direction="row"
$css={`
border: 1px solid ${colorsTokens()['greyscale-200']};
`}
className="regie__panel__container"
>
<Box $direction="row" $align="center" $gap="8px">
<Button
onClick={() => router.push('/mail-domains/')}
icon={<span className="material-icons">arrow_back</span>}
iconPosition="left"
color="secondary"
style={{
fontWeight: '500',
}}
>
{t('Domains')}
</Button>
<MailDomainsLogo aria-hidden="true" />
<Text as="h5" $size="h5" $weight="bold" $theme="primary">
{mailDomain.name}
</Text>
{mailDomain?.status && (
<button
data-testid="actions_required"
onClick={handleShowModal}
<Box
$direction="row"
$margin={{ vertical: 'base' }}
$justify="space-between"
$align="center"
$css="flex-wrap: wrap; gap: 12px;"
>
<Box $direction="row" $align="center" $gap="8px">
<Button
onClick={() => router.push('/mail-domains/')}
icon={<span className="material-icons">arrow_back</span>}
iconPosition="left"
color="neutral"
variant="tertiary"
style={{
backgroundColor: 'transparent',
border: 'none',
cursor: 'pointer',
fontWeight: '500',
marginRight: '12px',
}}
>
<Tag
showTooltip={true}
status={mailDomain.status}
tooltipType="domain"
placement="bottom"
showIcon={true}
></Tag>
</button>
)}
</Box>
<Box $align="center">
{t('Domains')}
</Button>
<Box
$direction="row"
$align="center"
$gap="8px"
className="hidden md:flex"
>
<MailDomainLogoCircle size={24} />
<Text as="h1" $size="h5" $weight="bold" $theme="primary">
{mailDomain.name}
</Text>
{(mailDomain?.status === 'pending' ||
mailDomain?.status === 'action_required' ||
mailDomain?.status) && (
<button
data-testid="actions_required"
onClick={handleShowModal}
style={{
backgroundColor: 'transparent',
border: 'none',
}}
>
<Tag
showTooltip={true}
status={mailDomain.status}
tooltipType="domain"
placement="bottom"
></Tag>
</button>
)}
</Box>
</Box>
<MailDomainAccessesAction
mailDomain={mailDomain}
currentRole={currentRole}
onConfigureDomain={handleShowModal}
/>
<Box
$direction="row"
$align="center"
$gap="8px"
className="md:hidden"
>
<Text as="h1" $size="h5" $weight="bold" $theme="primary">
{mailDomain.name}
</Text>
{(mailDomain?.status === 'pending' ||
mailDomain?.status === 'action_required' ||
mailDomain?.status) && (
<button
data-testid="actions_required"
onClick={handleShowModal}
style={{
backgroundColor: 'transparent',
border: 'none',
}}
>
<Tag
showTooltip={true}
status={mailDomain.status}
tooltipType="domain"
placement="bottom"
></Tag>
</button>
)}
</Box>
</Box>
</Box>
{showModal && (
<ModalRequiredActionDomain
mailDomain={mailDomain}
onMailDomainUpdate={onMailDomainUpdate ?? (() => {})}
closeModal={closeModal}
/>
)}
<Box
$padding={{ horizontal: 'md' }}
$margin={{ top: 'md' }}
$background="white"
$radius="4px"
$css={`
border: 1px solid ${colorsTokens()['greyscale-200']};
`}
>
<CustomTabs
theme="neutral"
tabs={[
{
id: 'mailboxes',
@@ -151,6 +173,14 @@ export const MailDomainView = ({
]}
/>
</Box>
{showModal && (
<ModalRequiredActionDomain
mailDomain={mailDomain}
onMailDomainUpdate={onMailDomainUpdate ?? (() => {})}
closeModal={closeModal}
/>
)}
</Box>
</>
);
@@ -14,7 +14,7 @@ export function MailDomainsLayout({ children }: PropsWithChildren) {
<Box $height="inherit" $direction="row">
<Panel />
<Box
$background={colorsTokens()['greyscale-050']}
$background={colorsTokens()['gray-050']}
$width="100%"
$overflow="auto"
$height="inherit"
@@ -1,5 +1,5 @@
import { Button, Loader, ModalSize } from '@gouvfr-lasuite/cunningham-react';
import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';
import { Button, Loader, ModalSize } from '@openfun/cunningham-react';
import React, { useState } from 'react';
import { Controller, FormProvider, useForm, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
@@ -118,7 +118,7 @@ export const ModalAddMailDomain = ({
</Button>
),
leftAction: (
<Button color="secondary" onClick={closeModal}>
<Button color="neutral" variant="secondary" onClick={closeModal}>
{t('Close')}
</Button>
),
@@ -156,7 +156,7 @@ export const ModalAddMailDomain = ({
/>
)}
/>
<Box $margin={{ vertical: '10px' }}>
<Box>
<Controller
control={methods.control}
name="supportEmail"
@@ -174,7 +174,7 @@ export const ModalAddMailDomain = ({
</Box>
</form>
</FormProvider>
<Text $theme="greyscale" $variation="600">
<Text $theme="neutral" $variation="tertiary">
{t(
'Once the domain is added, an administrator will need to validate it. In the meantime, you can still start adding email addresses.',
)}
@@ -182,7 +182,7 @@ export const ModalAddMailDomain = ({
</>
),
leftAction: (
<Button color="secondary" onClick={() => setStep(0)}>
<Button color="neutral" variant="secondary" onClick={() => setStep(0)}>
{t('Cancel')}
</Button>
),
@@ -3,7 +3,7 @@ import {
ModalSize,
VariantType,
useToastProvider,
} from '@openfun/cunningham-react';
} from '@gouvfr-lasuite/cunningham-react';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -115,7 +115,7 @@ export const ModalCreateAccess = ({
<Modal
isOpen
leftActions={
<Button color="secondary" fullWidth onClick={onClose}>
<Button color="neutral" variant="secondary" fullWidth onClick={onClose}>
{t('Cancel')}
</Button>
}
@@ -124,7 +124,7 @@ export const ModalCreateAccess = ({
hideCloseButton
rightActions={
<Button
color="primary"
color="brand"
fullWidth
disabled={!selectedMembers.length}
onClick={() => void handleValidate()}
@@ -4,11 +4,11 @@ import {
ModalSize,
VariantType,
useToastProvider,
} from '@openfun/cunningham-react';
} from '@gouvfr-lasuite/cunningham-react';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Box, Text } from '@/components';
import { Box, Icon, Text } from '@/components';
import { CustomModal } from '@/components/modal/CustomModal';
import { MailDomain } from '@/features/mail-domains/domains';
@@ -43,40 +43,55 @@ export const ModalRequiredActionDomain = ({
toast(t('copy done'), VariantType.SUCCESS);
};
const isActionRequired = mailDomain.status === 'action_required';
const isEnabled = mailDomain.status === 'enabled';
const title = isActionRequired
? t('Required actions on domain')
: t('Domain configuration');
const step = 0;
const steps = [
{
title:
mailDomain.status !== 'enabled'
? t('Required actions on domain')
: t('Domain configuration'),
title,
content: (
<Text>
{mailDomain.status !== 'enabled' ? (
<>
<p>
{t(
'The domain is currently in action required status. Please take the necessary actions to resolve those following issues.',
)}
</p>
<h3>{t('Actions required detail')}</h3>
</>
) : (
<p>{t('The domain is currently enabled.')}</p>
)}
{mailDomain.action_required_details && (
<pre>
{Object.entries(mailDomain.action_required_details).map(
([check, value], index) => (
<ul key={`action-required-list-${index}`}>
<li key={`action-required-${index}`}>
<b>{check}</b>: {value}
</li>
</ul>
),
<span>
{isActionRequired &&
t(
'The domain is currently in action required status. Please take the necessary actions to resolve those following issues.',
)}
</pre>
{isEnabled &&
t(
'The domain is currently enabled and its configuration looks correct. You can run a new check to make sure everything is still properly configured.',
)}
{!isActionRequired &&
!isEnabled &&
t(
'The domain status can be checked again to update its configuration details.',
)}
</span>
{mailDomain.action_required_details && (
<>
{isActionRequired && (
<>
<h3>{t('Actions required detail')}</h3>
<pre>
{Object.entries(mailDomain.action_required_details).map(
([check, value], index) => (
<ul key={`action-required-list-${index}`}>
<li key={`action-required-${index}`}>
<b>{check}</b>: {value}
</li>
</ul>
),
)}
</pre>
</>
)}
</>
)}
{mailDomain.expected_config && (
<Box $margin={{ bottom: 'medium' }}>
@@ -138,15 +153,18 @@ export const ModalRequiredActionDomain = ({
<Loader />
) : (
<Button
color="brand"
variant="primary"
onClick={() => {
void fetchMailDomain(mailDomain.slug);
}}
icon={<Icon iconName="refresh" $theme="gray" $variation="000" />}
>
{t('Re-run check')}
</Button>
),
leftAction: (
<Button color="secondary" onClick={closeModal}>
<Button color="neutral" variant="secondary" onClick={closeModal}>
{t('Close')}
</Button>
),
@@ -1,3 +1,4 @@
export * from './MailDomainLogoCircle';
export * from './MailDomainsLayout';
export * from './ModalAddMailDomain';
export * from './MailDomainAccessesAction';

Some files were not shown because too many files have changed in this diff Show More