Compare commits

...

29 Commits

Author SHA1 Message Date
Nathan Panchout
2b2a579d9f wip 2025-03-24 17:15:14 +01:00
Nathan Panchout
615ab564cc fixup! (frontend) refactor and theme token update 2025-03-24 09:00:59 +01:00
Nathan Panchout
aadd6d9ec3 fixup! (frontend) added subpage management and document tree features 2025-03-24 08:55:38 +01:00
Nathan Panchout
0a6502a77d (frontend) added subpage management and document tree features
New components were created to manage subpages in the document tree,
including the ability to add, reorder, and view subpages. Tests were
added to verify the functionality of these features. Additionally, API
changes were made to manage the creation and retrieval of document
children.
2025-03-24 08:55:36 +01:00
Nathan Panchout
a32ee20249 (Frontend) Added drag-and-drop functionality for document management
Added a new feature for moving documents within the user interface via
drag-and-drop. This includes the creation of Draggable and Droppable
components, as well as tests to verify document creation and movement
behavior. Changes have also been made to document types to include user
roles and child management capabilities.
2025-03-24 08:54:03 +01:00
Nathan Panchout
48db42f385 (frontend) refactor and theme token update
The configuration file has been simplified by importing configurations
from @gouvfr-lasuite/ui-kit . Colors and components have been updated to
reflect the new values. Additionally, adjustments have been made to
global styles, including the addition of styles for Material icons. Form
components have also been modified to incorporate the new style
properties.
2025-03-24 08:54:01 +01:00
Nathan Panchout
05b14b2948 (frontend) updated dependencies and added new packages
Added several new dependencies to the `package.json` file, including
`@dnd-kit/core`, `@dnd-kit/modifiers`, `@fontsource/material-icons`, and
`@gouvfr-lasuite/ui-kit`.
2025-03-24 08:53:04 +01:00
Nathan Panchout
12f4a72f5e 🐛(back) keep info if document has deleted children
With the soft delete feature, relying on the is_leaf method from the
treebeard is not accurate anymore. To determine if a node is a leaf, it
checks if the number of numchild is equal to 0. But a node can have soft
deleted children, then numchild is equal to 0, but it is not a leaf
because if we want to add a child we have to look for the last child to
compute a correct path. Otherwise we will have an error saying that the
path already exists.
2025-03-24 08:51:42 +01:00
Bastien Guerry
ef7cc67387 📄(legal) Require contributors to sign a DCO
Contributors are required to sign off their commits: this confirms
that they have read and accepted https://developercertificate.org.
2025-03-23 09:57:35 +01:00
Sylvain Zimmer
a8529e434a 🐛(media) fix compatibility with Scaleway Object Storage
Some providers with S3-compatible APIs have slightly different
implementations. In this case, Scaleway didn't accept version_id=""
and has a different version ID scheme. This was tested successfully
and should remain compatible with any other provider.
2025-03-22 18:00:43 +01:00
Manuel Raynaud
f8203a1766 🚨(back) lint code with ruff 0.11.2
New Ruff rule (C420) detects code that should be linted. We apply this
new rule on our code.
2025-03-22 10:28:48 +01:00
renovate[bot]
ce8b98e256 ⬆️(dependencies) update python dependencies 2025-03-22 10:28:48 +01:00
Anthony LC
4243519eee 🔥(frontend) remove Marianne font
Marianne font is now part of the UI kit.
We can remove it from the project.
2025-03-21 17:49:06 +01:00
Nathan Panchout
1abf529891 (frontend) refactor and theme token update
The configuration file has been simplified by importing configurations
from @gouvfr-lasuite/ui-kit . Colors and components have been updated to
reflect the new values. Additionally, adjustments have been made to
global styles, including the addition of styles for Material icons. Form
components have also been modified to incorporate the new style
properties.
2025-03-21 17:49:06 +01:00
Nathan Panchout
69ca4af539 (frontend) updated dependencies and added new packages
Added several new dependencies to the `package.json` file, including
`@dnd-kit/core`, `@dnd-kit/modifiers`, `@fontsource/material-icons`, and
`@gouvfr-lasuite/ui-kit`.
2025-03-21 17:49:06 +01:00
Anthony LC
14b2adedfb 🔖(minor) release 2.6.0
Added:
- 📝(doc) add publiccode.yml

Changed
- 🚸(frontend) ctrl+k modal not when editor is focused

Fixed:
- 🐛(back) allow only images to be used with
  the cors-proxy
- 🐛(backend) stop returning inactive users
  on the list endpoint
- 🔒️(backend) require at least 5 characters
  to search for users
- 🔒️(back) throttle user list endpoint
- 🔒️(back) remove pagination and limit to
   5 for user list endpoint
2025-03-21 17:07:26 +01:00
Anthony LC
a7edb382a7 🩹(frontent) change selector to block cmd+k
Multiple ctrl+k could open the search modal, we
change the selector, now if the toolbar is displayed
we don't open the search modal.
2025-03-21 17:07:26 +01:00
Anthony LC
fb5400c26b ️(frontend) search users with at least 5 characters
We now only search for users when the query
is at least 5 characters long.
2025-03-21 15:44:09 +01:00
Manuel Raynaud
8473facbee 🔒️(back) throttle user list endpoint
The user list endpoint is throttle to avoid users discovery. The
throttle is set to 500 requests per day. This can be changed using the
settings API_USERS_LIST_THROTTLE_RATE.
2025-03-21 15:44:09 +01:00
Anthony LC
5db446e8a8 🏷️(frontend) adapt type for user search
The response from the user request is now an
array of users, we don't paginate anymore.
We adapt the types to reflect this.
2025-03-21 15:44:09 +01:00
Manuel Raynaud
34dfb3fd66 🔒️(back) remove pagination and limit to 5 for user list endpoint
The user list endpoint does not use anymore a pagination, the results is
directly return in a list and the max results returned is limited to 5.
In order to modify this limit the settings API_USERS_LIST_LIMIT is
used.
2025-03-21 15:44:09 +01:00
Samuel Paccoud - DINUM
f9a91eda2d 🐛(backend) stop returning inactive users on the list endpoint
inactive users should not be returned as we don't want users to be
able to share new documents with them.
2025-03-21 15:44:09 +01:00
Samuel Paccoud - DINUM
eba926dea4 🔒️(backend) require at least 5 characters to search for users
Listing users is made a little to easy for authenticated users.
2025-03-21 15:44:09 +01:00
Anthony LC
3839a2e8b1 💄(frontend) improve contrast of Beta icon
The colors of the Beta icon were not contrasted
enough. This was posing an accessibility issue.
We now use a more contrasted color.
2025-03-21 09:22:42 +01:00
Anthony LC
a88d62e07d 🌐(frontend) make Docs title translatable
The title of the docs page was not translatable.
We now use the `t` function to translate the title.
2025-03-21 09:22:42 +01:00
Paul Mustière
b61a7a4961 📝(docs) fix typo
Correct language to not be past tense
2025-03-21 06:38:27 +01:00
Anthony LC
20d32ecc4e 🚸(frontend) ctrl+k modal not when editor is focused
ctrl+k interaction was as well used in the editor.
So if the user has a focus on the editor, we don't
open the searchmodal.
2025-03-20 17:43:32 +01:00
Manuel Raynaud
313acf4f78 🐛(back) allow only images to be used with the cors-proxy
The cors-proxy endpoint allowed to use every type of files and to
execute it in the browser. We limit the scope only to images and
Content-Security-Policy and Content-Disposition headers are also added
to not allow script execution that can be present in a SVG file.
2025-03-20 16:10:47 +01:00
Bastien
3a6105cc7e 📝(doc) add publiccode.yml (#770)
publiccode.yml is a standard for describing Free Software projects,
similar to other initiatives such as https://codemeta.github.io.

It is particularly suitable for describing projects funded by public
administrations. See https://github.com/publiccodeyml/publiccode.yml
2025-03-19 21:28:32 +01:00
103 changed files with 3276 additions and 2346 deletions

View File

@@ -8,6 +8,33 @@ and this project adheres to
## [Unreleased]
## Added
- 📄(legal) Require contributors to sign a DCO #779
## Changed
- ♻️(frontend) Integrate UI kit #783
## [2.6.0] - 2025-03-21
## Added
- 📝(doc) add publiccode.yml #770
## Changed
- 🚸(frontend) ctrl+k modal not when editor is focused #712
## Fixed
- 🐛(back) allow only images to be used with the cors-proxy #781
- 🐛(backend) stop returning inactive users on the list endpoint #636
- 🔒️(backend) require at least 5 characters to search for users #636
- 🔒️(back) throttle user list endpoint #636
- 🔒️(back) remove pagination and limit to 5 for user list endpoint #636
## [2.5.0] - 2025-03-18
## Added
@@ -468,7 +495,8 @@ and this project adheres to
- ✨(frontend) Coming Soon page (#67)
- 🚀 Impress, project to manage your documents easily and collaboratively.
[unreleased]: https://github.com/numerique-gouv/impress/compare/v2.5.0...main
[unreleased]: https://github.com/numerique-gouv/impress/compare/v2.6.0...main
[v2.5.0]: https://github.com/numerique-gouv/impress/releases/v2.6.0
[v2.5.0]: https://github.com/numerique-gouv/impress/releases/v2.5.0
[v2.4.0]: https://github.com/numerique-gouv/impress/releases/v2.4.0
[v2.3.0]: https://github.com/numerique-gouv/impress/releases/v2.3.0

View File

@@ -4,6 +4,8 @@ Thank you for taking the time to contribute! Please follow these guidelines to e
To get started with the project, please refer to the [README.md](https://github.com/suitenumerique/docs/blob/main/README.md) for detailed instructions.
Contributors are required to sign off their commits with `git commit --sign-off`: this confirms that they have read and accepted the [Developer's Certificate of Origin 1.1](https://developercertificate.org/).
Please also check out our [dev handbook](https://suitenumerique.gitbook.io/handbook) to learn our best practices.
## Help us with translations

View File

@@ -93,7 +93,6 @@ build: cache ?= --no-cache
build: ## build the project containers
@$(MAKE) build-backend cache=$(cache)
@$(MAKE) build-yjs-provider cache=$(cache)
@$(MAKE) build-frontend cache=$(cache)
.PHONY: build
build-backend: cache ?=
@@ -128,7 +127,6 @@ run-backend: ## Start only the backend application and all needed services
run: ## start the wsgi (production) and development server
run:
@$(MAKE) run-backend
@$(COMPOSE) up --force-recreate -d frontend
.PHONY: run
status: ## an alias for "docker compose ps"

View File

@@ -61,4 +61,4 @@ COLLABORATION_SERVER_SECRET=my-secret
COLLABORATION_WS_URL=ws://localhost:8083/collaboration/ws/
# Frontend
FRONTEND_THEME=dsfr
FRONTEND_THEME=default

27
publiccode.yml Normal file
View File

@@ -0,0 +1,27 @@
publiccodeYmlVersion: "2.4.0"
name: Docs
url: https://github.com/suitenumerique/docs
landingURL: https://github.com/suitenumerique/docs
creationDate: 2023-12-10
logo: https://raw.githubusercontent.com/suitenumerique/docs/main/docs/assets/docs-logo.png
usedBy:
- Direction interministériel du numérique (DINUM)
fundedBy:
- name: Direction interministériel du numérique (DINUM)
url: https://www.numerique.gouv.fr
roadmap: "https://github.com/orgs/suitenumerique/projects/2/views/1"
softwareType: "standalone/other"
description:
en:
shortDescription: "The open source document editor where your notes can become knowledge through live collaboration"
fr:
shortDescription: "L'éditeur de documents open source où vos notes peuvent devenir des connaissances grâce à la collaboration en direct."
legal:
license: MIT
maintenance:
type: internal
contacts:
- name: "Virgile Deville"
email: "virgile.deville@numerique.gouv.fr"
- name: "samuel.paccoud"
email: "samuel.paccoud@numerique.gouv.fr"

View File

@@ -24,6 +24,7 @@ from botocore.exceptions import ClientError
from rest_framework import filters, status, viewsets
from rest_framework import response as drf_response
from rest_framework.permissions import AllowAny
from rest_framework.throttling import UserRateThrottle
from core import authentication, enums, models
from core.services.ai_services import AIService
@@ -135,14 +136,35 @@ class Pagination(drf.pagination.PageNumberPagination):
page_size_query_param = "page_size"
class UserListThrottleBurst(UserRateThrottle):
"""Throttle for the user list endpoint."""
scope = "user_list_burst"
class UserListThrottleSustained(UserRateThrottle):
"""Throttle for the user list endpoint."""
scope = "user_list_sustained"
class UserViewSet(
drf.mixins.UpdateModelMixin, viewsets.GenericViewSet, drf.mixins.ListModelMixin
):
"""User ViewSet"""
permission_classes = [permissions.IsSelf]
queryset = models.User.objects.all()
queryset = models.User.objects.filter(is_active=True)
serializer_class = serializers.UserSerializer
pagination_class = None
throttle_classes = []
def get_throttles(self):
self.throttle_classes = []
if self.action == "list":
self.throttle_classes = [UserListThrottleBurst, UserListThrottleSustained]
return super().get_throttles()
def get_queryset(self):
"""
@@ -157,11 +179,11 @@ class UserViewSet(
return queryset
# Exclude all users already in the given document
if document_id := self.request.GET.get("document_id", ""):
if document_id := self.request.query_params.get("document_id", ""):
queryset = queryset.exclude(documentaccess__document_id=document_id)
if not (query := self.request.GET.get("q", "")):
return queryset
if not (query := self.request.query_params.get("q", "")) or len(query) < 5:
return queryset.none()
# For emails, match emails by Levenstein distance to prevent typing errors
if "@" in query:
@@ -170,7 +192,7 @@ class UserViewSet(
distance=RawSQL("levenshtein(email::text, %s::text)", (query,))
)
.filter(distance__lte=3)
.order_by("distance", "email")
.order_by("distance", "email")[: settings.API_USERS_LIST_LIMIT]
)
# Use trigram similarity for non-email-like queries
@@ -180,7 +202,7 @@ class UserViewSet(
queryset.filter(email__trigram_word_similar=query)
.annotate(similarity=TrigramSimilarity("email", query))
.filter(similarity__gt=0.2)
.order_by("-similarity", "email")
.order_by("-similarity", "email")[: settings.API_USERS_LIST_LIMIT]
)
@drf.decorators.action(
@@ -921,7 +943,7 @@ class DocumentViewSet(
@drf.decorators.action(
detail=True,
methods=["get", "delete"],
url_path="versions/(?P<version_id>[0-9a-f-]{36})",
url_path="versions/(?P<version_id>[0-9a-z-]+)",
)
# pylint: disable=unused-argument
def versions_detail(self, request, pk, version_id, *args, **kwargs):
@@ -1271,13 +1293,21 @@ class DocumentViewSet(
},
timeout=10,
)
content_type = response.headers.get("Content-Type", "")
if not content_type.startswith("image/"):
return drf.response.Response(
status=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
)
# Use StreamingHttpResponse with the response's iter_content to properly stream the data
proxy_response = StreamingHttpResponse(
streaming_content=response.iter_content(chunk_size=8192),
content_type=response.headers.get(
"Content-Type", "application/octet-stream"
),
content_type=content_type,
headers={
"Content-Disposition": "attachment;",
"Content-Security-Policy": "default-src 'none'; img-src 'none' data:;",
},
status=response.status_code,
)

View File

@@ -0,0 +1,17 @@
# Generated by Django 5.1.7 on 2025-03-14 14:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0019_alter_user_language_default_to_null"),
]
operations = [
migrations.AddField(
model_name="document",
name="has_deleted_children",
field=models.BooleanField(default=False),
),
]

View File

@@ -96,7 +96,7 @@ class LinkReachChoices(models.TextChoices):
"""
# If no ancestors, return all options
if not ancestors_links:
return {reach: LinkRoleChoices.values for reach in cls.values}
return dict.fromkeys(cls.values, LinkRoleChoices.values)
# Initialize result with all possible reaches and role options as sets
result = {reach: set(LinkRoleChoices.values) for reach in cls.values}
@@ -486,6 +486,7 @@ class Document(MP_Node, BaseModel):
)
deleted_at = models.DateTimeField(null=True, blank=True)
ancestors_deleted_at = models.DateTimeField(null=True, blank=True)
has_deleted_children = models.BooleanField(default=False)
_content = None
@@ -546,6 +547,12 @@ class Document(MP_Node, BaseModel):
content_file = ContentFile(bytes_content)
default_storage.save(file_key, content_file)
def is_leaf(self):
"""
:returns: True if the node is has no children
"""
return not self.has_deleted_children and self.numchild == 0
@property
def key_base(self):
"""Key base of the location where the document is stored in object storage."""
@@ -582,9 +589,13 @@ class Document(MP_Node, BaseModel):
def get_content_response(self, version_id=""):
"""Get the content in a specific version of the document"""
return default_storage.connection.meta.client.get_object(
Bucket=default_storage.bucket_name, Key=self.file_key, VersionId=version_id
)
params = {
"Bucket": default_storage.bucket_name,
"Key": self.file_key,
}
if version_id:
params["VersionId"] = version_id
return default_storage.connection.meta.client.get_object(**params)
def get_versions_slice(self, from_version_id="", min_datetime=None, page_size=None):
"""Get document versions from object storage with pagination and starting conditions"""
@@ -899,7 +910,8 @@ class Document(MP_Node, BaseModel):
if self.depth > 1:
self._meta.model.objects.filter(pk=self.get_parent().pk).update(
numchild=models.F("numchild") - 1
numchild=models.F("numchild") - 1,
has_deleted_children=True,
)
# Mark all descendants as soft deleted

View File

@@ -1,6 +1,7 @@
"""Test on the CORS proxy API for documents."""
import pytest
import responses
from rest_framework.test import APIClient
from core import factories
@@ -8,17 +9,24 @@ from core import factories
pytestmark = pytest.mark.django_db
@responses.activate
def test_api_docs_cors_proxy_valid_url():
"""Test the CORS proxy API for documents with a valid URL."""
document = factories.DocumentFactory(link_reach="public")
client = APIClient()
url_to_fetch = "https://docs.numerique.gouv.fr/assets/logo-gouv.png"
url_to_fetch = "https://external-url.com/assets/logo-gouv.png"
responses.get(url_to_fetch, body=b"", status=200, content_type="image/png")
response = client.get(
f"/api/v1.0/documents/{document.id!s}/cors-proxy/?url={url_to_fetch}"
)
assert response.status_code == 200
assert response.headers["Content-Type"] == "image/png"
assert response.headers["Content-Disposition"] == "attachment;"
assert (
response.headers["Content-Security-Policy"]
== "default-src 'none'; img-src 'none' data:;"
)
assert response.streaming_content
@@ -32,12 +40,14 @@ def test_api_docs_cors_proxy_without_url_query_string():
assert response.json() == {"detail": "Missing 'url' query parameter"}
@responses.activate
def test_api_docs_cors_proxy_anonymous_document_not_public():
"""Test the CORS proxy API for documents with an anonymous user and a non-public document."""
document = factories.DocumentFactory(link_reach="authenticated")
client = APIClient()
url_to_fetch = "https://docs.numerique.gouv.fr/assets/logo-gouv.png"
url_to_fetch = "https://external-url.com/assets/logo-gouv.png"
responses.get(url_to_fetch, body=b"", status=200, content_type="image/png")
response = client.get(
f"/api/v1.0/documents/{document.id!s}/cors-proxy/?url={url_to_fetch}"
)
@@ -47,6 +57,7 @@ def test_api_docs_cors_proxy_anonymous_document_not_public():
}
@responses.activate
def test_api_docs_cors_proxy_authenticated_user_accessing_protected_doc():
"""
Test the CORS proxy API for documents with an authenticated user accessing a protected
@@ -58,15 +69,22 @@ def test_api_docs_cors_proxy_authenticated_user_accessing_protected_doc():
client = APIClient()
client.force_login(user)
url_to_fetch = "https://docs.numerique.gouv.fr/assets/logo-gouv.png"
url_to_fetch = "https://external-url.com/assets/logo-gouv.png"
responses.get(url_to_fetch, body=b"", status=200, content_type="image/png")
response = client.get(
f"/api/v1.0/documents/{document.id!s}/cors-proxy/?url={url_to_fetch}"
)
assert response.status_code == 200
assert response.headers["Content-Type"] == "image/png"
assert response.headers["Content-Disposition"] == "attachment;"
assert (
response.headers["Content-Security-Policy"]
== "default-src 'none'; img-src 'none' data:;"
)
assert response.streaming_content
@responses.activate
def test_api_docs_cors_proxy_authenticated_not_accessing_restricted_doc():
"""
Test the CORS proxy API for documents with an authenticated user not accessing a restricted
@@ -78,7 +96,8 @@ def test_api_docs_cors_proxy_authenticated_not_accessing_restricted_doc():
client = APIClient()
client.force_login(user)
url_to_fetch = "https://docs.numerique.gouv.fr/assets/logo-gouv.png"
url_to_fetch = "https://external-url.com/assets/logo-gouv.png"
responses.get(url_to_fetch, body=b"", status=200, content_type="image/png")
response = client.get(
f"/api/v1.0/documents/{document.id!s}/cors-proxy/?url={url_to_fetch}"
)
@@ -86,3 +105,17 @@ def test_api_docs_cors_proxy_authenticated_not_accessing_restricted_doc():
assert response.json() == {
"detail": "You do not have permission to perform this action."
}
@responses.activate
def test_api_docs_cors_proxy_unsupported_media_type():
"""Test the CORS proxy API for documents with an unsupported media type."""
document = factories.DocumentFactory(link_reach="public")
client = APIClient()
url_to_fetch = "https://external-url.com/assets/index.html"
responses.get(url_to_fetch, body=b"", status=200, content_type="text/html")
response = client.get(
f"/api/v1.0/documents/{document.id!s}/cors-proxy/?url={url_to_fetch}"
)
assert response.status_code == 415

View File

@@ -24,7 +24,7 @@ def test_api_users_list_anonymous():
def test_api_users_list_authenticated():
"""
Authenticated users should be able to list users.
Authenticated users should not be able to list users without a query.
"""
user = factories.UserFactory()
@@ -37,7 +37,7 @@ def test_api_users_list_authenticated():
)
assert response.status_code == 200
content = response.json()
assert len(content["results"]) == 3
assert content == []
def test_api_users_list_query_email():
@@ -58,24 +58,76 @@ def test_api_users_list_query_email():
"/api/v1.0/users/?q=david.bowman@work.com",
)
assert response.status_code == 200
user_ids = [user["id"] for user in response.json()["results"]]
user_ids = [user["id"] for user in response.json()]
assert user_ids == [str(dave.id)]
response = client.get(
"/api/v1.0/users/?q=davig.bovman@worm.com",
)
assert response.status_code == 200
user_ids = [user["id"] for user in response.json()["results"]]
user_ids = [user["id"] for user in response.json()]
assert user_ids == [str(dave.id)]
response = client.get(
"/api/v1.0/users/?q=davig.bovman@worm.cop",
)
assert response.status_code == 200
user_ids = [user["id"] for user in response.json()["results"]]
user_ids = [user["id"] for user in response.json()]
assert user_ids == []
def test_api_users_list_limit(settings):
"""
Authenticated users should be able to list users and the number of results
should be limited to 10.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
# Use a base name with a length equal 5 to test that the limit is applied
base_name = "alice"
for i in range(15):
factories.UserFactory(email=f"{base_name}.{i}@example.com")
response = client.get(
"/api/v1.0/users/?q=alice",
)
assert response.status_code == 200
assert len(response.json()) == 5
# if the limit is changed, all users should be returned
settings.API_USERS_LIST_LIMIT = 100
response = client.get(
"/api/v1.0/users/?q=alice",
)
assert response.status_code == 200
assert len(response.json()) == 15
def test_api_users_list_throttling_authenticated(settings):
"""
Authenticated users should be throttled.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["user_list_burst"] = "3/minute"
for _i in range(3):
response = client.get(
"/api/v1.0/users/?q=alice",
)
assert response.status_code == 200
response = client.get(
"/api/v1.0/users/?q=alice",
)
assert response.status_code == 429
def test_api_users_list_query_email_matching():
"""While filtering by email, results should be filtered and sorted by Levenstein distance."""
user = factories.UserFactory()
@@ -94,13 +146,13 @@ def test_api_users_list_query_email_matching():
"/api/v1.0/users/?q=alice.johnson@example.gouv.fr",
)
assert response.status_code == 200
user_ids = [user["id"] for user in response.json()["results"]]
user_ids = [user["id"] for user in response.json()]
assert user_ids == [str(user1.id), str(user2.id), str(user3.id), str(user4.id)]
response = client.get("/api/v1.0/users/?q=alicia.johnnson@example.gouv.fr")
assert response.status_code == 200
user_ids = [user["id"] for user in response.json()["results"]]
user_ids = [user["id"] for user in response.json()]
assert user_ids == [str(user4.id), str(user2.id), str(user1.id), str(user5.id)]
@@ -126,10 +178,50 @@ def test_api_users_list_query_email_exclude_doc_user():
)
assert response.status_code == 200
user_ids = [user["id"] for user in response.json()["results"]]
user_ids = [user["id"] for user in response.json()]
assert user_ids == [str(nicole_fool.id)]
def test_api_users_list_query_short_queries():
"""
Queries shorter than 5 characters should return an empty result set.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
factories.UserFactory(email="john.doe@example.com")
factories.UserFactory(email="john.lennon@example.com")
response = client.get("/api/v1.0/users/?q=jo")
assert response.status_code == 200
assert response.json() == []
response = client.get("/api/v1.0/users/?q=john")
assert response.status_code == 200
assert response.json() == []
response = client.get("/api/v1.0/users/?q=john.")
assert response.status_code == 200
assert len(response.json()) == 2
def test_api_users_list_query_inactive():
"""Inactive users should not be listed."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
factories.UserFactory(email="john.doe@example.com", is_active=False)
lennon = factories.UserFactory(email="john.lennon@example.com")
response = client.get("/api/v1.0/users/?q=john.")
assert response.status_code == 200
user_ids = [user["id"] for user in response.json()]
assert user_ids == [str(lennon.id)]
def test_api_users_retrieve_me_anonymous():
"""Anonymous users should not be allowed to list users."""
factories.UserFactory.create_batch(2)

View File

@@ -1297,3 +1297,47 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
def test_models_documents_get_select_options(ancestors_links, select_options):
"""Validate that the "get_select_options" method operates as expected."""
assert models.LinkReachChoices.get_select_options(ancestors_links) == select_options
def test_models_documents_children_create_after_sibling_deletion():
"""
It should be possible to create a new child after all children have been deleted.
"""
root = factories.DocumentFactory()
assert root.numchild == 0
assert root.has_deleted_children is False
assert root.is_leaf() is True
child1 = factories.DocumentFactory(parent=root)
child2 = factories.DocumentFactory(parent=root)
root.refresh_from_db()
assert root.numchild == 2
assert root.has_deleted_children is False
assert root.is_leaf() is False
child1.soft_delete()
child2.soft_delete()
root.refresh_from_db()
assert root.numchild == 0
assert root.has_deleted_children is True
assert root.is_leaf() is False
factories.DocumentFactory(parent=root)
root.refresh_from_db()
assert root.numchild == 1
assert root.has_deleted_children is True
assert root.is_leaf() is False
def test_models_documents_has_deleted_children():
"""
A document should have its has_deleted_children attribute set to True if one of its children
has been solf deleted no matter if numchild is 0 or not.
"""
root = factories.DocumentFactory()
child = factories.DocumentFactory(parent=root)
assert root.has_deleted_children is False
child.soft_delete()
root.refresh_from_db()
assert root.has_deleted_children is True

View File

@@ -337,6 +337,18 @@ class Base(Configuration):
"PAGE_SIZE": 20,
"DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.URLPathVersioning",
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
"DEFAULT_THROTTLE_RATES": {
"user_list_sustained": values.Value(
default="180/hour",
environ_name="API_USERS_LIST_THROTTLE_RATE_SUSTAINED",
environ_prefix=None,
),
"user_list_burst": values.Value(
default="30/minute",
environ_name="API_USERS_LIST_THROTTLE_RATE_BURST",
environ_prefix=None,
),
},
}
SPECTACULAR_SETTINGS = {
@@ -604,6 +616,12 @@ class Base(Configuration):
},
}
API_USERS_LIST_LIMIT = values.PositiveIntegerValue(
default=5,
environ_name="API_USERS_LIST_LIMIT",
environ_prefix=None,
)
# pylint: disable=invalid-name
@property
def ENVIRONMENT(self):

View File

@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "impress"
version = "2.5.0"
version = "2.6.0"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -25,7 +25,7 @@ license = { file = "LICENSE" }
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"boto3==1.37.5",
"boto3==1.37.18",
"Brotli==1.1.0",
"celery[redis]==5.4.0",
"django-configurations==2.5.1",
@@ -48,12 +48,12 @@ dependencies = [
"jsonschema==4.23.0",
"markdown==3.7",
"nested-multipart-parser==1.5.0",
"openai==1.65.2",
"psycopg[binary]==3.2.5",
"openai==1.68.2",
"psycopg[binary]==3.2.6",
"PyJWT==2.10.1",
"python-magic==0.4.27",
"requests==2.32.3",
"sentry-sdk==2.22.0",
"sentry-sdk==2.24.0",
"url-normalize==1.4.3",
"whitenoise==6.9.0",
"mozilla-django-oidc==4.0.1",
@@ -72,18 +72,18 @@ dev = [
"drf-spectacular-sidecar==2025.3.1",
"freezegun==1.5.1",
"ipdb==0.13.13",
"ipython==9.0.1",
"pyfakefs==5.7.4",
"ipython==9.0.2",
"pyfakefs==5.8.0",
"pylint-django==2.6.1",
"pylint==3.3.4",
"pylint==3.3.6",
"pytest-cov==6.0.0",
"pytest-django==4.10.0",
"pytest==8.3.5",
"pytest-icdiff==0.9",
"pytest-xdist==3.6.1",
"responses==0.25.6",
"ruff==0.9.9",
"types-requests==2.32.0.20250301",
"responses==0.25.7",
"ruff==0.11.2",
"types-requests==2.32.0.20250306",
]
[tool.setuptools]

View File

@@ -80,11 +80,11 @@ export const addNewMember = async (
page: Page,
index: number,
role: 'Administrator' | 'Owner' | 'Member' | 'Editor' | 'Reader',
fillText: string = 'user',
fillText: string = 'user ',
) => {
const responsePromiseSearchUser = page.waitForResponse(
(response) =>
response.url().includes(`/users/?q=${fillText}`) &&
response.url().includes(`/users/?q=${encodeURIComponent(fillText)}`) &&
response.status() === 200,
);
@@ -97,7 +97,7 @@ export const addNewMember = async (
// Intercept response
const responseSearchUser = await responsePromiseSearchUser;
const users = (await responseSearchUser.json()).results as {
const users = (await responseSearchUser.json()) as {
email: string;
}[];
@@ -200,6 +200,22 @@ export const mockedDocument = async (page: Page, json: object) => {
});
};
export const mockedListDocs = async (page: Page, data: object[] = []) => {
await page.route('**/documents/**/', async (route) => {
const request = route.request();
if (request.method().includes('GET') && request.url().includes('page=')) {
await route.fulfill({
json: {
count: data.length,
next: null,
previous: null,
results: data,
},
});
}
});
};
export const mockedInvitations = async (page: Page, json?: object) => {
await page.route('**/invitations/**/', async (route) => {
const request = route.request();

View File

@@ -8,7 +8,7 @@ const config = {
CRISP_WEBSITE_ID: null,
COLLABORATION_WS_URL: 'ws://localhost:8083/collaboration/ws/',
ENVIRONMENT: 'development',
FRONTEND_THEME: 'dsfr',
FRONTEND_THEME: 'default',
MEDIA_BASE_URL: 'http://localhost:8083',
LANGUAGES: [
['en-us', 'English'],
@@ -159,7 +159,7 @@ test.describe('Config: Not loggued', () => {
expect(response.ok()).toBeTruthy();
const jsonResponse = await response.json();
expect(jsonResponse.FRONTEND_THEME).toStrictEqual('dsfr');
expect(jsonResponse.FRONTEND_THEME).toStrictEqual('default');
const footer = page.locator('footer').first();
// alt 'Gouvernement Logo' comes from the theme

View File

@@ -0,0 +1,309 @@
import { expect, test } from '@playwright/test';
import { createDoc, mockedListDocs } from './common';
test.describe('Doc grid dnd', () => {
test('it creates a doc', async ({ page, browserName }) => {
await page.goto('/');
const header = page.locator('header').first();
await createDoc(page, 'Draggable doc', browserName, 1);
await header.locator('h2').getByText('Docs').click();
await createDoc(page, 'Droppable doc', browserName, 1);
await header.locator('h2').getByText('Docs').click();
const response = await page.waitForResponse(
(response) =>
response.url().endsWith('documents/?page=1') &&
response.status() === 200,
);
const responseJson = await response.json();
const items = responseJson.results;
const docsGrid = page.getByTestId('docs-grid');
await expect(docsGrid).toBeVisible();
await expect(page.getByTestId('grid-loader')).toBeHidden();
const draggableElement = page.getByTestId(`draggable-doc-${items[1].id}`);
const dropZone = page.getByTestId(`droppable-doc-${items[0].id}`);
await expect(draggableElement).toBeVisible();
await expect(dropZone).toBeVisible();
// Obtenir les positions des éléments
const draggableBoundingBox = await draggableElement.boundingBox();
const dropZoneBoundingBox = await dropZone.boundingBox();
expect(draggableBoundingBox).toBeDefined();
expect(dropZoneBoundingBox).toBeDefined();
// eslint-disable-next-line playwright/no-conditional-in-test
if (!draggableBoundingBox || !dropZoneBoundingBox) {
throw new Error('Impossible de déterminer la position des éléments');
}
await page.mouse.move(
draggableBoundingBox.x + draggableBoundingBox.width / 2,
draggableBoundingBox.y + draggableBoundingBox.height / 2,
);
await page.mouse.down();
// Déplacer vers la zone cible
await page.mouse.move(
dropZoneBoundingBox.x + dropZoneBoundingBox.width / 2,
dropZoneBoundingBox.y + dropZoneBoundingBox.height / 2,
{ steps: 10 }, // Rendre le mouvement plus fluide
);
const dragOverlay = page.getByTestId('drag-doc-overlay');
await expect(dragOverlay).toBeVisible();
await expect(dragOverlay).toHaveText(items[1].title as string);
await page.mouse.up();
await expect(dragOverlay).toBeHidden();
});
test('it checks cant drop when we have not the minimum role', async ({
page,
}) => {
await mockedListDocs(page, data);
await page.goto('/');
const docsGrid = page.getByTestId('docs-grid');
await expect(docsGrid).toBeVisible();
await expect(page.getByTestId('grid-loader')).toBeHidden();
const canDropAndDrag = page.getByTestId('droppable-doc-can-drop-and-drag');
const noDropAndNoDrag = page.getByTestId(
'droppable-doc-no-drop-and-no-drag',
);
await expect(canDropAndDrag).toBeVisible();
await expect(noDropAndNoDrag).toBeVisible();
const canDropAndDragBoundigBox = await canDropAndDrag.boundingBox();
const noDropAndNoDragBoundigBox = await noDropAndNoDrag.boundingBox();
if (!canDropAndDragBoundigBox || !noDropAndNoDragBoundigBox) {
throw new Error('Impossible de déterminer la position des éléments');
}
await page.mouse.move(
canDropAndDragBoundigBox.x + canDropAndDragBoundigBox.width / 2,
canDropAndDragBoundigBox.y + canDropAndDragBoundigBox.height / 2,
);
await page.mouse.down();
await page.mouse.move(
noDropAndNoDragBoundigBox.x + noDropAndNoDragBoundigBox.width / 2,
noDropAndNoDragBoundigBox.y + noDropAndNoDragBoundigBox.height / 2,
{ steps: 10 },
);
const dragOverlay = page.getByTestId('drag-doc-overlay');
await expect(dragOverlay).toBeVisible();
await expect(dragOverlay).toHaveText(
'You must be at least the editor of the target document',
);
await page.mouse.up();
});
test('it checks cant drag when we have not the minimum role', async ({
page,
}) => {
await mockedListDocs(page, data);
await page.goto('/');
const docsGrid = page.getByTestId('docs-grid');
await expect(docsGrid).toBeVisible();
await expect(page.getByTestId('grid-loader')).toBeHidden();
const canDropAndDrag = page.getByTestId('droppable-doc-can-drop-and-drag');
const noDropAndNoDrag = page.getByTestId(
'droppable-doc-no-drop-and-no-drag',
);
await expect(canDropAndDrag).toBeVisible();
await expect(noDropAndNoDrag).toBeVisible();
const canDropAndDragBoundigBox = await canDropAndDrag.boundingBox();
const noDropAndNoDragBoundigBox = await noDropAndNoDrag.boundingBox();
if (!canDropAndDragBoundigBox || !noDropAndNoDragBoundigBox) {
throw new Error('Impossible de déterminer la position des éléments');
}
await page.mouse.move(
noDropAndNoDragBoundigBox.x + noDropAndNoDragBoundigBox.width / 2,
noDropAndNoDragBoundigBox.y + noDropAndNoDragBoundigBox.height / 2,
);
await page.mouse.down();
await page.mouse.move(
canDropAndDragBoundigBox.x + canDropAndDragBoundigBox.width / 2,
canDropAndDragBoundigBox.y + canDropAndDragBoundigBox.height / 2,
{ steps: 10 },
);
const dragOverlay = page.getByTestId('drag-doc-overlay');
await expect(dragOverlay).toBeVisible();
await expect(dragOverlay).toHaveText(
'You must be the owner to move the document',
);
await page.mouse.up();
});
});
const data = [
{
id: 'can-drop-and-drag',
abilities: {
accesses_manage: true,
accesses_view: true,
ai_transform: true,
ai_translate: true,
attachment_upload: true,
children_list: true,
children_create: true,
collaboration_auth: true,
descendants: true,
destroy: true,
favorite: true,
link_configuration: true,
invite_owner: true,
move: true,
partial_update: true,
restore: true,
retrieve: true,
media_auth: true,
link_select_options: {
restricted: ['reader', 'editor'],
authenticated: ['reader', 'editor'],
public: ['reader', 'editor'],
},
tree: true,
update: true,
versions_destroy: true,
versions_list: true,
versions_retrieve: true,
},
created_at: '2025-03-14T14:45:22.527221Z',
creator: 'bc6895e0-8f6d-4b00-827d-c143aa6b2ecb',
depth: 1,
excerpt: null,
is_favorite: false,
link_role: 'reader',
link_reach: 'restricted',
nb_accesses_ancestors: 1,
nb_accesses_direct: 1,
numchild: 5,
path: '000000o',
title: 'Can drop and drag',
updated_at: '2025-03-14T14:45:27.699542Z',
user_roles: ['owner'],
},
{
id: 'can-only-drop',
title: 'Can only drop',
abilities: {
accesses_manage: true,
accesses_view: true,
ai_transform: true,
ai_translate: true,
attachment_upload: true,
children_list: true,
children_create: true,
collaboration_auth: true,
descendants: true,
destroy: true,
favorite: true,
link_configuration: true,
invite_owner: true,
move: true,
partial_update: true,
restore: true,
retrieve: true,
media_auth: true,
link_select_options: {
restricted: ['reader', 'editor'],
authenticated: ['reader', 'editor'],
public: ['reader', 'editor'],
},
tree: true,
update: true,
versions_destroy: true,
versions_list: true,
versions_retrieve: true,
},
created_at: '2025-03-14T14:45:22.527221Z',
creator: 'bc6895e0-8f6d-4b00-827d-c143aa6b2ecb',
depth: 1,
excerpt: null,
is_favorite: false,
link_role: 'reader',
link_reach: 'restricted',
nb_accesses_ancestors: 1,
nb_accesses_direct: 1,
numchild: 5,
path: '000000o',
updated_at: '2025-03-14T14:45:27.699542Z',
user_roles: ['editor'],
},
{
id: 'no-drop-and-no-drag',
abilities: {
accesses_manage: false,
accesses_view: true,
ai_transform: false,
ai_translate: false,
attachment_upload: false,
children_list: true,
children_create: false,
collaboration_auth: true,
descendants: true,
destroy: false,
favorite: true,
link_configuration: false,
invite_owner: false,
move: false,
partial_update: false,
restore: false,
retrieve: true,
media_auth: true,
link_select_options: {
restricted: ['reader', 'editor'],
authenticated: ['reader', 'editor'],
public: ['reader', 'editor'],
},
tree: true,
update: false,
versions_destroy: false,
versions_list: true,
versions_retrieve: true,
},
created_at: '2025-03-14T14:44:16.032773Z',
creator: '9264f420-f018-4bd6-96ae-4788f41af56d',
depth: 1,
excerpt: null,
is_favorite: false,
link_role: 'reader',
link_reach: 'restricted',
nb_accesses_ancestors: 14,
nb_accesses_direct: 14,
numchild: 0,
path: '000000l',
title: 'No drop and no drag',
updated_at: '2025-03-14T14:44:16.032774Z',
user_roles: ['reader'],
},
];

View File

@@ -8,9 +8,11 @@ test.beforeEach(async ({ page }) => {
test.describe('Document create member', () => {
test('it selects 2 users and 1 invitation', async ({ page, browserName }) => {
const inputFill = 'user ';
const responsePromise = page.waitForResponse(
(response) =>
response.url().includes('/users/?q=user') && response.status() === 200,
response.url().includes(`/users/?q=${encodeURIComponent(inputFill)}`) &&
response.status() === 200,
);
await createDoc(page, 'select-multi-users', browserName, 1);
@@ -22,9 +24,9 @@ test.describe('Document create member', () => {
await expect(inputSearch).toBeVisible();
// Select user 1 and verify tag
await inputSearch.fill('user');
await inputSearch.fill(inputFill);
const response = await responsePromise;
const users = (await response.json()).results as {
const users = (await response.json()) as {
email: string;
full_name?: string | null;
}[];
@@ -45,7 +47,7 @@ test.describe('Document create member', () => {
).toBeVisible();
// Select user 2 and verify tag
await inputSearch.fill('user');
await inputSearch.fill(inputFill);
await quickSearchContent
.getByTestId(`search-user-row-${users[1].email}`)
.click();

View File

@@ -63,4 +63,35 @@ test.describe('Document search', () => {
listSearch.getByRole('option').getByText(doc2Title),
).toBeHidden();
});
test('it checks cmd+k modal search interaction', async ({
page,
browserName,
}) => {
const [doc1Title] = await createDoc(
page,
'Doc seack ctrl k',
browserName,
1,
);
await verifyDocName(page, doc1Title);
await page.keyboard.press('Control+k');
await expect(
page.getByLabel('Search modal').getByText('search'),
).toBeVisible();
await page.keyboard.press('Escape');
const editor = page.locator('.ProseMirror');
await editor.click();
await editor.fill('Hello world');
await editor.getByText('Hello world').dblclick();
await page.keyboard.press('Control+k');
await expect(page.getByRole('textbox', { name: 'Edit URL' })).toBeVisible();
await expect(
page.getByLabel('Search modal').getByText('search'),
).toBeHidden();
});
});

View File

@@ -0,0 +1,149 @@
/* eslint-disable playwright/no-conditional-in-test */
import { expect, test } from '@playwright/test';
import { createDoc } from './common';
test.describe('Doc Tree', () => {
test('create new sub pages', async ({ page, browserName }) => {
await page.goto('/');
await createDoc(page, 'doc-tree-content', browserName, 1);
const addButton = page.getByRole('button', { name: 'New page' });
const docTree = page.getByTestId('doc-tree');
await expect(addButton).toBeVisible();
// Attendre et intercepter la requête POST pour créer une nouvelle page
const responsePromise = page.waitForResponse(
(response) =>
response.url().includes('/documents/') &&
response.url().includes('/children/') &&
response.request().method() === 'POST',
);
await addButton.click();
const response = await responsePromise;
expect(response.ok()).toBeTruthy();
const subPageJson = await response.json();
await expect(docTree).toBeVisible();
const subPageItem = docTree
.getByTestId(`doc-sub-page-item-${subPageJson.id}`)
.first();
await expect(subPageItem).toBeVisible();
await subPageItem.click();
const input = page.getByRole('textbox', { name: 'doc title input' });
await input.click();
await input.fill('Test');
await input.press('Enter');
await expect(subPageItem.getByText('Test')).toBeVisible();
await page.reload();
await expect(subPageItem.getByText('Test')).toBeVisible();
});
test('check the reorder of sub pages', async ({ page, browserName }) => {
await page.goto('/');
await createDoc(page, 'doc-tree-content', browserName, 1);
const addButton = page.getByRole('button', { name: 'New page' });
await expect(addButton).toBeVisible();
const docTree = page.getByTestId('doc-tree');
// Create first sub page
const firstResponsePromise = page.waitForResponse(
(response) =>
response.url().includes('/documents/') &&
response.url().includes('/children/') &&
response.request().method() === 'POST',
);
await addButton.click();
const firstResponse = await firstResponsePromise;
expect(firstResponse.ok()).toBeTruthy();
const secondResponsePromise = page.waitForResponse(
(response) =>
response.url().includes('/documents/') &&
response.url().includes('/children/') &&
response.request().method() === 'POST',
);
// Create second sub page
await addButton.click();
const secondResponse = await secondResponsePromise;
expect(secondResponse.ok()).toBeTruthy();
const secondSubPageJson = await secondResponse.json();
const firstSubPageJson = await firstResponse.json();
const firstSubPageItem = docTree
.getByTestId(`doc-sub-page-item-${firstSubPageJson.id}`)
.first();
const secondSubPageItem = docTree
.getByTestId(`doc-sub-page-item-${secondSubPageJson.id}`)
.first();
// check that the sub pages are visible in the tree
await expect(firstSubPageItem).toBeVisible();
await expect(secondSubPageItem).toBeVisible();
// get the bounding boxes of the sub pages
const firstSubPageBoundingBox = await firstSubPageItem.boundingBox();
const secondSubPageBoundingBox = await secondSubPageItem.boundingBox();
expect(firstSubPageBoundingBox).toBeDefined();
expect(secondSubPageBoundingBox).toBeDefined();
if (!firstSubPageBoundingBox || !secondSubPageBoundingBox) {
throw new Error('Impossible de déterminer la position des éléments');
}
// move the first sub page to the second position
await page.mouse.move(
firstSubPageBoundingBox.x + firstSubPageBoundingBox.width / 2,
firstSubPageBoundingBox.y + firstSubPageBoundingBox.height / 2,
);
await page.mouse.down();
await page.mouse.move(
secondSubPageBoundingBox.x + secondSubPageBoundingBox.width / 2,
secondSubPageBoundingBox.y + secondSubPageBoundingBox.height + 4,
{ steps: 10 },
);
await page.mouse.up();
// check that the sub pages are visible in the tree
await expect(firstSubPageItem).toBeVisible();
await expect(secondSubPageItem).toBeVisible();
// reload the page
await page.reload();
// check that the sub pages are visible in the tree
await expect(firstSubPageItem).toBeVisible();
await expect(secondSubPageItem).toBeVisible();
// Check the position of the sub pages
const allSubPageItems = await docTree
.getByTestId(/^doc-sub-page-item/)
.all();
expect(allSubPageItems.length).toBe(2);
// Vérifier que le premier élément a l'ID de la deuxième sous-page après le drag and drop
await expect(allSubPageItems[0]).toHaveAttribute(
'data-testid',
`doc-sub-page-item-${secondSubPageJson.id}`,
);
// Vérifier que le deuxième élément a l'ID de la première sous-page après le drag and drop
await expect(allSubPageItems[1]).toHaveAttribute(
'data-testid',
`doc-sub-page-item-${firstSubPageJson.id}`,
);
});
});

View File

@@ -1,6 +1,6 @@
{
"name": "app-e2e",
"version": "2.5.0",
"version": "2.6.0",
"private": true,
"scripts": {
"lint": "eslint . --ext .ts",

View File

@@ -1,492 +1,58 @@
const config = {
themes: {
default: {
theme: {
colors: {
'card-border': '#ededed',
'primary-bg': '#FAFAFA',
'primary-action': '#1212FF',
'primary-050': '#F5F5FE',
'primary-100': '#EDF5FA',
'primary-150': '#E5EEFA',
'primary-950': '#1B1B35',
'info-150': '#E5EEFA',
'greyscale-000': '#fff',
'greyscale-1000': '#161616',
'blue-400': '#7AB1E8',
'blue-500': '#417DC4',
'blue-600': '#3558A2',
'brown-400': '#E6BE92',
'brown-500': '#BD987A',
'brown-600': '#745B47',
'cyan-400': '#34BAB5',
'cyan-500': '#009099',
'cyan-600': '#006A6F',
'gold-400': '#FFCA00',
'gold-500': '#C3992A',
'gold-600': '#695240',
'green-400': '#34CB6A',
'green-500': '#00A95F',
'green-600': '#297254',
'olive-400': '#99C221',
'olive-500': '#68A532',
'olive-600': '#447049',
'orange-400': '#FF732C',
'orange-500': '#E4794A',
'orange-600': '#755348',
'pink-400': '#FFB7AE',
'pink-500': '#E18B76',
'pink-600': '#8D533E',
'purple-400': '#CE70CC',
'purple-500': '#A558A0',
'purple-600': '#6E445A',
'yellow-400': '#D8C634',
'yellow-500': '#B7A73F',
'yellow-600': '#66673D',
},
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,
},
},
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',
xxxl: '3.5rem',
'4xl': '4rem',
'5xl': '4.5rem',
'6xl': '6rem',
'7xl': '7.5rem',
},
breakpoints: {
xxs: '320px',
xs: '480px',
},
logo: {
src: '',
widthHeader: '',
widthFooter: '',
alt: '',
},
},
components: {
datagrid: {
header: {
weight: 'var(--c--theme--font--weights--extrabold)',
size: 'var(--c--theme--font--sizes--ml)',
},
cell: {
color: 'var(--c--theme--colors--primary-500)',
size: 'var(--c--theme--font--sizes--ml)',
},
},
'forms-checkbox': {
'background-color': {
hover: '#055fd214',
},
color: 'var(--c--theme--colors--primary-500)',
'font-size': 'var(--c--theme--font--sizes--ml)',
},
'forms-datepicker': {
'border-color': 'var(--c--theme--colors--primary-500)',
'value-color': 'var(--c--theme--colors--primary-500)',
'border-radius': {
hover: 'var(--c--components--forms-datepicker--border-radius)',
focus: 'var(--c--components--forms-datepicker--border-radius)',
},
},
'forms-field': {
color: 'var(--c--theme--colors--primary-500)',
'value-color': 'var(--c--theme--colors--primary-500)',
width: 'auto',
},
'forms-input': {
'value-color': 'var(--c--theme--colors--primary-500)',
'border-color': 'var(--c--theme--colors--primary-500)',
color: {
error: 'var(--c--theme--colors--danger-500)',
'error-hover': 'var(--c--theme--colors--danger-500)',
'box-shadow-error-hover': 'var(--c--theme--colors--danger-500)',
},
},
'forms-labelledbox': {
'label-color': {
small: 'var(--c--theme--colors--primary-500)',
'small-disabled': 'var(--c--theme--colors--greyscale-400)',
big: {
disabled: 'var(--c--theme--colors--greyscale-400)',
},
},
},
'forms-select': {
'border-color': 'var(--c--theme--colors--primary-500)',
'border-color-disabled-hover':
'var(--c--theme--colors--greyscale-200)',
'border-radius': {
hover: 'var(--c--components--forms-select--border-radius)',
focus: 'var(--c--components--forms-select--border-radius)',
},
'font-size': 'var(--c--theme--font--sizes--ml)',
'menu-background-color': '#fff',
'item-background-color': {
hover: 'var(--c--theme--colors--primary-300)',
},
},
'forms-switch': {
'accent-color': 'var(--c--theme--colors--primary-400)',
},
'forms-textarea': {
'border-color': 'var(--c--components--forms-textarea--border-color)',
'border-color-hover':
'var(--c--components--forms-textarea--border-color)',
'border-radius': {
hover: 'var(--c--components--forms-textarea--border-radius)',
focus: 'var(--c--components--forms-textarea--border-radius)',
},
color: 'var(--c--theme--colors--primary-500)',
disabled: {
'border-color-hover': 'var(--c--theme--colors--greyscale-200)',
},
},
modal: {
'background-color': '#fff',
},
button: {
'border-radius': {
active: 'var(--c--components--button--border-radius)',
},
'medium-height': 'auto',
'small-height': 'auto',
success: {
color: 'white',
'color-disabled': 'white',
'color-hover': 'white',
background: {
color: 'var(--c--theme--colors--success-600)',
'color-disabled': 'var(--c--theme--colors--greyscale-300)',
'color-hover': 'var(--c--theme--colors--success-800)',
},
},
danger: {
'color-hover': 'white',
background: {
color: 'var(--c--theme--colors--danger-600)',
'color-hover': '#FF2725',
'color-disabled': 'var(--c--theme--colors--danger-100)',
},
},
primary: {
color: 'var(--c--theme--colors--primary-text)',
'color-active': 'var(--c--theme--colors--primary-text)',
background: {
color: 'var(--c--theme--colors--primary-400)',
'color-active': 'var(--c--theme--colors--primary-500)',
},
border: {
'color-active': 'transparent',
},
},
secondary: {
color: 'var(--c--theme--colors--primary-500)',
'color-hover': 'var(--c--theme--colors--primary-text)',
background: {
color: 'white',
'color-hover': 'var(--c--theme--colors--primary-700)',
},
border: {
color: 'var(--c--theme--colors--greyscale-300)',
},
},
tertiary: {
color: 'var(--c--theme--colors--primary-text)',
'color-disabled': 'var(--c--theme--colors--greyscale-600)',
background: {
color: 'var(--c--theme--colors--primary-100)',
'color-hover': 'var(--c--theme--colors--primary-300)',
'color-active': 'var(--c--theme--colors--primary-100)',
'color-disabled': 'var(--c--theme--colors--greyscale-200)',
},
},
disabled: {
color: 'white',
background: {
color: '#b3cef0',
},
},
},
'la-gauffre': {
activated: false,
},
'home-proconnect': {
activated: false,
},
},
import { cunninghamConfig } from '@gouvfr-lasuite/ui-kit';
const tokens = {
...cunninghamConfig,
};
const customColors = {
'primary-action': '#1212FF',
'primary-bg': '#FAFAFA',
'blue-400': '#7AB1E8',
'blue-500': '#417DC4',
'blue-600': '#3558A2',
'brown-400': '#E6BE92',
'brown-500': '#BD987A',
'brown-600': '#745B47',
'cyan-400': '#34BAB5',
'cyan-500': '#009099',
'cyan-600': '#006A6F',
'gold-400': '#FFCA00',
'gold-500': '#C3992A',
'gold-600': '#695240',
'green-400': '#34CB6A',
'green-500': '#00A95F',
'green-600': '#297254',
'olive-400': '#99C221',
'olive-500': '#68A532',
'olive-600': '#447049',
'orange-400': '#FF732C',
'orange-500': '#E4794A',
'orange-600': '#755348',
'pink-400': '#FFB7AE',
'pink-500': '#E18B76',
'pink-600': '#8D533E',
'purple-400': '#CE70CC',
'purple-500': '#A558A0',
'purple-600': '#6E445A',
'yellow-400': '#D8C634',
'yellow-500': '#B7A73F',
'yellow-600': '#66673D',
};
tokens.themes.default.theme.colors = {
...tokens.themes.default.theme.colors,
...customColors,
};
tokens.themes.default.components = {
...tokens.themes.default.components,
...{
'la-gauffre': {
activated: true,
},
dsfr: {
theme: {
colors: {
'card-border': '#E5E5E5',
'primary-text': '#000091',
'primary-100': '#ECECFE',
'primary-150': '#F4F4FD',
'primary-200': '#E3E3FD',
'primary-300': '#CACAFB',
'primary-400': '#8585F6',
'primary-500': '#6A6AF4',
'primary-600': '#313178',
'primary-700': '#272747',
'primary-800': '#000091',
'primary-900': '#21213F',
'secondary-text': '#fff',
'secondary-100': '#fee9ea',
'secondary-200': '#fedfdf',
'secondary-300': '#fdbfbf',
'secondary-400': '#e1020f',
'secondary-500': '#c91a1f',
'secondary-600': '#5e2b2b',
'secondary-700': '#3b2424',
'secondary-800': '#341f1f',
'secondary-900': '#2b1919',
'greyscale-text': '#303C4B',
'greyscale-000': '#fff',
'greyscale-050': '#F6F6F6',
'greyscale-100': '#eee',
'greyscale-200': '#E5E5E5',
'greyscale-250': '#ddd',
'greyscale-300': '#CECECE',
'greyscale-350': '#ddd',
'greyscale-400': '#929292',
'greyscale-500': '#7C7C7C',
'greyscale-600': '#666666',
'greyscale-700': '#3A3A3A',
'greyscale-750': '#353535',
'greyscale-800': '#2A2A2A',
'greyscale-900': '#242424',
'greyscale-950': '#1E1E1E',
'greyscale-1000': '#161616',
'success-text': '#1f8d49',
'success-100': '#dffee6',
'success-200': '#b8fec9',
'success-300': '#88fdaa',
'success-400': '#3bea7e',
'success-500': '#1f8d49',
'success-600': '#18753c',
'success-700': '#204129',
'success-800': '#1e2e22',
'success-900': '#19281d',
'info-text': '#0078f3',
'info-100': '#E8EDFF',
'info-200': '#DDE5FF',
'info-300': '#BCCDFF',
'info-400': '#518FFF',
'info-500': '#0078F3',
'info-600': '#0063CB',
'info-700': '#273961',
'info-800': '#222A3F',
'info-900': '#1D2437',
'warning-text': '#d64d00',
'warning-100': '#fff4f3',
'warning-200': '#ffe9e6',
'warning-300': '#ffded9',
'warning-400': '#ffbeb4',
'warning-500': '#d64d00',
'warning-600': '#b34000',
'warning-700': '#5e2c21',
'warning-800': '#3e241e',
'warning-900': '#361e19',
'danger-text': '#FFF',
'danger-100': '#FFE9E9',
'danger-200': '#FFDDDD',
'danger-300': '#FFBDBD',
'danger-400': '#FF5655',
'danger-500': '#F60700',
'danger-600': '#CE0500',
'danger-700': '#642626',
'danger-800': '#412121',
'danger-900': '#391C1C',
},
font: {
families: {
accent: 'Marianne',
base: 'Marianne',
},
},
logo: {
src: '/assets/logo-gouv.svg',
widthHeader: '110px',
widthFooter: '220px',
alt: 'Gouvernement Logo',
},
},
components: {
alert: {
'border-radius': '0',
error: {
'background-color': 'var(--c--theme--colors--danger-100)',
'border-left-color': 'var(--c--theme--colors--danger-400)',
close: {
color: 'white',
'background-color': 'var(--c--theme--colors--danger-400)',
'background-color-hover': 'var(--c--theme--colors--danger-600)',
},
},
},
modal: {
'width-small': '342px',
},
button: {
'medium-height': '40px',
'medium-text-height': '40px',
'border-radius': '4px',
primary: {
background: {
color: 'var(--c--theme--colors--primary-text)',
'color-hover': '#1212ff',
'color-active': '#2323ff',
},
color: '#fff',
'color-hover': '#fff',
'color-active': '#fff',
},
'primary-text': {
background: {
'color-hover': 'var(--c--theme--colors--primary-100)',
'color-active': 'var(--c--theme--colors--primary-100)',
},
'color-hover': 'var(--c--theme--colors--primary-text)',
color: 'var(--c--theme--colors--primary-800)',
},
secondary: {
background: {
'color-hover': '#F6F6F6',
'color-active': '#EDEDED',
},
border: {
color: 'var(--c--theme--colors--greyscale-300)',
'color-hover': 'var(--c--theme--colors--greyscale-300)',
},
color: 'var(--c--theme--colors--primary-text)',
},
'tertiary-text': {
background: {
'color-hover': 'var(--c--theme--colors--greyscale-100)',
},
'color-hover': 'var(--c--theme--colors--primary-text)',
color: 'var(--c--theme--colors--primary-600)',
},
},
datagrid: {
header: {
color: 'var(--c--theme--colors--primary-text)',
size: 'var(--c--theme--font--sizes--s)',
},
body: {
'background-color': 'transparent',
'background-color-hover': '#F4F4FD',
},
pagination: {
'background-color': 'transparent',
'background-color-active': 'var(--c--theme--colors--primary-300)',
'border-color': 'var(--c--theme--colors--primary-400)',
},
},
'forms-checkbox': {
'border-radius': '0',
color: 'var(--c--theme--colors--primary-text)',
text: {
color: 'var(--c--theme--colors--greyscale-text)',
size: 'var(--c--theme--font--sizes--t)',
},
},
'forms-datepicker': {
'border-radius': '0',
},
'forms-fileuploader': {
'border-radius': '0',
},
'forms-field': {
color: 'var(--c--theme--colors--primary-text)',
'footer-font-size': 'var(--c--theme--font--sizes--t)',
'footer-color': 'var(--c--theme--colors--greyscale-text)',
},
'forms-input': {
'border-radius': '4px',
'background-color': '#fff',
'border-color': 'var(--c--theme--colors--primary-text)',
'box-shadow-color': 'var(--c--theme--colors--primary-text)',
'value-color': 'var(--c--theme--colors--primary-text)',
'font-size': '14px',
},
'forms-labelledbox': {
'label-color': {
big: 'var(--c--theme--colors--primary-text)',
},
},
'forms-radio': {
'accent-color': 'var(--c--theme--colors--primary-600)',
},
'forms-select': {
'item-font-size': '14px',
'border-radius': '4px',
'border-radius-hover': '4px',
'background-color': '#fff',
'border-color': 'var(--c--theme--colors--primary-text)',
'border-color-hover': 'var(--c--theme--colors--primary-text)',
'box-shadow-color': 'var(--c--theme--colors--primary-text)',
},
'forms-switch': {
'handle-border-radius': '2px',
'rail-border-radius': '4px',
'accent-color': 'var(--c--theme--colors--primary-text)',
},
'forms-textarea': {
'border-radius': '0',
},
'la-gauffre': {
activated: true,
},
'home-proconnect': {
activated: true,
},
},
'home-proconnect': {
activated: true,
},
},
};
export default config;
export default tokens;

View File

@@ -1,6 +1,6 @@
{
"name": "app-impress",
"version": "2.5.0",
"version": "2.6.0",
"private": true,
"scripts": {
"dev": "next dev",
@@ -21,13 +21,18 @@
"@blocknote/react": "0.23.2-hotfix.0",
"@blocknote/xl-docx-exporter": "0.23.2-hotfix.0",
"@blocknote/xl-pdf-exporter": "0.23.2-hotfix.0",
"@dnd-kit/core": "6.3.1",
"@dnd-kit/modifiers": "9.0.0",
"@fontsource/material-icons": "5.2.5",
"@gouvfr-lasuite/integration": "1.0.2",
"@gouvfr-lasuite/ui-kit": "/Users/melde/Documents/societes/melde/clients/dinum/design-system",
"@hocuspocus/provider": "2.15.2",
"@openfun/cunningham-react": "3.0.0",
"@react-pdf/renderer": "4.1.6",
"@sentry/nextjs": "9.3.0",
"@tanstack/react-query": "5.67.1",
"canvg": "4.0.3",
"clsx": "2.1.1",
"cmdk": "1.0.4",
"crisp-sdk-web": "1.0.25",
"docx": "9.1.1",
@@ -44,6 +49,7 @@
"react-i18next": "15.4.1",
"react-intersection-observer": "9.15.1",
"react-select": "5.10.1",
"react-stately": "3.36.1",
"styled-components": "6.1.15",
"use-debounce": "10.0.4",
"y-protocols": "1.0.6",

View File

@@ -1,101 +0,0 @@
@font-face {
font-family: Marianne;
src:
url('Marianne-Thin.woff2') format('woff2'),
url('Marianne-Thin.woff') format('woff');
font-weight: 100;
}
@font-face {
font-family: Marianne;
src:
url('Marianne-Thin_Italic.woff2') format('woff2'),
url('Marianne-Thin_Italic.woff') format('woff');
font-weight: 100;
font-style: italic;
}
@font-face {
font-family: Marianne;
src:
url('Marianne-Light.woff2') format('woff2'),
url('Marianne-Light.woff') format('woff');
font-weight: 300;
}
@font-face {
font-family: Marianne;
src:
url('Marianne-Light_Italic.woff2') format('woff2'),
url('Marianne-Light_Italic.woff') format('woff');
font-weight: 300;
font-style: italic;
}
@font-face {
font-family: Marianne;
src:
url('Marianne-Regular.woff2') format('woff2'),
url('Marianne-Regular.woff') format('woff');
font-weight: 400;
}
@font-face {
font-family: Marianne;
src:
url('Marianne-Regular_Italic.woff2') format('woff2'),
url('Marianne-Regular_Italic.woff') format('woff');
font-weight: 400;
font-style: italic;
}
@font-face {
font-family: Marianne;
src:
url('Marianne-Medium.woff2') format('woff2'),
url('Marianne-Medium.woff') format('woff');
font-weight: 500;
}
@font-face {
font-family: Marianne;
src:
url('Marianne-Medium_Italic.woff2') format('woff2'),
url('Marianne-Medium_Italic.woff') format('woff');
font-weight: 500;
font-style: italic;
}
@font-face {
font-family: Marianne;
src:
url('Marianne-Bold.woff2') format('woff2'),
url('Marianne-Bold.woff') format('woff');
font-weight: 700;
}
@font-face {
font-family: Marianne;
src:
url('Marianne-Bold_Italic.woff2') format('woff2'),
url('Marianne-Bold_Italic.woff') format('woff');
font-weight: 700;
font-style: italic;
}
@font-face {
font-family: Marianne;
src:
url('Marianne-ExtraBold.woff2') format('woff2'),
url('Marianne-ExtraBold.woff') format('woff');
font-weight: 800;
}
@font-face {
font-family: Marianne;
src:
url('Marianne-ExtraBold_Italic.woff2') format('woff2'),
url('Marianne-ExtraBold_Italic.woff') format('woff');
font-weight: 800;
font-style: italic;
}

View File

@@ -8,6 +8,7 @@ export type DropdownMenuOption = {
icon?: string;
label: string;
testId?: string;
value?: string;
callback?: () => void | Promise<unknown>;
danger?: boolean;
isSelected?: boolean;
@@ -23,6 +24,8 @@ export type DropdownMenuProps = {
buttonCss?: BoxProps['$css'];
disabled?: boolean;
topMessage?: string;
selectedValues?: string[];
afterOpenChange?: (isOpen: boolean) => void;
};
export const DropdownMenu = ({
@@ -34,6 +37,8 @@ export const DropdownMenu = ({
buttonCss,
label,
topMessage,
afterOpenChange,
selectedValues,
}: PropsWithChildren<DropdownMenuProps>) => {
const theme = useCunninghamTheme();
const spacings = theme.spacingsTokens();
@@ -43,6 +48,7 @@ export const DropdownMenu = ({
const onOpenChange = (isOpen: boolean) => {
setIsOpen(isOpen);
afterOpenChange?.(isOpen);
};
if (disabled) {
@@ -161,7 +167,8 @@ export const DropdownMenu = ({
{option.label}
</Text>
</Box>
{option.isSelected && (
{(option.isSelected ||
selectedValues?.includes(option.value ?? '')) && (
<Icon iconName="check" $size="20px" $theme="greyscale" />
)}
</BoxButton>

View File

@@ -8,7 +8,7 @@ type IconProps = TextType & {
};
export const Icon = ({ iconName, ...textProps }: IconProps) => {
return (
<Text $isMaterialIcon {...textProps}>
<Text $isMaterialIcon={textProps.$isMaterialIcon ?? true} {...textProps}>
{iconName}
</Text>
);
@@ -27,7 +27,7 @@ export const IconBG = ({ iconName, ...textProps }: IconBGProps) => {
$size="36px"
$theme="primary"
$variation="600"
$background={colorsTokens()['primary-bg']}
$background={colorsTokens()['greyscale-000']}
$css={`
border: 1px solid ${colorsTokens()['primary-200']};
user-select: none;

View File

@@ -1,3 +1,4 @@
import clsx from 'clsx';
import { CSSProperties, ComponentPropsWithRef, forwardRef } from 'react';
import styled from 'styled-components';
@@ -11,7 +12,7 @@ type TextSizes = keyof typeof sizes;
export interface TextProps extends BoxProps {
as?: 'p' | 'span' | 'div' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
$elipsis?: boolean;
$isMaterialIcon?: boolean;
$isMaterialIcon?: boolean | 'filled';
$weight?: CSSProperties['fontWeight'];
$textAlign?: CSSProperties['textAlign'];
$size?: TextSizes | (string & {});
@@ -58,13 +59,20 @@ export const TextStyled = styled(Box)<TextProps>`
const Text = forwardRef<HTMLElement, ComponentPropsWithRef<typeof TextStyled>>(
({ className, $isMaterialIcon, ...props }, ref) => {
const isFilled = $isMaterialIcon === 'filled';
const isMaterialIcon =
typeof $isMaterialIcon === 'boolean' && $isMaterialIcon;
return (
<TextStyled
ref={ref}
as="span"
$theme="greyscale"
$variation="text"
className={`${className || ''}${$isMaterialIcon ? ' material-icons' : ''}`}
className={clsx(className || '', {
'material-icons': isMaterialIcon,
'material-icons-filled': isFilled,
})}
{...props}
/>
);

View File

@@ -0,0 +1,63 @@
import { css } from 'styled-components';
import { Box } from '../Box';
import { DropdownMenu, DropdownMenuOption } from '../DropdownMenu';
import { Icon } from '../Icon';
import { Text } from '../Text';
export type FilterDropdownProps = {
options: DropdownMenuOption[];
selectedValue?: string;
};
export const FilterDropdown = ({
options,
selectedValue,
}: FilterDropdownProps) => {
const selectedOption = options.find(
(option) => option.value === selectedValue,
);
if (options.length === 0) {
return null;
}
return (
<DropdownMenu
selectedValues={selectedValue ? [selectedValue] : undefined}
options={options}
>
<Box
$css={css`
border: 1px solid
${selectedOption
? 'var(--c--theme--colors--primary-500)'
: 'var(--c--theme--colors--greyscale-250)'};
border-radius: 4px;
background-color: ${selectedOption
? 'var(--c--theme--colors--primary-100)'
: 'var(--c--theme--colors--greyscale-000)'};
gap: var(--c--theme--spacings--2xs);
padding: var(--c--theme--spacings--2xs) var(--c--theme--spacings--xs);
`}
color="secondary"
$direction="row"
$align="center"
>
<Text
$weight={400}
$variation={selectedOption ? '800' : '600'}
$theme={selectedOption ? 'primary' : 'greyscale'}
>
{selectedOption?.label ?? options[0].label}
</Text>
<Icon
$size="16px"
iconName="keyboard_arrow_down"
$variation={selectedOption ? '800' : '600'}
$theme={selectedOption ? 'primary' : 'greyscale'}
/>
</Box>
</DropdownMenu>
);
};

View File

@@ -57,6 +57,9 @@ export const QuickSearchInput = ({
/* eslint-disable-next-line jsx-a11y/no-autofocus */
autoFocus={true}
aria-label={t('Quick search input')}
onClick={(e) => {
e.stopPropagation();
}}
value={inputValue}
role="combobox"
placeholder={placeholder ?? t('Search')}

View File

@@ -1,9 +1,9 @@
import { useCunninghamTheme } from '../useCunninghamTheme';
describe('<useCunninghamTheme />', () => {
it('has the dsfr logo correctly set', () => {
it('has the logo correctly set', () => {
const { themeTokens, setTheme } = useCunninghamTheme.getState();
setTheme('dsfr');
setTheme('default');
const logo = themeTokens().logo;
expect(logo?.src).toBe('/assets/logo-gouv.svg');
expect(logo?.widthHeader).toBe('110px');

View File

@@ -25,4 +25,11 @@
--c--components--forms-select--value-color--disabled: var(
--c--theme--colors--greyscale-400
);
/**
* Button
**/
--c--components--button--border-radius--active: var(
--c--components--button--border-radius
);
}

View File

@@ -1,633 +0,0 @@
@import url('@openfun/cunningham-react/icons');
@import url('@openfun/cunningham-react/style');
@import url('@openfun/cunningham-react/fonts');
@import url('./cunningham-tokens.css');
@import url('./cunningham-custom-tokens.css');
@import url('../assets/fonts/Marianne/Marianne-font.css');
.c__input,
.c__field,
.c__select,
.c__datagrid {
font-family: var(--c--theme--font--families--base);
}
.c__field {
line-height: initial;
}
.c__field .c__field__footer {
padding: 2px 0 0;
font-size: var(--c--components--forms-field--footer-font-size);
color: var(--c--components--forms-field--footer-color);
}
.labelled-box label {
color: var(--c--theme--colors--primary-text);
}
.labelled-box--disabled label {
color: var(--c--components--forms-labelledbox--label-color--small-disabled);
}
.c__field :not(.c__textarea__wrapper, div) .labelled-box label.placeholder {
top: 50%;
transform: translateY(-50%);
}
/**
* Input
* TextArea
*/
.c__input__wrapper,
.c__textarea__wrapper {
transition: all var(--c--theme--transitions--duration)
var(--c--theme--transitions--ease-out);
}
.c__input__wrapper:has(input[readonly]),
.c__input__wrapper:has(input[readonly]) * {
cursor: default;
}
.c__textarea__wrapper:has(input.border-none),
.c__textarea__wrapper:has(input.border-none) *,
.c__input__wrapper:has(input.border-none),
.c__input__wrapper:has(input.border-none) * {
border: none;
}
.c__input__wrapper:hover,
.c__textarea__wrapper:hover {
box-shadow: var(--c--components--forms-input--box-shadow-color) 0 0 0 2px;
}
.c__textarea__wrapper--disabled:hover,
.c__input__wrapper--disabled:hover,
.c__input__wrapper:hover:has(input[readonly]) {
box-shadow: var(--c--theme--colors--primary-500) 0 0 0 0;
}
.c__input__wrapper--disabled {
color: var(--c--components--forms-input--value-color--disabled);
}
.c__input__wrapper .labelled-box__label.placeholder {
cursor: inherit;
}
.c__input__wrapper .c__input,
.c__textarea__wrapper .c__textarea {
width: 100%;
}
.c__input__wrapper--disabled .c__input {
color: var(--c--components--forms-input--value-color--disabled);
}
.c__input__wrapper--error .c__input {
color: var(--c--components--forms-input--color--error);
}
.c__input__wrapper--error:not(.c__input__wrapper--disabled):hover {
border-color: var(--c--components--forms-input--border--color-error-hover);
color: var(--c--components--forms-input--color--error-hover);
}
.c__input__wrapper--error:hover {
box-shadow: var(--c--components--forms-input--color--box-shadow-error-hover) 0
0 0 2px;
}
.c__input__wrapper--error:not(.c__input__wrapper--disabled):hover label {
color: var(--c--components--forms-input--border--color-error-hover);
}
input:-webkit-autofill,
input:-webkit-autofill:focus {
transition:
background-color 0s 600000s,
color 0s 600000s;
}
.c__textarea__wrapper .c__textarea {
color: var(--c--components--forms-textarea--color);
}
.c__textarea__wrapper:hover {
border-color: var(--c--components--forms-textarea--border-color-hover);
}
.c__textarea__wrapper--disabled:hover {
border-color: var(
--c--components--forms-textarea--disabled--border-color-hover
);
}
/**
* Select
*/
.c_select__no_border .c__select .c__select__wrapper,
.c_select__no_border .c__select .c__select__wrapper:hover,
.c_select__no_border
.c__select:not(.c__select--disabled)
.c__select__wrapper:hover {
border: none;
box-shadow: none;
}
.c__select__wrapper {
transition: all var(--c--theme--transitions--duration)
var(--c--theme--transitions--ease-out);
min-height: var(--c--components--forms-select--height);
height: auto;
}
.c__select:not(.c__select--disabled) .c__select__wrapper:hover {
box-shadow: var(--c--components--forms-input--box-shadow-color) 0 0 0 2px;
}
.c__select__wrapper:hover {
border-radius: var(--c--components--forms-select--border-radius-hover);
border-color: var(--c--components--forms-select--border-color-hover);
}
.c__select--disabled .c__select__wrapper:hover {
border-color: var(--c--components--forms-select--border-color-disabled-hover);
}
.c__select--disabled .c__select__wrapper label,
.c__select--disabled .c__select__wrapper input,
.c__select--disabled .c__select__wrapper {
cursor: not-allowed;
}
.c__select__menu__item {
transition: all var(--c--theme--transitions--duration)
var(--c--theme--transitions--ease-out);
}
.c__select--disabled .c__select__wrapper label,
.c__select--disabled .c__select__wrapper input,
.c_select__no_bg .c__select__wrapper {
background: none;
}
.c__select__wrapper:focus-within .labelled-box--disabled label {
color: var(--c--components--forms-labelledbox--label-color--small-disabled);
}
.c__select__wrapper .labelled-box {
display: flex;
gap: 0.6rem;
flex-direction: column;
align-items: flex-start;
}
.c__select__wrapper .labelled-box .labelled-box__children {
padding: unset;
padding-right: 5rem;
}
.c__select__wrapper .labelled-box .c__select__inner__actions {
right: 0;
top: 50%;
position: absolute;
}
.c__select__wrapper label {
position: relative;
padding-right: 5rem;
max-width: none;
}
.c__select__wrapper .c__select__inner__actions__open:focus {
outline: none;
}
.c__select__wrapper .labelled-box__label.c__offscreen {
display: none;
}
/**
* DataGrid
*/
.c__datagrid__table__container {
overflow: auto;
}
.c__datagrid__table__container > table th .c__datagrid__header {
color: var(--c--components--datagrid--header--color);
font-weight: var(--c--components--datagrid--header--weight);
font-size: var(--c--components--datagrid--header--size);
padding-block: 2rem;
}
.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;
}
.c__datagrid__table__container > table tbody {
background-color: var(--c--components--datagrid--body--background-color);
}
.c__datagrid__table__container > table tbody tr:hover {
background-color: var(
--c--components--datagrid--body--background-color-hover
);
}
.c__datagrid__table__container > table {
table-layout: auto;
}
.c__datagrid__table__container > table td {
white-space: break-spaces;
}
.c__datagrid__table__container > table th:first-child,
.c__datagrid__table__container > table td:first-child {
padding-left: 2rem;
}
.c__datagrid > .c__pagination {
padding-inline: 1rem;
justify-content: flex-end;
}
.c__pagination__list {
gap: 3px;
border-radius: 4px;
background: var(--c--components--datagrid--pagination--background-color);
border-color: var(--c--components--datagrid--pagination--border-color);
}
.c__pagination__list .c__button--tertiary-text.c__button--active {
background-color: var(
--c--components--datagrid--pagination--background-color-active
);
color: var(--c--theme--colors--greyscale-800);
}
.c__pagination__list .c__button--tertiary-text:disabled {
display: none;
}
@media (width <= 380px) {
.c__datagrid > .c__pagination {
flex-direction: column;
align-items: center;
gap: 1rem;
}
}
/**
* Date picker
*/
.c__popover.c__popover--borderless {
z-index: 3;
}
.c__date-picker__wrapper {
transition: all var(--c--theme--transitions--duration)
var(--c--theme--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;
}
.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;
}
.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);
}
/**
* Checkbox
*/
.c__checkbox:focus-within {
border-color: transparent;
background-color: transparent;
}
.c__checkbox {
transition: all 0.8s ease-in-out;
}
.c__checkbox .c__field__text {
color: var(--c--components--forms-checkbox--text--color);
font-size: var(--c--components--forms-checkbox--text--size);
}
.c__checkbox.c__checkbox--disabled .c__field__text {
color: var(--c--theme--colors--greyscale-600);
}
.c__switch.c__checkbox--disabled .c__switch__rail {
cursor: not-allowed;
}
.c__checkbox.c__checkbox--disabled .c__checkbox__label {
color: var(--c--theme--colors--greyscale-400);
}
/**
* Button
*/
.c__button {
text-decoration: none;
}
.c__button:hover.c__button-no-bg,
.c__button.c__button-no-bg,
.c__button:disabled.c__button-no-bg {
background-color: transparent;
}
.c__button--nano {
padding: 0 var(--c--theme--spacings--3xs);
gap: var(--c--theme--spacings--3xs);
}
.c__button--nano.c__button--icon-only {
width: auto;
}
.c__button--nano.c__button--icon-only.c__button--full-width {
width: 100%;
}
.c__button--medium {
height: auto;
min-height: var(--c--components--button--medium-height);
}
.c__button--small {
padding: 0.6rem 0.75rem;
}
.c__button--with-icon--right {
padding: 0.7rem var(--c--theme--spacings--t) 0.7rem
var(--c--theme--spacings--s);
}
.c__button--primary {
background-color: var(--c--components--button--primary--background--color);
color: var(--c--components--button--primary--color);
}
.c__button--primary:hover {
background-color: var(
--c--components--button--primary--background--color-hover
);
color: var(--c--components--button--primary--color-hover);
}
.c__button--primary:active,
.c__button--primary.c__button--active {
background-color: var(
--c--components--button--primary--background--color-active
);
color: var(--c--components--button--primary--color-active);
border-color: var(--c--components--button--primary--border--color-active);
}
.c__button--primary-text:active,
.c__button--primary-text.c__button--active {
border: none;
background-color: var(
--c--components--button--primary-text--background--color-active
);
}
.c__button--primary-text {
color: var(--c--components--button--primary-text--color);
}
.c__button--primary-text:hover,
.c__button--primary-text:focus-visible {
background-color: var(
--c--components--button--primary-text--background--color-hover
);
color: var(--c--components--button--primary-text--color-hover);
}
.c__button:disabled {
background-color: var(--c--components--button--disabled--background--color);
color: var(--c--components--button--disabled--color);
}
.c__button--success {
background-color: var(--c--components--button--success--background--color);
color: var(--c--components--button--success--color);
}
.c__button--success:hover,
.c__button--success:focus-visible {
background-color: var(
--c--components--button--success--background--color-hover
);
color: var(--c--components--button--success--color-hover);
}
.c__button--success:disabled {
background-color: var(
--c--components--button--success--background--color-disabled
);
color: var(--c--components--button--success--color-disabled);
}
.c__button--secondary {
background-color: var(--c--components--button--secondary--background--color);
color: var(--c--components--button--secondary--color);
border: 1px solid var(--c--components--button--secondary--border--color);
}
.c__button--secondary:hover,
.c__button--secondary:focus-visible {
background-color: var(
--c--components--button--secondary--background--color-hover
);
color: var(--c--components--button--secondary--color-hover);
border: 1px solid var(--c--components--button--secondary--border--color-hover);
}
.c__button--tertiary {
background-color: var(--c--components--button--tertiary--background--color);
color: var(--c--components--button--tertiary--color);
border: none;
}
.c__button--tertiary:hover,
.c__button--tertiary:focus-visible {
background-color: var(
--c--components--button--tertiary--background--color-hover
);
color: var(--c--components--button--tertiary--color);
}
.c__button--tertiary:active {
background-color: var(
--c--components--button--tertiary--background--color-active
);
color: var(--c--components--button--tertiary--color-active);
}
.c__button--tertiary:disabled {
background-color: var(
--c--components--button--tertiary--background--color-disabled
);
color: var(--c--components--button--tertiary--color-disabled);
}
.c__button--tertiary-text {
border: none;
color: var(--c--components--button--tertiary-text--color);
}
.c__button--tertiary-text:hover,
.c__button--tertiary-text:focus-visible {
background-color: var(
--c--components--button--tertiary-text--background--color-hover
);
color: var(--c--components--button--tertiary-text--color-hover);
}
.c__button--tertiary-text:disabled {
background-color: var(
--c--components--button--tertiary-text--background--color-disabled
);
color: var(--c--components--button--tertiary-text--color-disabled);
}
.c__button--danger {
background-color: var(--c--components--button--danger--background--color);
}
.c__button--danger:hover,
.c__button--danger:focus-visible {
background-color: var(
--c--components--button--danger--background--color-hover
);
color: var(--c--components--button--danger--color-hover);
}
.c__button--danger:disabled {
background-color: var(
--c--components--button--danger--background--color-disabled
);
}
/**
* Modal
*/
.c__modal__backdrop {
z-index: 1000;
}
.c__modal__close .c__button--tertiary-text:hover,
.c__modal__close .c__button--tertiary-text:focus-visible {
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;
top: -0.65rem;
right: -0.65rem;
}
.c__modal--full .c__modal__content {
overflow-y: auto;
}
.c__modal__title {
padding: 0;
font-size: 1.125rem;
margin-bottom: var(--c--theme--spacings--2xs);
}
@media screen and (width <= 420px) {
.c__modal__scroller {
padding: 0.7rem;
}
.c__modal__title h2 {
font-size: 1rem;
}
}
@media (width <= 576px) {
.c__modal__footer--sided {
gap: 0.5rem;
flex-direction: column-reverse;
}
}
.c__modal__scroller:has(.noPadding) {
padding: 0 !important;
.c__modal__close .c__button {
right: 5px;
top: 5px;
padding: 1.5rem 1rem;
}
.c__modal__title {
font-size: var(--c--theme--font--sizes--xs);
padding: var(--c--theme--spacings--base);
margin-bottom: 0;
}
}
/**
* Toast
*/
.c__toast__container {
z-index: 10000;
}
/**
* Tooltip
*/
.c__tooltip {
padding: 4px 6px;
}
/**
* Alert
*/
.c__alert--error {
background-color: var(--c--components--alert--error--background-color);
border-left-color: var(--c--components--alert--error--border-left-color);
}
.c__alert--error .c__button--tertiary {
background-color: var(--c--components--alert--error--close--background-color);
color: var(--c--components--alert--error--close--color);
}
.c__alert.c__alert--error .c__button--tertiary:hover {
background-color: var(
--c--components--alert--error--close--background-color-hover
);
}

View File

@@ -1,82 +1,85 @@
:root {
--c--theme--colors--secondary-text: var(--c--theme--colors--greyscale-700);
--c--theme--colors--secondary-100: #f2f7fc;
--c--theme--colors--secondary-200: #ebf3fa;
--c--theme--colors--secondary-300: #e2eef8;
--c--theme--colors--secondary-400: #ddeaf7;
--c--theme--colors--secondary-500: #d4e5f5;
--c--theme--colors--secondary-600: #c1d0df;
--c--theme--colors--secondary-700: #97a3ae;
--c--theme--colors--secondary-800: #757e87;
--c--theme--colors--secondary-900: #596067;
--c--theme--colors--info-text: var(--c--theme--colors--greyscale-000);
--c--theme--colors--info-100: #ebf2fc;
--c--theme--colors--info-200: #8cb5ea;
--c--theme--colors--info-300: #5894e1;
--c--theme--colors--info-400: #377fdb;
--c--theme--colors--info-500: #055fd2;
--c--theme--colors--info-600: #0556bf;
--c--theme--colors--info-700: #044395;
--c--theme--colors--info-800: #033474;
--c--theme--colors--info-900: #022858;
--c--theme--colors--greyscale-100: #fafafb;
--c--theme--colors--greyscale-200: #f3f4f4;
--c--theme--colors--greyscale-300: #e7e8ea;
--c--theme--colors--greyscale-400: #c2c6ca;
--c--theme--colors--greyscale-500: #9ea3aa;
--c--theme--colors--greyscale-600: #79818a;
--c--theme--colors--greyscale-700: #555f6b;
--c--theme--colors--greyscale-800: #303c4b;
--c--theme--colors--greyscale-900: #0c1a2b;
--c--theme--colors--secondary-text: #fff;
--c--theme--colors--secondary-100: #fee9ea;
--c--theme--colors--secondary-200: #fedfdf;
--c--theme--colors--secondary-300: #fdbfbf;
--c--theme--colors--secondary-400: #e1020f;
--c--theme--colors--secondary-500: #c91a1f;
--c--theme--colors--secondary-600: #5e2b2b;
--c--theme--colors--secondary-700: #3b2424;
--c--theme--colors--secondary-800: #341f1f;
--c--theme--colors--secondary-900: #2b1919;
--c--theme--colors--info-text: #0078f3;
--c--theme--colors--info-100: #e8edff;
--c--theme--colors--info-200: #dde5ff;
--c--theme--colors--info-300: #bccdff;
--c--theme--colors--info-400: #518fff;
--c--theme--colors--info-500: #0078f3;
--c--theme--colors--info-600: #0063cb;
--c--theme--colors--info-700: #273961;
--c--theme--colors--info-800: #222a3f;
--c--theme--colors--info-900: #1d2437;
--c--theme--colors--greyscale-100: #eee;
--c--theme--colors--greyscale-200: #e5e5e5;
--c--theme--colors--greyscale-300: #cecece;
--c--theme--colors--greyscale-400: #929292;
--c--theme--colors--greyscale-500: #7c7c7c;
--c--theme--colors--greyscale-600: #666;
--c--theme--colors--greyscale-700: #3a3a3a;
--c--theme--colors--greyscale-800: #2a2a2a;
--c--theme--colors--greyscale-900: #242424;
--c--theme--colors--greyscale-000: #fff;
--c--theme--colors--primary-100: #edf5fa;
--c--theme--colors--primary-200: #8cb5ea;
--c--theme--colors--primary-300: #5894e1;
--c--theme--colors--primary-400: #377fdb;
--c--theme--colors--primary-500: #055fd2;
--c--theme--colors--primary-600: #0556bf;
--c--theme--colors--primary-700: #044395;
--c--theme--colors--primary-800: #033474;
--c--theme--colors--primary-900: #022858;
--c--theme--colors--success-100: #effcd3;
--c--theme--colors--success-200: #dbfaa9;
--c--theme--colors--success-300: #bef27c;
--c--theme--colors--success-400: #a0e659;
--c--theme--colors--success-500: #76d628;
--c--theme--colors--success-600: #5ab81d;
--c--theme--colors--success-700: #419a14;
--c--theme--colors--success-800: #2c7c0c;
--c--theme--colors--success-900: #1d6607;
--c--theme--colors--warning-100: #fff8cd;
--c--theme--colors--warning-200: #ffef9b;
--c--theme--colors--warning-300: #ffe469;
--c--theme--colors--warning-400: #ffda43;
--c--theme--colors--warning-500: #ffc805;
--c--theme--colors--warning-600: #dba603;
--c--theme--colors--warning-700: #b78702;
--c--theme--colors--warning-800: #936901;
--c--theme--colors--warning-900: #7a5400;
--c--theme--colors--danger-100: #f4b0b0;
--c--theme--colors--danger-200: #ee8a8a;
--c--theme--colors--danger-300: #e65454;
--c--theme--colors--danger-400: #e13333;
--c--theme--colors--danger-500: #da0000;
--c--theme--colors--danger-600: #c60000;
--c--theme--colors--danger-700: #9b0000;
--c--theme--colors--danger-800: #780000;
--c--theme--colors--danger-900: #5c0000;
--c--theme--colors--primary-text: var(--c--theme--colors--greyscale-000);
--c--theme--colors--success-text: var(--c--theme--colors--greyscale-000);
--c--theme--colors--warning-text: var(--c--theme--colors--greyscale-000);
--c--theme--colors--danger-text: var(--c--theme--colors--greyscale-000);
--c--theme--colors--card-border: #ededed;
--c--theme--colors--primary-bg: #fafafa;
--c--theme--colors--primary-action: #1212ff;
--c--theme--colors--primary-100: #ececfe;
--c--theme--colors--primary-200: #e3e3fd;
--c--theme--colors--primary-300: #cacafb;
--c--theme--colors--primary-400: #8585f6;
--c--theme--colors--primary-500: #6a6af4;
--c--theme--colors--primary-600: #313178;
--c--theme--colors--primary-700: #272747;
--c--theme--colors--primary-800: #000091;
--c--theme--colors--primary-900: #21213f;
--c--theme--colors--success-100: #dffee6;
--c--theme--colors--success-200: #b8fec9;
--c--theme--colors--success-300: #88fdaa;
--c--theme--colors--success-400: #3bea7e;
--c--theme--colors--success-500: #1f8d49;
--c--theme--colors--success-600: #18753c;
--c--theme--colors--success-700: #204129;
--c--theme--colors--success-800: #1e2e22;
--c--theme--colors--success-900: #19281d;
--c--theme--colors--warning-100: #fff4f3;
--c--theme--colors--warning-200: #ffe9e6;
--c--theme--colors--warning-300: #ffded9;
--c--theme--colors--warning-400: #ffbeb4;
--c--theme--colors--warning-500: #d64d00;
--c--theme--colors--warning-600: #b34000;
--c--theme--colors--warning-700: #5e2c21;
--c--theme--colors--warning-800: #3e241e;
--c--theme--colors--warning-900: #361e19;
--c--theme--colors--danger-100: #ffe9e9;
--c--theme--colors--danger-200: #fdd;
--c--theme--colors--danger-300: #ffbdbd;
--c--theme--colors--danger-400: #ff5655;
--c--theme--colors--danger-500: #f60700;
--c--theme--colors--danger-600: #ce0500;
--c--theme--colors--danger-700: #642626;
--c--theme--colors--danger-800: #412121;
--c--theme--colors--danger-900: #391c1c;
--c--theme--colors--primary-text: #000091;
--c--theme--colors--success-text: #1f8d49;
--c--theme--colors--warning-text: #d64d00;
--c--theme--colors--danger-text: #fff;
--c--theme--colors--primary-050: #f5f5fe;
--c--theme--colors--primary-150: #e5eefa;
--c--theme--colors--primary-950: #1b1b35;
--c--theme--colors--info-150: #e5eefa;
--c--theme--colors--primary-150: #f4f4fd;
--c--theme--colors--greyscale-text: #303c4b;
--c--theme--colors--greyscale-050: #f6f6f6;
--c--theme--colors--greyscale-250: #ddd;
--c--theme--colors--greyscale-350: #ddd;
--c--theme--colors--greyscale-750: #353535;
--c--theme--colors--greyscale-950: #1e1e1e;
--c--theme--colors--greyscale-1000: #161616;
--c--theme--colors--primary-action: #1212ff;
--c--theme--colors--primary-bg: #fafafa;
--c--theme--colors--blue-400: #7ab1e8;
--c--theme--colors--blue-500: #417dc4;
--c--theme--colors--blue-600: #3558a2;
@@ -135,8 +138,8 @@
--c--theme--font--weights--bold: 600;
--c--theme--font--weights--extrabold: 800;
--c--theme--font--weights--black: 900;
--c--theme--font--families--base: 'Roboto Flex Variable', sans-serif;
--c--theme--font--families--accent: 'Roboto Flex Variable', sans-serif;
--c--theme--font--families--base: marianne;
--c--theme--font--families--accent: marianne;
--c--theme--font--letterspacings--h1: normal;
--c--theme--font--letterspacings--h2: normal;
--c--theme--font--letterspacings--h3: normal;
@@ -182,170 +185,271 @@
--c--theme--breakpoints--xl: 1200px;
--c--theme--breakpoints--xxl: 1400px;
--c--theme--breakpoints--xxs: 320px;
--c--theme--logo--src: ;
--c--theme--logo--widthheader: ;
--c--theme--logo--widthfooter: ;
--c--theme--logo--alt: ;
--c--components--datagrid--header--weight: var(
--c--theme--font--weights--extrabold
);
--c--components--datagrid--header--size: var(--c--theme--font--sizes--ml);
--c--components--datagrid--cell--color: var(--c--theme--colors--primary-500);
--c--components--datagrid--cell--size: var(--c--theme--font--sizes--ml);
--c--components--forms-checkbox--background-color--hover: #055fd214;
--c--components--forms-checkbox--color: var(--c--theme--colors--primary-500);
--c--components--forms-checkbox--font-size: var(--c--theme--font--sizes--ml);
--c--components--forms-datepicker--border-color: var(
--c--theme--colors--primary-500
);
--c--components--forms-datepicker--value-color: var(
--c--theme--colors--primary-500
);
--c--components--forms-datepicker--border-radius--hover: var(
--c--components--forms-datepicker--border-radius
);
--c--components--forms-datepicker--border-radius--focus: var(
--c--components--forms-datepicker--border-radius
);
--c--components--forms-field--color: var(--c--theme--colors--primary-500);
--c--components--forms-field--value-color: var(
--c--theme--colors--primary-500
);
--c--components--forms-field--width: auto;
--c--components--forms-input--value-color: var(
--c--theme--colors--primary-500
);
--c--components--forms-input--border-color: var(
--c--theme--colors--primary-500
);
--c--components--forms-input--color--error: var(
--c--theme--colors--danger-500
);
--c--components--forms-input--color--error-hover: var(
--c--theme--colors--danger-500
);
--c--components--forms-input--color--box-shadow-error-hover: var(
--c--theme--colors--danger-500
);
--c--components--forms-labelledbox--label-color--small: var(
--c--theme--colors--primary-500
);
--c--components--forms-labelledbox--label-color--small-disabled: var(
--c--theme--colors--greyscale-400
);
--c--components--forms-labelledbox--label-color--big--disabled: var(
--c--theme--colors--greyscale-400
);
--c--components--forms-select--border-color: var(
--c--theme--colors--primary-500
);
--c--components--forms-select--border-color-disabled-hover: var(
--c--theme--colors--greyscale-200
);
--c--components--forms-select--border-radius--hover: var(
--c--components--forms-select--border-radius
);
--c--components--forms-select--border-radius--focus: var(
--c--components--forms-select--border-radius
);
--c--components--forms-select--font-size: var(--c--theme--font--sizes--ml);
--c--components--forms-select--menu-background-color: #fff;
--c--components--forms-select--item-background-color--hover: var(
--c--theme--colors--primary-300
);
--c--components--forms-switch--accent-color: var(
--c--theme--colors--primary-400
);
--c--components--forms-textarea--border-color: var(
--c--components--forms-textarea--border-color
);
--c--components--forms-textarea--border-color-hover: var(
--c--components--forms-textarea--border-color
);
--c--components--forms-textarea--border-radius--hover: var(
--c--components--forms-textarea--border-radius
);
--c--components--forms-textarea--border-radius--focus: var(
--c--components--forms-textarea--border-radius
);
--c--components--forms-textarea--color: var(--c--theme--colors--primary-500);
--c--components--forms-textarea--disabled--border-color-hover: var(
--c--theme--colors--greyscale-200
);
--c--components--modal--background-color: #fff;
--c--components--button--border-radius--active: var(
--c--components--button--border-radius
);
--c--components--button--medium-height: auto;
--c--components--button--small-height: auto;
--c--components--button--success--color: white;
--c--components--button--success--color-disabled: white;
--c--components--button--success--color-hover: white;
--c--components--button--success--background--color: var(
--c--theme--colors--success-600
);
--c--components--button--success--background--color-disabled: var(
--c--theme--colors--greyscale-300
);
--c--components--button--success--background--color-hover: var(
--c--theme--colors--success-800
);
--c--components--button--danger--color-hover: white;
--c--components--button--danger--background--color: var(
--c--theme--colors--danger-600
);
--c--components--button--danger--background--color-hover: #ff2725;
--c--components--button--danger--background--color-disabled: var(
--c--theme--colors--danger-100
);
--c--components--button--primary--color: var(
--c--theme--colors--primary-text
);
--c--components--button--primary--color-active: var(
--c--theme--colors--primary-text
);
--c--theme--breakpoints--mobile: 768px;
--c--theme--breakpoints--tablet: 1024px;
--c--theme--logo--src: /assets/logo-gouv.svg;
--c--theme--logo--widthheader: 110px;
--c--theme--logo--widthfooter: 220px;
--c--theme--logo--alt: gouvernement logo;
--c--components--modal--width-small: 342px;
--c--components--button--medium-height: 40px;
--c--components--button--medium-text-height: 40px;
--c--components--button--border-radius: 4px;
--c--components--button--small-height: 26px;
--c--components--button--primary--background--color: var(
--c--theme--colors--primary-400
);
--c--components--button--primary--background--color-active: var(
--c--theme--colors--primary-500
);
--c--components--button--primary--border--color-active: transparent;
--c--components--button--secondary--color: var(
--c--theme--colors--primary-500
);
--c--components--button--secondary--color-hover: var(
--c--theme--colors--primary-text
);
--c--components--button--secondary--background--color: white;
--c--components--button--secondary--background--color-hover: var(
--c--theme--colors--primary-700
--c--components--button--primary--background--color-hover: #1212ff;
--c--components--button--primary--background--color-active: #2323ff;
--c--components--button--primary--background--color-disabled: var(
--c--theme--colors--greyscale-100
);
--c--components--button--primary--color: #fff;
--c--components--button--primary--color-hover: #fff;
--c--components--button--primary--color-active: #fff;
--c--components--button--primary--color-focus-visible: #fff;
--c--components--button--primary--disabled: var(
--c--theme--colors--greyscale-500
);
--c--components--button--primary-text--background--color: var(
--c--theme--colors--primary-text
);
--c--components--button--primary-text--background--color-hover: var(
--c--theme--colors--greyscale-100
);
--c--components--button--primary-text--background--color-active: var(
--c--theme--colors--primary-100
);
--c--components--button--primary-text--background--color-focus-visible: #fff;
--c--components--button--primary-text--background--color-disabled: var(
--c--theme--colors--greyscale-000
);
--c--components--button--primary-text--color: var(
--c--theme--colors--primary-800
);
--c--components--button--primary-text--color-hover: var(
--c--theme--colors--primary-800
);
--c--components--button--primary-text--disabled: var(
--c--theme--colors--greyscale-400
);
--c--components--button--secondary--background--color-hover: #f6f6f6;
--c--components--button--secondary--background--color-active: #ededed;
--c--components--button--secondary--background--color-focus-visible: var(
--c--theme--colors--greyscale-000
);
--c--components--button--secondary--background--disabled: var(
--c--theme--colors--greyscale-000
);
--c--components--button--secondary--color: var(
--c--theme--colors--primary-800
);
--c--components--button--secondary--border--color: var(
--c--theme--colors--greyscale-300
);
--c--components--button--tertiary--color: var(
--c--theme--colors--primary-text
--c--components--button--secondary--border--color-hover: var(
--c--theme--colors--greyscale-300
);
--c--components--button--tertiary--color-disabled: var(
--c--theme--colors--greyscale-600
--c--components--button--secondary--border--color-disabled: var(
--c--theme--colors--greyscale-300
);
--c--components--button--secondary--disabled: var(
--c--theme--colors--greyscale-400
);
--c--components--button--tertiary--background--color: var(
--c--theme--colors--primary-100
);
--c--components--button--tertiary--background--color-focus-visible: var(
--c--theme--colors--primary-100
);
--c--components--button--tertiary--background--color-hover: var(
--c--theme--colors--primary-300
);
--c--components--button--tertiary--background--color-active: var(
--c--theme--colors--primary-100
--c--theme--colors--primary-300
);
--c--components--button--tertiary--background--color-disabled: var(
--c--components--button--tertiary--background--disabled: var(
--c--theme--colors--primary-050
);
--c--components--button--tertiary--color: var(
--c--theme--colors--primary-800
);
--c--components--button--tertiary--disabled: var(
--c--theme--colors--primary-300
);
--c--components--button--tertiary-text--background--color-hover: var(
--c--theme--colors--greyscale-100
);
--c--components--button--tertiary-text--color-hover: var(
--c--theme--colors--primary-text
);
--c--components--button--tertiary-text--color: var(
--c--theme--colors--primary-600
);
--c--components--button--danger--color-hover: white;
--c--components--button--danger--background--color: var(
--c--theme--colors--danger-600
);
--c--components--button--danger--background--color-hover: #ff2725;
--c--components--button--danger--background--color-focus-visible: var(
--c--theme--colors--danger-600
);
--c--components--button--danger--background--color-disabled: var(
--c--theme--colors--greyscale-100
);
--c--components--button--danger--color-disabled: var(
--c--theme--colors--greyscale-400
);
--c--components--datagrid--header--color: var(
--c--theme--colors--greyscale-600
);
--c--components--datagrid--header--size: 12px;
--c--components--datagrid--header--weight: 500;
--c--components--datagrid--body--background-color-hover: var(
--c--theme--colors--greyscale-100
);
--c--components--forms-checkbox--border-radius: 4px;
--c--components--forms-checkbox--border-color: var(
--c--theme--colors--primary-800
);
--c--components--forms-checkbox--background-color--hover: var(
--c--theme--colors--greyscale-100
);
--c--components--forms-checkbox--border--color-disabled: var(
--c--theme--colors--greyscale-200
);
--c--components--button--disabled--color: white;
--c--components--button--disabled--background--color: #b3cef0;
--c--components--la-gauffre--activated: false;
--c--components--home-proconnect--activated: false;
--c--components--forms-checkbox--border--color: var(
--c--theme--colors--primary-800
);
--c--components--forms-checkbox--background--disabled: var(
--c--theme--colors--greyscale-200
);
--c--components--forms-checkbox--background--enable: var(
--c--theme--colors--primary-800
);
--c--components--forms-checkbox--check--disabled: var(
--c--theme--colors--greyscale-300
);
--c--components--forms-checkbox--check--enable: var(
--c--theme--colors--greyscale-000
);
--c--components--forms-checkbox--color: var(--c--theme--colors--primary-text);
--c--components--forms-checkbox--label--color: var(
--c--theme--colors--greyscale-1000
);
--c--components--forms-checkbox--label--size: var(
--c--theme--font--sizes--sm
);
--c--components--forms-checkbox--label--weight: 500;
--c--components--forms-checkbox--text--color: var(
--c--theme--colors--greyscale-600
);
--c--components--forms-checkbox--text--size: var(--c--theme--font--sizes--s);
--c--components--forms-checkbox--text--weight: 400;
--c--components--forms-checkbox--text--color-disabled: var(
--c--theme--colors--greyscale-300
);
--c--components--forms-labelledbox--label-color--small: var(
--c--theme--colors--greyscale-950
);
--c--components--forms-labelledbox--label-color--small--disabled: var(
--c--theme--colors--greyscale-300
);
--c--components--forms-labelledbox--label-color--big: var(
--c--theme--colors--greyscale-950
);
--c--components--forms-labelledbox--label-color--big--disabled: var(
--c--theme--colors--greyscale-300
);
--c--components--forms-radio--border-color: var(
--c--theme--colors--primary-800
);
--c--components--forms-radio--background-color: var(
--c--theme--colors--greyscale-000
);
--c--components--forms-radio--accent-color: var(
--c--theme--colors--primary-800
);
--c--components--forms-radio--accent-color-disabled: var(
--c--theme--colors--greyscale-300
);
--c--components--forms-switch--border--color-disabled: var(
--c--theme--colors--greyscale-300
);
--c--components--forms-switch--border--color: var(
--c--theme--colors--primary-800
);
--c--components--forms-switch--handle-background-color: white;
--c--components--forms-switch--handle-background-color--disabled: var(
--c--theme--colors--greyscale-000
);
--c--components--forms-switch--rail-background-color--disabled: var(
--c--theme--colors--greyscale-000
);
--c--components--forms-switch--accent-color: var(
--c--theme--colors--primary-800
);
--c--components--forms-textarea--label-color--focus: var(
--c--theme--colors--greyscale-1000
);
--c--components--forms-textarea--border-radius: 4px;
--c--components--forms-textarea--border-color: var(
--c--theme--colors--greyscale-400
);
--c--components--forms-textarea--box-shadow--color--hover: var(
--c--theme--colors--greyscale-400
);
--c--components--forms-textarea--box-shadow--color--focus: var(
--c--theme--colors--primary-800
);
--c--components--forms-textarea--value-color: var(
--c--theme--colors--greyscale-950
);
--c--components--forms-textarea--value-color--disabled: var(
--c--theme--colors--greyscale-300
);
--c--components--forms-textarea--font-size: 14px;
--c--components--forms-input--label-color--focus: var(
--c--theme--colors--greyscale-1000
);
--c--components--forms-input--border-radius: 4px;
--c--components--forms-input--border-color: var(
--c--theme--colors--greyscale-400
);
--c--components--forms-input--box-shadow--color--hover: var(
--c--theme--colors--greyscale-400
);
--c--components--forms-input--box-shadow--color--focus: var(
--c--theme--colors--primary-800
);
--c--components--forms-input--value-color: var(
--c--theme--colors--greyscale-950
);
--c--components--forms-input--value-color--disabled: var(
--c--theme--colors--greyscale-300
);
--c--components--forms-input--font-size: 14px;
--c--components--forms-select--label-color--focus: var(
--c--theme--colors--greyscale-1000
);
--c--components--forms-select--item-font-size: 14px;
--c--components--forms-select--border-radius: 4px;
--c--components--forms-select--border-radius-hover: 4px;
--c--components--forms-select--border-color: var(
--c--theme--colors--greyscale-400
);
--c--components--forms-select--box-shadow--color--hover: var(
--c--theme--colors--greyscale-400
);
--c--components--forms-select--box-shadow--color--focus: var(
--c--theme--colors--primary-800
);
--c--components--forms-select--value-color: var(
--c--theme--colors--greyscale-950
);
--c--components--forms-select--font-size: 14px;
--c--components--la-gauffre--activated: true;
--c--components--home-proconnect--activated: true;
}
.cunningham-theme--dark {
@@ -397,219 +501,6 @@
--c--theme--colors--danger-900: #9d6666;
}
.cunningham-theme--dsfr {
--c--theme--colors--card-border: #e5e5e5;
--c--theme--colors--primary-text: #000091;
--c--theme--colors--primary-100: #ececfe;
--c--theme--colors--primary-150: #f4f4fd;
--c--theme--colors--primary-200: #e3e3fd;
--c--theme--colors--primary-300: #cacafb;
--c--theme--colors--primary-400: #8585f6;
--c--theme--colors--primary-500: #6a6af4;
--c--theme--colors--primary-600: #313178;
--c--theme--colors--primary-700: #272747;
--c--theme--colors--primary-800: #000091;
--c--theme--colors--primary-900: #21213f;
--c--theme--colors--secondary-text: #fff;
--c--theme--colors--secondary-100: #fee9ea;
--c--theme--colors--secondary-200: #fedfdf;
--c--theme--colors--secondary-300: #fdbfbf;
--c--theme--colors--secondary-400: #e1020f;
--c--theme--colors--secondary-500: #c91a1f;
--c--theme--colors--secondary-600: #5e2b2b;
--c--theme--colors--secondary-700: #3b2424;
--c--theme--colors--secondary-800: #341f1f;
--c--theme--colors--secondary-900: #2b1919;
--c--theme--colors--greyscale-text: #303c4b;
--c--theme--colors--greyscale-000: #fff;
--c--theme--colors--greyscale-050: #f6f6f6;
--c--theme--colors--greyscale-100: #eee;
--c--theme--colors--greyscale-200: #e5e5e5;
--c--theme--colors--greyscale-250: #ddd;
--c--theme--colors--greyscale-300: #cecece;
--c--theme--colors--greyscale-350: #ddd;
--c--theme--colors--greyscale-400: #929292;
--c--theme--colors--greyscale-500: #7c7c7c;
--c--theme--colors--greyscale-600: #666;
--c--theme--colors--greyscale-700: #3a3a3a;
--c--theme--colors--greyscale-750: #353535;
--c--theme--colors--greyscale-800: #2a2a2a;
--c--theme--colors--greyscale-900: #242424;
--c--theme--colors--greyscale-950: #1e1e1e;
--c--theme--colors--greyscale-1000: #161616;
--c--theme--colors--success-text: #1f8d49;
--c--theme--colors--success-100: #dffee6;
--c--theme--colors--success-200: #b8fec9;
--c--theme--colors--success-300: #88fdaa;
--c--theme--colors--success-400: #3bea7e;
--c--theme--colors--success-500: #1f8d49;
--c--theme--colors--success-600: #18753c;
--c--theme--colors--success-700: #204129;
--c--theme--colors--success-800: #1e2e22;
--c--theme--colors--success-900: #19281d;
--c--theme--colors--info-text: #0078f3;
--c--theme--colors--info-100: #e8edff;
--c--theme--colors--info-200: #dde5ff;
--c--theme--colors--info-300: #bccdff;
--c--theme--colors--info-400: #518fff;
--c--theme--colors--info-500: #0078f3;
--c--theme--colors--info-600: #0063cb;
--c--theme--colors--info-700: #273961;
--c--theme--colors--info-800: #222a3f;
--c--theme--colors--info-900: #1d2437;
--c--theme--colors--warning-text: #d64d00;
--c--theme--colors--warning-100: #fff4f3;
--c--theme--colors--warning-200: #ffe9e6;
--c--theme--colors--warning-300: #ffded9;
--c--theme--colors--warning-400: #ffbeb4;
--c--theme--colors--warning-500: #d64d00;
--c--theme--colors--warning-600: #b34000;
--c--theme--colors--warning-700: #5e2c21;
--c--theme--colors--warning-800: #3e241e;
--c--theme--colors--warning-900: #361e19;
--c--theme--colors--danger-text: #fff;
--c--theme--colors--danger-100: #ffe9e9;
--c--theme--colors--danger-200: #fdd;
--c--theme--colors--danger-300: #ffbdbd;
--c--theme--colors--danger-400: #ff5655;
--c--theme--colors--danger-500: #f60700;
--c--theme--colors--danger-600: #ce0500;
--c--theme--colors--danger-700: #642626;
--c--theme--colors--danger-800: #412121;
--c--theme--colors--danger-900: #391c1c;
--c--theme--font--families--accent: marianne;
--c--theme--font--families--base: marianne;
--c--theme--logo--src: /assets/logo-gouv.svg;
--c--theme--logo--widthHeader: 110px;
--c--theme--logo--widthFooter: 220px;
--c--theme--logo--alt: gouvernement logo;
--c--components--alert--border-radius: 0;
--c--components--alert--error--background-color: var(
--c--theme--colors--danger-100
);
--c--components--alert--error--border-left-color: var(
--c--theme--colors--danger-400
);
--c--components--alert--error--close--color: white;
--c--components--alert--error--close--background-color: var(
--c--theme--colors--danger-400
);
--c--components--alert--error--close--background-color-hover: var(
--c--theme--colors--danger-600
);
--c--components--modal--width-small: 342px;
--c--components--button--medium-height: 40px;
--c--components--button--medium-text-height: 40px;
--c--components--button--border-radius: 4px;
--c--components--button--primary--background--color: var(
--c--theme--colors--primary-text
);
--c--components--button--primary--background--color-hover: #1212ff;
--c--components--button--primary--background--color-active: #2323ff;
--c--components--button--primary--color: #fff;
--c--components--button--primary--color-hover: #fff;
--c--components--button--primary--color-active: #fff;
--c--components--button--primary-text--background--color-hover: var(
--c--theme--colors--primary-100
);
--c--components--button--primary-text--background--color-active: var(
--c--theme--colors--primary-100
);
--c--components--button--primary-text--color-hover: var(
--c--theme--colors--primary-text
);
--c--components--button--primary-text--color: var(
--c--theme--colors--primary-800
);
--c--components--button--secondary--background--color-hover: #f6f6f6;
--c--components--button--secondary--background--color-active: #ededed;
--c--components--button--secondary--border--color: var(
--c--theme--colors--greyscale-300
);
--c--components--button--secondary--border--color-hover: var(
--c--theme--colors--greyscale-300
);
--c--components--button--secondary--color: var(
--c--theme--colors--primary-text
);
--c--components--button--tertiary-text--background--color-hover: var(
--c--theme--colors--greyscale-100
);
--c--components--button--tertiary-text--color-hover: var(
--c--theme--colors--primary-text
);
--c--components--button--tertiary-text--color: var(
--c--theme--colors--primary-600
);
--c--components--datagrid--header--color: var(
--c--theme--colors--primary-text
);
--c--components--datagrid--header--size: var(--c--theme--font--sizes--s);
--c--components--datagrid--body--background-color: transparent;
--c--components--datagrid--body--background-color-hover: #f4f4fd;
--c--components--datagrid--pagination--background-color: transparent;
--c--components--datagrid--pagination--background-color-active: var(
--c--theme--colors--primary-300
);
--c--components--datagrid--pagination--border-color: var(
--c--theme--colors--primary-400
);
--c--components--forms-checkbox--border-radius: 0;
--c--components--forms-checkbox--color: var(--c--theme--colors--primary-text);
--c--components--forms-checkbox--text--color: var(
--c--theme--colors--greyscale-text
);
--c--components--forms-checkbox--text--size: var(--c--theme--font--sizes--t);
--c--components--forms-datepicker--border-radius: 0;
--c--components--forms-fileuploader--border-radius: 0;
--c--components--forms-field--color: var(--c--theme--colors--primary-text);
--c--components--forms-field--footer-font-size: var(
--c--theme--font--sizes--t
);
--c--components--forms-field--footer-color: var(
--c--theme--colors--greyscale-text
);
--c--components--forms-input--border-radius: 4px;
--c--components--forms-input--background-color: #fff;
--c--components--forms-input--border-color: var(
--c--theme--colors--primary-text
);
--c--components--forms-input--box-shadow-color: var(
--c--theme--colors--primary-text
);
--c--components--forms-input--value-color: var(
--c--theme--colors--primary-text
);
--c--components--forms-input--font-size: 14px;
--c--components--forms-labelledbox--label-color--big: var(
--c--theme--colors--primary-text
);
--c--components--forms-radio--accent-color: var(
--c--theme--colors--primary-600
);
--c--components--forms-select--item-font-size: 14px;
--c--components--forms-select--border-radius: 4px;
--c--components--forms-select--border-radius-hover: 4px;
--c--components--forms-select--background-color: #fff;
--c--components--forms-select--border-color: var(
--c--theme--colors--primary-text
);
--c--components--forms-select--border-color-hover: var(
--c--theme--colors--primary-text
);
--c--components--forms-select--box-shadow-color: var(
--c--theme--colors--primary-text
);
--c--components--forms-switch--handle-border-radius: 2px;
--c--components--forms-switch--rail-border-radius: 4px;
--c--components--forms-switch--accent-color: var(
--c--theme--colors--primary-text
);
--c--components--forms-textarea--border-radius: 0;
--c--components--la-gauffre--activated: true;
--c--components--home-proconnect--activated: true;
}
.clr-secondary-text {
color: var(--c--theme--colors--secondary-text);
}
@@ -890,18 +781,6 @@
color: var(--c--theme--colors--danger-text);
}
.clr-card-border {
color: var(--c--theme--colors--card-border);
}
.clr-primary-bg {
color: var(--c--theme--colors--primary-bg);
}
.clr-primary-action {
color: var(--c--theme--colors--primary-action);
}
.clr-primary-050 {
color: var(--c--theme--colors--primary-050);
}
@@ -910,18 +789,42 @@
color: var(--c--theme--colors--primary-150);
}
.clr-primary-950 {
color: var(--c--theme--colors--primary-950);
.clr-greyscale-text {
color: var(--c--theme--colors--greyscale-text);
}
.clr-info-150 {
color: var(--c--theme--colors--info-150);
.clr-greyscale-050 {
color: var(--c--theme--colors--greyscale-050);
}
.clr-greyscale-250 {
color: var(--c--theme--colors--greyscale-250);
}
.clr-greyscale-350 {
color: var(--c--theme--colors--greyscale-350);
}
.clr-greyscale-750 {
color: var(--c--theme--colors--greyscale-750);
}
.clr-greyscale-950 {
color: var(--c--theme--colors--greyscale-950);
}
.clr-greyscale-1000 {
color: var(--c--theme--colors--greyscale-1000);
}
.clr-primary-action {
color: var(--c--theme--colors--primary-action);
}
.clr-primary-bg {
color: var(--c--theme--colors--primary-bg);
}
.clr-blue-400 {
color: var(--c--theme--colors--blue-400);
}
@@ -1322,18 +1225,6 @@
background-color: var(--c--theme--colors--danger-text);
}
.bg-card-border {
background-color: var(--c--theme--colors--card-border);
}
.bg-primary-bg {
background-color: var(--c--theme--colors--primary-bg);
}
.bg-primary-action {
background-color: var(--c--theme--colors--primary-action);
}
.bg-primary-050 {
background-color: var(--c--theme--colors--primary-050);
}
@@ -1342,18 +1233,42 @@
background-color: var(--c--theme--colors--primary-150);
}
.bg-primary-950 {
background-color: var(--c--theme--colors--primary-950);
.bg-greyscale-text {
background-color: var(--c--theme--colors--greyscale-text);
}
.bg-info-150 {
background-color: var(--c--theme--colors--info-150);
.bg-greyscale-050 {
background-color: var(--c--theme--colors--greyscale-050);
}
.bg-greyscale-250 {
background-color: var(--c--theme--colors--greyscale-250);
}
.bg-greyscale-350 {
background-color: var(--c--theme--colors--greyscale-350);
}
.bg-greyscale-750 {
background-color: var(--c--theme--colors--greyscale-750);
}
.bg-greyscale-950 {
background-color: var(--c--theme--colors--greyscale-950);
}
.bg-greyscale-1000 {
background-color: var(--c--theme--colors--greyscale-1000);
}
.bg-primary-action {
background-color: var(--c--theme--colors--primary-action);
}
.bg-primary-bg {
background-color: var(--c--theme--colors--primary-bg);
}
.bg-blue-400 {
background-color: var(--c--theme--colors--blue-400);
}

View File

@@ -3,84 +3,87 @@ export const tokens = {
default: {
theme: {
colors: {
'secondary-text': '#555F6B',
'secondary-100': '#F2F7FC',
'secondary-200': '#EBF3FA',
'secondary-300': '#E2EEF8',
'secondary-400': '#DDEAF7',
'secondary-500': '#D4E5F5',
'secondary-600': '#C1D0DF',
'secondary-700': '#97A3AE',
'secondary-800': '#757E87',
'secondary-900': '#596067',
'info-text': '#fff',
'info-100': '#EBF2FC',
'info-200': '#8CB5EA',
'info-300': '#5894E1',
'info-400': '#377FDB',
'info-500': '#055FD2',
'info-600': '#0556BF',
'info-700': '#044395',
'info-800': '#033474',
'info-900': '#022858',
'greyscale-100': '#FAFAFB',
'greyscale-200': '#F3F4F4',
'greyscale-300': '#E7E8EA',
'greyscale-400': '#C2C6CA',
'greyscale-500': '#9EA3AA',
'greyscale-600': '#79818A',
'greyscale-700': '#555F6B',
'greyscale-800': '#303C4B',
'greyscale-900': '#0C1A2B',
'secondary-text': '#fff',
'secondary-100': '#fee9ea',
'secondary-200': '#fedfdf',
'secondary-300': '#fdbfbf',
'secondary-400': '#e1020f',
'secondary-500': '#c91a1f',
'secondary-600': '#5e2b2b',
'secondary-700': '#3b2424',
'secondary-800': '#341f1f',
'secondary-900': '#2b1919',
'info-text': '#0078f3',
'info-100': '#E8EDFF',
'info-200': '#DDE5FF',
'info-300': '#BCCDFF',
'info-400': '#518FFF',
'info-500': '#0078F3',
'info-600': '#0063CB',
'info-700': '#273961',
'info-800': '#222A3F',
'info-900': '#1D2437',
'greyscale-100': '#eee',
'greyscale-200': '#E5E5E5',
'greyscale-300': '#CECECE',
'greyscale-400': '#929292',
'greyscale-500': '#7C7C7C',
'greyscale-600': '#666666',
'greyscale-700': '#3A3A3A',
'greyscale-800': '#2A2A2A',
'greyscale-900': '#242424',
'greyscale-000': '#fff',
'primary-100': '#EDF5FA',
'primary-200': '#8CB5EA',
'primary-300': '#5894E1',
'primary-400': '#377FDB',
'primary-500': '#055FD2',
'primary-600': '#0556BF',
'primary-700': '#044395',
'primary-800': '#033474',
'primary-900': '#022858',
'success-100': '#EFFCD3',
'success-200': '#DBFAA9',
'success-300': '#BEF27C',
'success-400': '#A0E659',
'success-500': '#76D628',
'success-600': '#5AB81D',
'success-700': '#419A14',
'success-800': '#2C7C0C',
'success-900': '#1D6607',
'warning-100': '#FFF8CD',
'warning-200': '#FFEF9B',
'warning-300': '#FFE469',
'warning-400': '#FFDA43',
'warning-500': '#FFC805',
'warning-600': '#DBA603',
'warning-700': '#B78702',
'warning-800': '#936901',
'warning-900': '#7A5400',
'danger-100': '#F4B0B0',
'danger-200': '#EE8A8A',
'danger-300': '#E65454',
'danger-400': '#E13333',
'danger-500': '#DA0000',
'danger-600': '#C60000',
'danger-700': '#9B0000',
'danger-800': '#780000',
'danger-900': '#5C0000',
'primary-text': '#fff',
'success-text': '#fff',
'warning-text': '#fff',
'danger-text': '#fff',
'card-border': '#ededed',
'primary-bg': '#FAFAFA',
'primary-action': '#1212FF',
'primary-100': '#ECECFE',
'primary-200': '#E3E3FD',
'primary-300': '#CACAFB',
'primary-400': '#8585F6',
'primary-500': '#6A6AF4',
'primary-600': '#313178',
'primary-700': '#272747',
'primary-800': '#000091',
'primary-900': '#21213F',
'success-100': '#dffee6',
'success-200': '#b8fec9',
'success-300': '#88fdaa',
'success-400': '#3bea7e',
'success-500': '#1f8d49',
'success-600': '#18753c',
'success-700': '#204129',
'success-800': '#1e2e22',
'success-900': '#19281d',
'warning-100': '#fff4f3',
'warning-200': '#ffe9e6',
'warning-300': '#ffded9',
'warning-400': '#ffbeb4',
'warning-500': '#d64d00',
'warning-600': '#b34000',
'warning-700': '#5e2c21',
'warning-800': '#3e241e',
'warning-900': '#361e19',
'danger-100': '#FFE9E9',
'danger-200': '#FFDDDD',
'danger-300': '#FFBDBD',
'danger-400': '#FF5655',
'danger-500': '#F60700',
'danger-600': '#CE0500',
'danger-700': '#642626',
'danger-800': '#412121',
'danger-900': '#391C1C',
'primary-text': '#000091',
'success-text': '#1f8d49',
'warning-text': '#d64d00',
'danger-text': '#FFF',
'primary-050': '#F5F5FE',
'primary-150': '#E5EEFA',
'primary-950': '#1B1B35',
'info-150': '#E5EEFA',
'primary-150': '#F4F4FD',
'greyscale-text': '#303C4B',
'greyscale-050': '#F6F6F6',
'greyscale-250': '#ddd',
'greyscale-350': '#ddd',
'greyscale-750': '#353535',
'greyscale-950': '#1E1E1E',
'greyscale-1000': '#161616',
'primary-action': '#1212FF',
'primary-bg': '#FAFAFA',
'blue-400': '#7AB1E8',
'blue-500': '#417DC4',
'blue-600': '#3558A2',
@@ -145,10 +148,7 @@ export const tokens = {
extrabold: 800,
black: 900,
},
families: {
base: '"Roboto Flex Variable", sans-serif',
accent: '"Roboto Flex Variable", sans-serif',
},
families: { base: 'Marianne', accent: 'Marianne' },
letterSpacings: {
h1: 'normal',
h2: 'normal',
@@ -202,141 +202,164 @@ export const tokens = {
xl: '1200px',
xxl: '1400px',
xxs: '320px',
mobile: '768px',
tablet: '1024px',
},
logo: {
src: '/assets/logo-gouv.svg',
widthHeader: '110px',
widthFooter: '220px',
alt: 'Gouvernement Logo',
},
logo: { src: '', widthHeader: '', widthFooter: '', alt: '' },
},
components: {
datagrid: {
header: {
weight: 'var(--c--theme--font--weights--extrabold)',
size: 'var(--c--theme--font--sizes--ml)',
},
cell: {
color: 'var(--c--theme--colors--primary-500)',
size: 'var(--c--theme--font--sizes--ml)',
},
},
'forms-checkbox': {
'background-color': { hover: '#055fd214' },
color: 'var(--c--theme--colors--primary-500)',
'font-size': 'var(--c--theme--font--sizes--ml)',
},
'forms-datepicker': {
'border-color': 'var(--c--theme--colors--primary-500)',
'value-color': 'var(--c--theme--colors--primary-500)',
'border-radius': {
hover: 'var(--c--components--forms-datepicker--border-radius)',
focus: 'var(--c--components--forms-datepicker--border-radius)',
},
},
'forms-field': {
color: 'var(--c--theme--colors--primary-500)',
'value-color': 'var(--c--theme--colors--primary-500)',
width: 'auto',
},
'forms-input': {
'value-color': 'var(--c--theme--colors--primary-500)',
'border-color': 'var(--c--theme--colors--primary-500)',
color: {
error: 'var(--c--theme--colors--danger-500)',
'error-hover': 'var(--c--theme--colors--danger-500)',
'box-shadow-error-hover': 'var(--c--theme--colors--danger-500)',
},
},
'forms-labelledbox': {
'label-color': {
small: 'var(--c--theme--colors--primary-500)',
'small-disabled': 'var(--c--theme--colors--greyscale-400)',
big: { disabled: 'var(--c--theme--colors--greyscale-400)' },
},
},
'forms-select': {
'border-color': 'var(--c--theme--colors--primary-500)',
'border-color-disabled-hover':
'var(--c--theme--colors--greyscale-200)',
'border-radius': {
hover: 'var(--c--components--forms-select--border-radius)',
focus: 'var(--c--components--forms-select--border-radius)',
},
'font-size': 'var(--c--theme--font--sizes--ml)',
'menu-background-color': '#fff',
'item-background-color': {
hover: 'var(--c--theme--colors--primary-300)',
},
},
'forms-switch': {
'accent-color': 'var(--c--theme--colors--primary-400)',
},
'forms-textarea': {
'border-color': 'var(--c--components--forms-textarea--border-color)',
'border-color-hover':
'var(--c--components--forms-textarea--border-color)',
'border-radius': {
hover: 'var(--c--components--forms-textarea--border-radius)',
focus: 'var(--c--components--forms-textarea--border-radius)',
},
color: 'var(--c--theme--colors--primary-500)',
disabled: {
'border-color-hover': 'var(--c--theme--colors--greyscale-200)',
},
},
modal: { 'background-color': '#fff' },
modal: { 'width-small': '342px' },
button: {
'border-radius': {
active: 'var(--c--components--button--border-radius)',
'medium-height': '40px',
'medium-text-height': '40px',
'border-radius': '4px',
'small-height': '26px',
primary: {
'background--color': 'var(--c--theme--colors--primary-text)',
'background--color-hover': '#1212ff',
'background--color-active': '#2323ff',
'background--color-disabled':
'var(--c--theme--colors--greyscale-100)',
color: '#fff',
'color-hover': '#fff',
'color-active': '#fff',
'color-focus-visible': '#fff',
disabled: 'var(--c--theme--colors--greyscale-500)',
},
'medium-height': 'auto',
'small-height': 'auto',
success: {
color: 'white',
'color-disabled': 'white',
'color-hover': 'white',
background: {
color: 'var(--c--theme--colors--success-600)',
'color-disabled': 'var(--c--theme--colors--greyscale-300)',
'color-hover': 'var(--c--theme--colors--success-800)',
},
'primary-text': {
'background--color': 'var(--c--theme--colors--primary-text)',
'background--color-hover': 'var(--c--theme--colors--greyscale-100)',
'background--color-active': 'var(--c--theme--colors--primary-100)',
'background--color-focus-visible': '#fff',
'background--color-disabled':
'var(--c--theme--colors--greyscale-000)',
color: 'var(--c--theme--colors--primary-800)',
'color-hover': 'var(--c--theme--colors--primary-800)',
disabled: 'var(--c--theme--colors--greyscale-400)',
},
secondary: {
'background--color-hover': '#F6F6F6',
'background--color-active': '#EDEDED',
'background--color-focus-visible':
'var(--c--theme--colors--greyscale-000)',
'background--disabled': 'var(--c--theme--colors--greyscale-000)',
color: 'var(--c--theme--colors--primary-800)',
'border--color': 'var(--c--theme--colors--greyscale-300)',
'border--color-hover': 'var(--c--theme--colors--greyscale-300)',
'border--color-disabled': 'var(--c--theme--colors--greyscale-300)',
disabled: 'var(--c--theme--colors--greyscale-400)',
},
tertiary: {
'background--color': 'var(--c--theme--colors--primary-100)',
'background--color-focus-visible':
'var(--c--theme--colors--primary-100)',
'background--color-hover': 'var(--c--theme--colors--primary-300)',
'background--color-active': 'var(--c--theme--colors--primary-300)',
'background--disabled': 'var(--c--theme--colors--primary-050)',
color: 'var(--c--theme--colors--primary-800)',
disabled: 'var(--c--theme--colors--primary-300)',
},
'tertiary-text': {
'background--color-hover': 'var(--c--theme--colors--greyscale-100)',
'color-hover': 'var(--c--theme--colors--primary-text)',
color: 'var(--c--theme--colors--primary-600)',
},
danger: {
'color-hover': 'white',
background: {
color: 'var(--c--theme--colors--danger-600)',
'color-hover': '#FF2725',
'color-disabled': 'var(--c--theme--colors--danger-100)',
},
'background--color': 'var(--c--theme--colors--danger-600)',
'background--color-hover': '#FF2725',
'background--color-focus-visible':
'var(--c--theme--colors--danger-600)',
'background--color-disabled':
'var(--c--theme--colors--greyscale-100)',
'color-disabled': 'var(--c--theme--colors--greyscale-400)',
},
primary: {
color: 'var(--c--theme--colors--primary-text)',
'color-active': 'var(--c--theme--colors--primary-text)',
background: {
color: 'var(--c--theme--colors--primary-400)',
'color-active': 'var(--c--theme--colors--primary-500)',
},
border: { 'color-active': 'transparent' },
},
secondary: {
color: 'var(--c--theme--colors--primary-500)',
'color-hover': 'var(--c--theme--colors--primary-text)',
background: {
color: 'white',
'color-hover': 'var(--c--theme--colors--primary-700)',
},
border: { color: 'var(--c--theme--colors--greyscale-300)' },
},
tertiary: {
color: 'var(--c--theme--colors--primary-text)',
'color-disabled': 'var(--c--theme--colors--greyscale-600)',
background: {
color: 'var(--c--theme--colors--primary-100)',
'color-hover': 'var(--c--theme--colors--primary-300)',
'color-active': 'var(--c--theme--colors--primary-100)',
'color-disabled': 'var(--c--theme--colors--greyscale-200)',
},
},
disabled: { color: 'white', background: { color: '#b3cef0' } },
},
'la-gauffre': { activated: false },
'home-proconnect': { activated: false },
datagrid: {
'header--color': '#666666',
'header--size': '12px',
'header--weight': '500',
'body--background-color-hover': '#eee',
},
'forms-checkbox': {
'border-radius': '4px',
'border-color': 'var(--c--theme--colors--primary-800)',
'background-color--hover': 'var(--c--theme--colors--greyscale-100)',
'border--color-disabled': 'var(--c--theme--colors--greyscale-200)',
'border--color': 'var(--c--theme--colors--primary-800)',
'background--disabled': 'var(--c--theme--colors--greyscale-200)',
'background--enable': 'var(--c--theme--colors--primary-800)',
'check--disabled': 'var(--c--theme--colors--greyscale-300)',
'check--enable': 'var(--c--theme--colors--greyscale-000)',
color: 'var(--c--theme--colors--primary-text)',
'label--color': 'var(--c--theme--colors--greyscale-1000)',
'label--size': 'var(--c--theme--font--sizes--sm)',
'label--weight': '500',
'text--color': 'var(--c--theme--colors--greyscale-600)',
'text--size': 'var(--c--theme--font--sizes--s)',
'text--weight': '400',
'text--color-disabled': 'var(--c--theme--colors--greyscale-300)',
},
'forms-labelledbox': {
'label-color--small': '#1E1E1E',
'label-color--small--disabled': '#CECECE',
'label-color--big': '#1E1E1E',
'label-color--big--disabled': '#CECECE',
},
'forms-radio': {
'border-color': 'var(--c--theme--colors--primary-800)',
'background-color': 'var(--c--theme--colors--greyscale-000)',
'accent-color': 'var(--c--theme--colors--primary-800)',
'accent-color-disabled': 'var(--c--theme--colors--greyscale-300)',
},
'forms-switch': {
'border--color-disabled': 'var(--c--theme--colors--greyscale-300)',
'border--color': 'var(--c--theme--colors--primary-800)',
'handle-background-color': 'white',
'handle-background-color--disabled':
'var(--c--theme--colors--greyscale-000)',
'rail-background-color--disabled':
'var(--c--theme--colors--greyscale-000)',
'accent-color': 'var(--c--theme--colors--primary-800)',
},
'forms-textarea': {
'label-color--focus': '#161616',
'border-radius': '4px',
'border-color': '#929292',
'box-shadow--color--hover': '#929292',
'box-shadow--color--focus': '#000091',
'value-color': '#1E1E1E',
'value-color--disabled': '#CECECE',
'font-size': '14px',
},
'forms-input': {
'label-color--focus': '#161616',
'border-radius': '4px',
'border-color': '#929292',
'box-shadow--color--hover': '#929292',
'box-shadow--color--focus': '#000091',
'value-color': '#1E1E1E',
'value-color--disabled': '#CECECE',
'font-size': '14px',
},
'forms-select': {
'label-color--focus': '#161616',
'item-font-size': '14px',
'border-radius': '4px',
'border-radius-hover': '4px',
'border-color': '#929292',
'box-shadow--color--hover': '#929292',
'box-shadow--color--focus': '#000091',
'value-color': '#1E1E1E',
'font-size': '14px',
},
'la-gauffre': { activated: true },
'home-proconnect': { activated: true },
},
},
dark: {
@@ -391,211 +414,5 @@ export const tokens = {
},
},
},
dsfr: {
theme: {
colors: {
'card-border': '#E5E5E5',
'primary-text': '#000091',
'primary-100': '#ECECFE',
'primary-150': '#F4F4FD',
'primary-200': '#E3E3FD',
'primary-300': '#CACAFB',
'primary-400': '#8585F6',
'primary-500': '#6A6AF4',
'primary-600': '#313178',
'primary-700': '#272747',
'primary-800': '#000091',
'primary-900': '#21213F',
'secondary-text': '#fff',
'secondary-100': '#fee9ea',
'secondary-200': '#fedfdf',
'secondary-300': '#fdbfbf',
'secondary-400': '#e1020f',
'secondary-500': '#c91a1f',
'secondary-600': '#5e2b2b',
'secondary-700': '#3b2424',
'secondary-800': '#341f1f',
'secondary-900': '#2b1919',
'greyscale-text': '#303C4B',
'greyscale-000': '#fff',
'greyscale-050': '#F6F6F6',
'greyscale-100': '#eee',
'greyscale-200': '#E5E5E5',
'greyscale-250': '#ddd',
'greyscale-300': '#CECECE',
'greyscale-350': '#ddd',
'greyscale-400': '#929292',
'greyscale-500': '#7C7C7C',
'greyscale-600': '#666666',
'greyscale-700': '#3A3A3A',
'greyscale-750': '#353535',
'greyscale-800': '#2A2A2A',
'greyscale-900': '#242424',
'greyscale-950': '#1E1E1E',
'greyscale-1000': '#161616',
'success-text': '#1f8d49',
'success-100': '#dffee6',
'success-200': '#b8fec9',
'success-300': '#88fdaa',
'success-400': '#3bea7e',
'success-500': '#1f8d49',
'success-600': '#18753c',
'success-700': '#204129',
'success-800': '#1e2e22',
'success-900': '#19281d',
'info-text': '#0078f3',
'info-100': '#E8EDFF',
'info-200': '#DDE5FF',
'info-300': '#BCCDFF',
'info-400': '#518FFF',
'info-500': '#0078F3',
'info-600': '#0063CB',
'info-700': '#273961',
'info-800': '#222A3F',
'info-900': '#1D2437',
'warning-text': '#d64d00',
'warning-100': '#fff4f3',
'warning-200': '#ffe9e6',
'warning-300': '#ffded9',
'warning-400': '#ffbeb4',
'warning-500': '#d64d00',
'warning-600': '#b34000',
'warning-700': '#5e2c21',
'warning-800': '#3e241e',
'warning-900': '#361e19',
'danger-text': '#FFF',
'danger-100': '#FFE9E9',
'danger-200': '#FFDDDD',
'danger-300': '#FFBDBD',
'danger-400': '#FF5655',
'danger-500': '#F60700',
'danger-600': '#CE0500',
'danger-700': '#642626',
'danger-800': '#412121',
'danger-900': '#391C1C',
},
font: { families: { accent: 'Marianne', base: 'Marianne' } },
logo: {
src: '/assets/logo-gouv.svg',
widthHeader: '110px',
widthFooter: '220px',
alt: 'Gouvernement Logo',
},
},
components: {
alert: {
'border-radius': '0',
error: {
'background-color': 'var(--c--theme--colors--danger-100)',
'border-left-color': 'var(--c--theme--colors--danger-400)',
close: {
color: 'white',
'background-color': 'var(--c--theme--colors--danger-400)',
'background-color-hover': 'var(--c--theme--colors--danger-600)',
},
},
},
modal: { 'width-small': '342px' },
button: {
'medium-height': '40px',
'medium-text-height': '40px',
'border-radius': '4px',
primary: {
background: {
color: 'var(--c--theme--colors--primary-text)',
'color-hover': '#1212ff',
'color-active': '#2323ff',
},
color: '#fff',
'color-hover': '#fff',
'color-active': '#fff',
},
'primary-text': {
background: {
'color-hover': 'var(--c--theme--colors--primary-100)',
'color-active': 'var(--c--theme--colors--primary-100)',
},
'color-hover': 'var(--c--theme--colors--primary-text)',
color: 'var(--c--theme--colors--primary-800)',
},
secondary: {
background: { 'color-hover': '#F6F6F6', 'color-active': '#EDEDED' },
border: {
color: 'var(--c--theme--colors--greyscale-300)',
'color-hover': 'var(--c--theme--colors--greyscale-300)',
},
color: 'var(--c--theme--colors--primary-text)',
},
'tertiary-text': {
background: {
'color-hover': 'var(--c--theme--colors--greyscale-100)',
},
'color-hover': 'var(--c--theme--colors--primary-text)',
color: 'var(--c--theme--colors--primary-600)',
},
},
datagrid: {
header: {
color: 'var(--c--theme--colors--primary-text)',
size: 'var(--c--theme--font--sizes--s)',
},
body: {
'background-color': 'transparent',
'background-color-hover': '#F4F4FD',
},
pagination: {
'background-color': 'transparent',
'background-color-active': 'var(--c--theme--colors--primary-300)',
'border-color': 'var(--c--theme--colors--primary-400)',
},
},
'forms-checkbox': {
'border-radius': '0',
color: 'var(--c--theme--colors--primary-text)',
text: {
color: 'var(--c--theme--colors--greyscale-text)',
size: 'var(--c--theme--font--sizes--t)',
},
},
'forms-datepicker': { 'border-radius': '0' },
'forms-fileuploader': { 'border-radius': '0' },
'forms-field': {
color: 'var(--c--theme--colors--primary-text)',
'footer-font-size': 'var(--c--theme--font--sizes--t)',
'footer-color': 'var(--c--theme--colors--greyscale-text)',
},
'forms-input': {
'border-radius': '4px',
'background-color': '#fff',
'border-color': 'var(--c--theme--colors--primary-text)',
'box-shadow-color': 'var(--c--theme--colors--primary-text)',
'value-color': 'var(--c--theme--colors--primary-text)',
'font-size': '14px',
},
'forms-labelledbox': {
'label-color': { big: 'var(--c--theme--colors--primary-text)' },
},
'forms-radio': {
'accent-color': 'var(--c--theme--colors--primary-600)',
},
'forms-select': {
'item-font-size': '14px',
'border-radius': '4px',
'border-radius-hover': '4px',
'background-color': '#fff',
'border-color': 'var(--c--theme--colors--primary-text)',
'border-color-hover': 'var(--c--theme--colors--primary-text)',
'box-shadow-color': 'var(--c--theme--colors--primary-text)',
},
'forms-switch': {
'handle-border-radius': '2px',
'rail-border-radius': '4px',
'accent-color': 'var(--c--theme--colors--primary-text)',
},
'forms-textarea': { 'border-radius': '0' },
'la-gauffre': { activated: true },
'home-proconnect': { activated: true },
},
},
},
};

View File

@@ -3,7 +3,7 @@ import { create } from 'zustand';
import { tokens } from './cunningham-tokens';
type Tokens = typeof tokens.themes.default & Partial<typeof tokens.themes.dsfr>;
type Tokens = typeof tokens.themes.default;
type ColorsTokens = Tokens['theme']['colors'];
type FontSizesTokens = Tokens['theme']['font']['sizes'];
type SpacingsTokens = Tokens['theme']['spacings'];
@@ -28,7 +28,7 @@ export const useCunninghamTheme = create<AuthStore>((set, get) => {
) as Tokens;
return {
theme: 'dsfr',
theme: 'default',
themeTokens: () => currentTheme().theme,
colorsTokens: () => currentTheme().theme.colors,
componentTokens: () => currentTheme().components,

View File

@@ -1,10 +1,7 @@
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
import {
Tooltip,
VariantType,
useToastProvider,
} from '@openfun/cunningham-react';
import { Tooltip } from '@openfun/cunningham-react';
import { useQueryClient } from '@tanstack/react-query';
import React, { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
@@ -15,11 +12,13 @@ import {
Doc,
KEY_DOC,
KEY_LIST_DOC,
KEY_SUB_DOC,
useTrans,
useUpdateDoc,
} from '@/docs/doc-management';
import { useBroadcastStore, useResponsiveStore } from '@/stores';
import { useDocTreeStore } from '../../doc-tree/context/DocTreeContext';
interface DocTitleProps {
doc: Doc;
}
@@ -55,20 +54,27 @@ export const DocTitleText = ({ title }: DocTitleTextProps) => {
const DocTitleInput = ({ doc }: DocTitleProps) => {
const { isDesktop } = useResponsiveStore();
const { t } = useTranslation();
const queryClient = useQueryClient();
const { colorsTokens } = useCunninghamTheme();
const treeStore = useDocTreeStore();
const [titleDisplay, setTitleDisplay] = useState(doc.title);
const { toast } = useToastProvider();
const { untitledDocument } = useTrans();
const { broadcast } = useBroadcastStore();
const { mutate: updateDoc } = useUpdateDoc({
listInvalideQueries: [KEY_DOC, KEY_LIST_DOC],
onSuccess(data) {
toast(t('Document title updated successfully'), VariantType.SUCCESS);
listInvalideQueries: [KEY_LIST_DOC],
onSuccess(updatedDoc) {
// Broadcast to every user connected to the document
broadcast(`${KEY_DOC}-${data.id}`);
broadcast(`${KEY_DOC}-${updatedDoc.id}`);
if (updatedDoc.id === treeStore.root?.id) {
treeStore.setRoot(updatedDoc);
}
queryClient.setQueryData(
[KEY_SUB_DOC, { id: updatedDoc.id }],
updatedDoc,
);
},
});

View File

@@ -6,6 +6,7 @@ import { Doc } from '../types';
export type DocParams = {
id: string;
isTree?: boolean;
};
export const getDoc = async ({ id }: DocParams): Promise<Doc> => {
@@ -19,14 +20,15 @@ export const getDoc = async ({ id }: DocParams): Promise<Doc> => {
};
export const KEY_DOC = 'doc';
export const KEY_SUB_DOC = 'sub-doc';
export const KEY_DOC_VISIBILITY = 'doc-visibility';
export function useDoc(
param: DocParams,
queryConfig?: UseQueryOptions<Doc, APIError, Doc>,
queryConfig?: Omit<UseQueryOptions<Doc, APIError, Doc>, 'queryFn'>,
) {
return useQuery<Doc, APIError, Doc>({
queryKey: [KEY_DOC, param],
queryKey: queryConfig?.queryKey ?? [KEY_DOC, param],
queryFn: () => getDoc(param),
...queryConfig,
});

View File

@@ -8,6 +8,7 @@ import {
useAPIInfiniteQuery,
} from '@/api';
import { DocSearchTarget } from '../../doc-search/components/DocSearchFilters';
import { Doc } from '../types';
export const isDocsOrdering = (data: string): data is DocsOrdering => {
@@ -31,6 +32,8 @@ export type DocsParams = {
is_creator_me?: boolean;
title?: string;
is_favorite?: boolean;
target?: DocSearchTarget;
parent_id?: string;
};
export type DocsResponse = APIList<Doc>;
@@ -53,8 +56,14 @@ export const getDocs = async (params: DocsParams): Promise<DocsResponse> => {
if (params.is_favorite !== undefined) {
searchParams.set('is_favorite', params.is_favorite.toString());
}
const response = await fetchAPI(`documents/?${searchParams.toString()}`);
let response: Response;
if (params.parent_id && params.target === DocSearchTarget.CURRENT) {
response = await fetchAPI(
`documents/${params.parent_id}/descendants/?${searchParams.toString()}`,
);
} else {
response = await fetchAPI(`documents/?${searchParams.toString()}`);
}
if (!response.ok) {
throw new APIError('Failed to get the docs', await errorCauses(response));

View File

@@ -17,16 +17,20 @@ import { Doc } from '../types';
interface ModalRemoveDocProps {
onClose: () => void;
doc: Doc;
afterDelete?: (doc: Doc) => void;
}
export const ModalRemoveDoc = ({ onClose, doc }: ModalRemoveDocProps) => {
export const ModalRemoveDoc = ({
onClose,
doc,
afterDelete,
}: ModalRemoveDocProps) => {
const { toast } = useToastProvider();
const { push } = useRouter();
const pathname = usePathname();
const {
mutate: removeDoc,
isError,
error,
} = useRemoveDoc({
@@ -34,6 +38,11 @@ export const ModalRemoveDoc = ({ onClose, doc }: ModalRemoveDocProps) => {
toast(t('The document has been deleted.'), VariantType.SUCCESS, {
duration: 4000,
});
if (afterDelete) {
afterDelete(doc);
return;
}
if (pathname === '/') {
onClose();
} else {
@@ -87,7 +96,9 @@ export const ModalRemoveDoc = ({ onClose, doc }: ModalRemoveDocProps) => {
<Box aria-label={t('Content modal to delete document')}>
{!isError && (
<Text $size="sm" $variation="600">
{t('Are you sure you want to delete this document ?')}
{t('Are you sure you want to delete the document "{{title}}"?', {
title: doc.title ?? t('Untitled document'),
})}
</Text>
)}

View File

@@ -42,10 +42,14 @@ export interface Doc {
is_favorite: boolean;
link_reach: LinkReach;
link_role: LinkRole;
nb_accesses_ancestors: number;
nb_accesses_direct: number;
user_roles: Role[];
created_at: string;
updated_at: string;
nb_accesses_direct: number;
nb_accesses_ancestors: number;
children?: Doc[];
childrenCount?: number;
numchild: number;
abilities: {
accesses_manage: boolean;
accesses_view: boolean;

View File

@@ -0,0 +1,66 @@
import { Button } from '@openfun/cunningham-react';
import { useTranslation } from 'react-i18next';
import { Box } from '@/components';
import { FilterDropdown } from '@/components/filter/FilterDropdown';
export enum DocSearchTarget {
ALL = 'all',
CURRENT = 'current',
}
export type DocSearchFiltersValues = {
target?: DocSearchTarget;
};
export type DocSearchFiltersProps = {
values?: DocSearchFiltersValues;
onValuesChange?: (values: DocSearchFiltersValues) => void;
onReset?: () => void;
};
export const DocSearchFilters = ({
values,
onValuesChange,
onReset,
}: DocSearchFiltersProps) => {
const { t } = useTranslation();
const hasFilters = Object.keys(values ?? {}).length > 0;
const handleTargetChange = (target: DocSearchTarget) => {
onValuesChange?.({ ...values, target });
};
return (
<Box
$direction="row"
$align="center"
$height="35px"
$justify="space-between"
$gap="10px"
$margin={{ vertical: 'base' }}
>
<Box $direction="row" $align="center" $gap="10px">
<FilterDropdown
selectedValue={values?.target}
options={[
{
label: t('All docs'),
value: DocSearchTarget.ALL,
callback: () => handleTargetChange(DocSearchTarget.ALL),
},
{
label: t('Current doc'),
value: DocSearchTarget.CURRENT,
callback: () => handleTargetChange(DocSearchTarget.CURRENT),
},
]}
/>
</Box>
{hasFilters && (
<Button color="primary-text" size="small" onClick={onReset}>
{t('Reset')}
</Button>
)}
</Box>
);
};

View File

@@ -15,17 +15,37 @@ import {
import { Doc, useInfiniteDocs } from '@/docs/doc-management';
import { useResponsiveStore } from '@/stores';
import { useDocTreeStore } from '../../doc-tree/context/DocTreeContext';
import EmptySearchIcon from '../assets/illustration-docs-empty.png';
import {
DocSearchFilters,
DocSearchFiltersValues,
DocSearchTarget,
} from './DocSearchFilters';
import { DocSearchItem } from './DocSearchItem';
type DocSearchModalProps = ModalProps & {};
type DocSearchModalProps = ModalProps & {
showFilters?: boolean;
defaultFilters?: DocSearchFiltersValues;
};
export const DocSearchModal = ({ ...modalProps }: DocSearchModalProps) => {
export const DocSearchModal = ({
showFilters = false,
defaultFilters,
...modalProps
}: DocSearchModalProps) => {
const { t } = useTranslation();
const router = useRouter();
const treeStore = useDocTreeStore();
const [search, setSearch] = useState('');
const [filters, setFilters] = useState<DocSearchFiltersValues>(
defaultFilters ?? {},
);
const { isDesktop } = useResponsiveStore();
const {
data,
isFetching,
@@ -36,27 +56,41 @@ export const DocSearchModal = ({ ...modalProps }: DocSearchModalProps) => {
} = useInfiniteDocs({
page: 1,
title: search,
...filters,
parent_id: treeStore?.root?.id,
});
const loading = isFetching || isRefetching || isLoading;
const handleInputSearch = useDebouncedCallback(setSearch, 300);
const handleSelect = (doc: Doc) => {
if (treeStore?.initialRootId !== doc.id) {
treeStore.setSelectedNode(doc);
treeStore.setRoot(doc);
treeStore.setInitialTargetId(doc.id);
}
router.push(`/docs/${doc.id}`);
modalProps.onClose?.();
};
const handleResetFilters = () => {
setFilters({});
};
const docsData: QuickSearchData<Doc> = useMemo(() => {
const docs = data?.pages.flatMap((page) => page.results) || [];
const groupName =
filters.target === DocSearchTarget.CURRENT
? t('Select a page')
: t('Select a document');
return {
groupName: docs.length > 0 ? t('Select a document') : '',
groupName: docs.length > 0 ? groupName : '',
elements: search ? docs : [],
emptyString: t('No document found'),
endActions: hasNextPage
? [{ content: <InView onChange={() => void fetchNextPage()} /> }]
: [],
};
}, [data, hasNextPage, fetchNextPage, t, search]);
}, [data, hasNextPage, fetchNextPage, t, search, filters.target]);
return (
<Modal
@@ -75,6 +109,13 @@ export const DocSearchModal = ({ ...modalProps }: DocSearchModalProps) => {
onFilter={handleInputSearch}
>
<Box $height={isDesktop ? '500px' : 'calc(100vh - 68px - 1rem)'}>
{showFilters && (
<DocSearchFilters
values={filters}
onValuesChange={setFilters}
onReset={handleResetFilters}
/>
)}
{search.length === 0 && (
<Box
$direction="column"

View File

@@ -1,6 +1,6 @@
import { UseQueryOptions, useQuery } from '@tanstack/react-query';
import { APIError, APIList, errorCauses, fetchAPI } from '@/api';
import { APIError, errorCauses, fetchAPI } from '@/api';
import { Doc } from '@/docs/doc-management';
import { User } from '@/features/auth';
@@ -9,14 +9,14 @@ export type UsersParams = {
docId: Doc['id'];
};
type UsersResponse = APIList<User>;
type UsersResponse = User[];
export const getUsers = async ({
query,
docId,
}: UsersParams): Promise<UsersResponse> => {
const queriesParams = [];
queriesParams.push(query ? `q=${query}` : '');
queriesParams.push(query ? `q=${encodeURIComponent(query)}` : '');
queriesParams.push(docId ? `document_id=${docId}` : '');
const queryParams = queriesParams.filter(Boolean).join('&');

View File

@@ -3,6 +3,7 @@ import {
VariantType,
useToastProvider,
} from '@openfun/cunningham-react';
import { useQueryClient } from '@tanstack/react-query';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
@@ -11,7 +12,7 @@ import { APIError } from '@/api';
import { Box } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { User } from '@/features/auth';
import { Doc, Role } from '@/features/docs';
import { Doc, KEY_SUB_DOC, Role } from '@/features/docs';
import { useCreateDocAccess, useCreateDocInvitation } from '../api';
import { OptionType } from '../types';
@@ -39,6 +40,7 @@ export const DocShareAddMemberList = ({
}: Props) => {
const { t } = useTranslation();
const { toast } = useToastProvider();
const queryClient = useQueryClient();
const [isLoading, setIsLoading] = useState(false);
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
const [invitationRole, setInvitationRole] = useState<Role>(Role.EDITOR);
@@ -91,14 +93,32 @@ export const DocShareAddMemberList = ({
};
return isInvitationMode
? createInvitation({
...payload,
email: user.email,
})
: createDocAccess({
...payload,
memberId: user.id,
});
? createInvitation(
{
...payload,
email: user.email,
},
{
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: [KEY_SUB_DOC, { id: doc.id }],
});
},
},
)
: createDocAccess(
{
...payload,
memberId: user.id,
},
{
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: [KEY_SUB_DOC, { id: doc.id }],
});
},
},
);
});
const settledPromises = await Promise.allSettled(promises);

View File

@@ -1,4 +1,5 @@
import { VariantType, useToastProvider } from '@openfun/cunningham-react';
import { useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import {
@@ -8,7 +9,7 @@ import {
IconOptions,
} from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { Doc, Role } from '@/docs/doc-management';
import { Doc, KEY_SUB_DOC, Role } from '@/docs/doc-management';
import { User } from '@/features/auth';
import { useDeleteDocInvitation, useUpdateDocInvitation } from '../api';
@@ -23,6 +24,7 @@ type Props = {
};
export const DocShareInvitationItem = ({ doc, invitation }: Props) => {
const { t } = useTranslation();
const queryClient = useQueryClient();
const { spacingsTokens } = useCunninghamTheme();
const spacing = spacingsTokens();
const fakeUser: User = {
@@ -37,6 +39,11 @@ export const DocShareInvitationItem = ({ doc, invitation }: Props) => {
const canUpdate = doc.abilities.accesses_manage;
const { mutate: updateDocInvitation } = useUpdateDocInvitation({
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: [KEY_SUB_DOC, { id: doc.id }],
});
},
onError: (error) => {
toast(
error?.data?.role?.[0] ?? t('Error during update invitation'),
@@ -49,6 +56,11 @@ export const DocShareInvitationItem = ({ doc, invitation }: Props) => {
});
const { mutate: removeDocInvitation } = useDeleteDocInvitation({
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: [KEY_SUB_DOC, { id: doc.id }],
});
},
onError: (error) => {
toast(
error?.data?.role?.[0] ?? t('Error during delete invitation'),

View File

@@ -1,4 +1,5 @@
import { VariantType, useToastProvider } from '@openfun/cunningham-react';
import { useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import {
@@ -8,7 +9,7 @@ import {
IconOptions,
} from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { Access, Doc, Role } from '@/docs/doc-management/';
import { Access, Doc, KEY_SUB_DOC, Role } from '@/docs/doc-management/';
import { useResponsiveStore } from '@/stores';
import { useDeleteDocAccess, useUpdateDocAccess } from '../api';
@@ -25,13 +26,20 @@ export const DocShareMemberItem = ({ doc, access }: Props) => {
const { t } = useTranslation();
const { isLastOwner, isOtherOwner } = useWhoAmI(access);
const { toast } = useToastProvider();
const { isDesktop } = useResponsiveStore();
const { spacingsTokens } = useCunninghamTheme();
const queryClient = useQueryClient();
const spacing = spacingsTokens();
const isNotAllowed =
isOtherOwner || !!isLastOwner || !doc.abilities.accesses_manage;
const { mutate: updateDocAccess } = useUpdateDocAccess({
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: [KEY_SUB_DOC, { id: doc.id }],
});
},
onError: () => {
toast(t('Error during invitation update'), VariantType.ERROR, {
duration: 4000,
@@ -40,6 +48,11 @@ export const DocShareMemberItem = ({ doc, access }: Props) => {
});
const { mutate: removeDocAccess } = useDeleteDocAccess({
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: [KEY_SUB_DOC, { id: doc.id }],
});
},
onError: () => {
toast(t('Error while deleting invitation'), VariantType.ERROR, {
duration: 4000,

View File

@@ -58,6 +58,7 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
const canViewAccesses = doc.abilities.accesses_view;
const showMemberSection = inputValue === '' && selectedUsers.length === 0;
const showFooter = selectedUsers.length === 0 && !inputValue;
const MIN_CHARACTERS_FOR_SEARCH = 4;
const onSelect = (user: User) => {
setSelectedUsers((prev) => [...prev, user]);
@@ -76,7 +77,7 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
const searchUsersQuery = useUsers(
{ query: userQuery, docId: doc.id },
{
enabled: !!userQuery,
enabled: userQuery?.length > MIN_CHARACTERS_FOR_SEARCH,
queryKey: [KEY_LIST_USER, { query: userQuery }],
},
);
@@ -125,7 +126,7 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
}, [invitationQuery, t]);
const searchUserData: QuickSearchData<User> = useMemo(() => {
const users = searchUsersQuery.data?.results || [];
const users = searchUsersQuery.data || [];
const isEmail = isValidEmail(userQuery);
const newUser: User = {
id: userQuery,

View File

@@ -0,0 +1,44 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { APIError, errorCauses, fetchAPI } from '@/api';
import { Doc, KEY_LIST_DOC } from '../../doc-management';
export type CreateDocParam = Pick<Doc, 'title'> & {
parentId: string;
};
export const createDocChildren = async ({
title,
parentId,
}: CreateDocParam): Promise<Doc> => {
const response = await fetchAPI(`documents/${parentId}/children/`, {
method: 'POST',
body: JSON.stringify({
title,
}),
});
if (!response.ok) {
throw new APIError('Failed to create the doc', await errorCauses(response));
}
return response.json() as Promise<Doc>;
};
interface CreateDocProps {
onSuccess: (data: Doc) => void;
}
export function useCreateChildrenDoc({ onSuccess }: CreateDocProps) {
const queryClient = useQueryClient();
return useMutation<Doc, APIError, CreateDocParam>({
mutationFn: createDocChildren,
onSuccess: (data) => {
void queryClient.resetQueries({
queryKey: [KEY_LIST_DOC],
});
onSuccess(data);
},
});
}

View File

@@ -0,0 +1,58 @@
import { UseQueryOptions, useQuery } from '@tanstack/react-query';
import { APIError, errorCauses, fetchAPI, useAPIInfiniteQuery } from '@/api';
import { DocsResponse } from '../../doc-management';
export type DocsChildrenParams = {
docId: string;
page?: number;
page_size?: number;
};
export const getDocChildren = async (
params: DocsChildrenParams,
): Promise<DocsResponse> => {
const { docId, page, page_size } = params;
const searchParams = new URLSearchParams();
if (page) {
searchParams.set('page', page.toString());
}
if (page_size) {
searchParams.set('page_size', page_size.toString());
}
const response = await fetchAPI(
`documents/${docId}/children/?${searchParams.toString()}`,
);
if (!response.ok) {
throw new APIError(
'Failed to get the doc children',
await errorCauses(response),
);
}
return response.json() as Promise<DocsResponse>;
};
export const KEY_LIST_DOC_CHILDREN = 'doc-children';
export function useDocChildren(
params: DocsChildrenParams,
queryConfig?: Omit<
UseQueryOptions<DocsResponse, APIError, DocsResponse>,
'queryKey' | 'queryFn'
>,
) {
return useQuery<DocsResponse, APIError, DocsResponse>({
queryKey: [KEY_LIST_DOC_CHILDREN, params],
queryFn: () => getDocChildren(params),
...queryConfig,
});
}
export const useInfiniteDocChildren = (params: DocsChildrenParams) => {
return useAPIInfiniteQuery(KEY_LIST_DOC_CHILDREN, getDocChildren, params);
};

View File

@@ -0,0 +1,45 @@
import { UseQueryOptions, useQuery } from '@tanstack/react-query';
import { APIError, errorCauses, fetchAPI } from '@/api';
import { Doc } from '../../doc-management';
export type DocsTreeParams = {
docId: string;
};
export const getDocTree = async (params: DocsTreeParams): Promise<Doc> => {
const { docId } = params;
const searchParams = new URLSearchParams();
const response = await fetchAPI(
`documents/${docId}/tree/?${searchParams.toString()}`,
);
if (!response.ok) {
throw new APIError(
'Failed to get the doc tree',
await errorCauses(response),
);
}
return response.json() as Promise<Doc>;
};
export const KEY_LIST_DOC_CHILDREN = 'doc-tree';
export function useDocTree(
params: DocsTreeParams,
queryConfig?: Omit<
UseQueryOptions<Doc, APIError, Doc>,
'queryKey' | 'queryFn'
>,
) {
return useQuery<Doc, APIError, Doc>({
queryKey: [KEY_LIST_DOC_CHILDREN, params],
queryFn: () => getDocTree(params),
staleTime: 0,
refetchOnWindowFocus: false,
...queryConfig,
});
}

View File

@@ -0,0 +1,36 @@
import { TreeViewMoveModeEnum } from '@gouvfr-lasuite/ui-kit';
import { useMutation } from '@tanstack/react-query';
import { APIError, errorCauses, fetchAPI } from '@/api';
export type MoveDocParam = {
sourceDocumentId: string;
targetDocumentId: string;
position: TreeViewMoveModeEnum;
};
export const moveDoc = async ({
sourceDocumentId,
targetDocumentId,
position,
}: MoveDocParam): Promise<void> => {
const response = await fetchAPI(`documents/${sourceDocumentId}/move/`, {
method: 'POST',
body: JSON.stringify({
target_document_id: targetDocumentId,
position,
}),
});
if (!response.ok) {
throw new APIError('Failed to move the doc', await errorCauses(response));
}
return response.json() as Promise<void>;
};
export function useMoveDoc() {
return useMutation<void, APIError, MoveDocParam>({
mutationFn: moveDoc,
});
}

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.40918 4.69434C5.28613 4.69434 5.18359 4.65332 5.10156 4.57129C5.02409 4.48926 4.98535 4.389 4.98535 4.27051C4.98535 4.15202 5.02409 4.05404 5.10156 3.97656C5.18359 3.89453 5.28613 3.85352 5.40918 3.85352H10.5977C10.7161 3.85352 10.8141 3.89453 10.8916 3.97656C10.9736 4.05404 11.0146 4.15202 11.0146 4.27051C11.0146 4.389 10.9736 4.48926 10.8916 4.57129C10.8141 4.65332 10.7161 4.69434 10.5977 4.69434H5.40918ZM5.40918 7.08008C5.28613 7.08008 5.18359 7.03906 5.10156 6.95703C5.02409 6.875 4.98535 6.77474 4.98535 6.65625C4.98535 6.53776 5.02409 6.43978 5.10156 6.3623C5.18359 6.28027 5.28613 6.23926 5.40918 6.23926H10.5977C10.7161 6.23926 10.8141 6.28027 10.8916 6.3623C10.9736 6.43978 11.0146 6.53776 11.0146 6.65625C11.0146 6.77474 10.9736 6.875 10.8916 6.95703C10.8141 7.03906 10.7161 7.08008 10.5977 7.08008H5.40918ZM5.40918 9.46582C5.28613 9.46582 5.18359 9.42708 5.10156 9.34961C5.02409 9.26758 4.98535 9.1696 4.98535 9.05566C4.98535 8.93262 5.02409 8.83008 5.10156 8.74805C5.18359 8.66602 5.28613 8.625 5.40918 8.625H7.86328C7.98633 8.625 8.08659 8.66602 8.16406 8.74805C8.24609 8.83008 8.28711 8.93262 8.28711 9.05566C8.28711 9.1696 8.24609 9.26758 8.16406 9.34961C8.08659 9.42708 7.98633 9.46582 7.86328 9.46582H5.40918ZM2.25098 13.2529V2.88281C2.25098 2.17188 2.42643 1.63639 2.77734 1.27637C3.13281 0.916341 3.66374 0.736328 4.37012 0.736328H11.6299C12.3363 0.736328 12.8649 0.916341 13.2158 1.27637C13.5713 1.63639 13.749 2.17188 13.749 2.88281V13.2529C13.749 13.9684 13.5713 14.5039 13.2158 14.8594C12.8649 15.2148 12.3363 15.3926 11.6299 15.3926H4.37012C3.66374 15.3926 3.13281 15.2148 2.77734 14.8594C2.42643 14.5039 2.25098 13.9684 2.25098 13.2529ZM3.35156 13.2324C3.35156 13.5742 3.44043 13.8363 3.61816 14.0186C3.80046 14.2008 4.06934 14.292 4.4248 14.292H11.5752C11.9307 14.292 12.1973 14.2008 12.375 14.0186C12.5573 13.8363 12.6484 13.5742 12.6484 13.2324V2.90332C12.6484 2.56152 12.5573 2.29948 12.375 2.11719C12.1973 1.93034 11.9307 1.83691 11.5752 1.83691H4.4248C4.06934 1.83691 3.80046 1.93034 3.61816 2.11719C3.44043 2.29948 3.35156 2.56152 3.35156 2.90332V13.2324Z" fill="#8585F6"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,178 @@
import {
TreeViewItem,
TreeViewNodeProps,
useTree,
} from '@gouvfr-lasuite/ui-kit';
import { useRouter } from 'next/navigation';
import { useEffect, useRef } from 'react';
import { css } from 'styled-components';
import { Box, Icon, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { Doc, KEY_SUB_DOC, useDoc } from '@/features/docs/doc-management';
import { useLeftPanelStore } from '@/features/left-panel';
import Logo from './../assets/sub-page-logo.svg';
import { DocTreeItemActions } from './DocTreeItemActions';
const ItemTextCss = css`
overflow: hidden;
text-overflow: ellipsis;
white-space: initial;
display: -webkit-box;
line-clamp: 1;
/* width: 100%; */
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
`;
type Props = TreeViewNodeProps<Doc> & {
treeData: ReturnType<typeof useTree<Doc>>;
doc: Doc;
setSelectedNode: (node: Doc) => void;
};
export const DocSubPageItem = ({
doc,
setSelectedNode,
treeData,
...props
}: Props) => {
const { loadChildren, node } = props;
const isInitialLoad = useRef(false);
const { spacingsTokens } = useCunninghamTheme();
const spacing = spacingsTokens();
const router = useRouter();
const { togglePanel } = useLeftPanelStore();
const { data: docQuery } = useDoc(
{ isTree: true, id: doc.id },
{
initialData: doc,
queryKey: [KEY_SUB_DOC, { id: doc.id }],
refetchOnMount: false,
refetchOnWindowFocus: false,
},
);
useEffect(() => {
if (docQuery && isInitialLoad.current === true) {
console.log('docQuery', docQuery);
treeData?.updateNode(docQuery.id, docQuery);
}
if (docQuery) {
isInitialLoad.current = true;
}
}, [docQuery, treeData]);
const afterCreate = (createdDoc: Doc) => {
const actualChildren = node.data.children ?? [];
if (actualChildren.length === 0 && loadChildren) {
loadChildren(node?.data.value)
.then((allChildren) => {
node.open();
router.push(`/docs/${doc.id}`);
treeData?.setChildren(node.data.value.id, allChildren);
togglePanel();
})
.catch(console.error);
} else {
const newDoc = {
...createdDoc,
children: [],
childrenCount: 0,
parentId: node.id,
};
treeData?.addChild(node.data.value.id, newDoc);
node.open();
router.push(`/docs/${createdDoc.id}`);
togglePanel();
}
};
if (!treeData) {
return null;
}
return (
<TreeViewItem
{...props}
loadChildren={() =>
treeData?.handleLoadChildren(props.node.data.value.id)
}
onClick={() => {
setSelectedNode(props.node.data.value as Doc);
router.push(`/docs/${props.node.data.value.id}`);
}}
>
<Box
data-testid={`doc-sub-page-item-${props.node.data.value.id}`}
$width="100%"
$direction="row"
$gap={spacing['xs']}
role="button"
tabIndex={0}
$align="center"
$css={css`
.light-doc-item-actions {
display: 'flex';
opacity: 0;
&:has(.isOpen) {
opacity: 1;
}
}
&:hover {
.light-doc-item-actions {
opacity: 1;
}
}
`}
>
<Box $width={16} $height={16}>
<Logo />
</Box>
<Box
$direction="row"
$align="center"
$css={css`
display: flex;
flex-direction: row;
width: 100%;
gap: 0.5rem;
align-items: center;
`}
>
<Text $css={ItemTextCss} $size="sm">
{doc.title}
</Text>
{doc.nb_accesses_direct > 1 && (
<Icon
$isMaterialIcon="filled"
iconName="group"
$size="16px"
$variation="400"
/>
)}
</Box>
<Box
$direction="row"
$gap={spacing['xs']}
$align="center"
className="light-doc-item-actions"
>
<DocTreeItemActions
doc={doc}
treeData={treeData}
parentId={node.data.parentKey}
onCreateSuccess={afterCreate}
/>
</Box>
</Box>
</TreeViewItem>
);
};

View File

@@ -0,0 +1,238 @@
import {
OpenMap,
TreeView,
TreeViewMoveResult,
useTree,
} from '@gouvfr-lasuite/ui-kit';
import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { css } from 'styled-components';
import { Box, SeparatedSection, StyledLink } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import {
Doc,
KEY_DOC,
LinkReach,
LinkRole,
getDoc,
} from '../../doc-management';
import { SimpleDocItem } from '../../docs-grid';
import { getDocChildren } from '../api/useDocChildren';
import { useDocTree } from '../api/useDocTree';
import { useMoveDoc } from '../api/useMove';
import { subPageToTree, useDocTreeStore } from '../context/DocTreeContext';
import { DocSubPageItem } from './DocSubPageItem';
import { DocTreeItemActions } from './DocTreeItemActions';
type DocTreeProps = {
initialTargetId: string;
};
export const DocTree = ({ initialTargetId }: DocTreeProps) => {
const { spacingsTokens } = useCunninghamTheme();
const queryClient = useQueryClient();
const store = useDocTreeStore();
const spacing = spacingsTokens();
const treeData = useTree(
[],
async (docId) => {
const doc = await getDoc({ id: docId });
const newDoc = { ...doc, childrenCount: doc.numchild };
void queryClient.setQueryData([KEY_DOC, { id: docId }], newDoc);
return newDoc;
},
async (docId) => {
const doc = await getDocChildren({ docId: docId });
return subPageToTree(doc.results ?? []);
},
);
const router = useRouter();
const [initialOpenState, setInitialOpenState] = useState<OpenMap | undefined>(
undefined,
);
const { mutate: moveDoc } = useMoveDoc();
const { data } = useDocTree({
docId: initialTargetId,
});
const handleMove = (result: TreeViewMoveResult) => {
moveDoc({
sourceDocumentId: result.sourceId,
targetDocumentId: result.targetModeId,
position: result.mode,
});
treeData?.handleMove(result);
};
const buildDocTree = (data?: Doc) => {
if (!data) {
return;
}
const { children: rootChildren, ...root } = data;
const children = rootChildren ?? [];
store.setRoot(root);
const initialOpenState: OpenMap = {};
initialOpenState[root.id] = true;
subPageToTree(children, (child) => {
if (child?.children?.length && child?.children?.length > 0) {
initialOpenState[child.id] = true;
}
});
treeData.resetTree(children);
setInitialOpenState(initialOpenState);
if (initialTargetId === root.id) {
treeData?.setSelectedNode(root);
} else {
treeData?.selectNodeById(initialTargetId);
}
};
useEffect(() => {
if (treeData?.selectedNode?.id !== store.selectedNode?.id) {
store.setSelectedNode(treeData?.selectedNode ?? null);
}
}, [store, treeData?.selectedNode]);
useEffect(() => {
buildDocTree(data);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]);
const rootIsSelected = treeData?.selectedNode?.id === store.root?.id;
if (!initialTargetId) {
return null;
}
return (
<Box data-testid="doc-tree" $height="100%">
<SeparatedSection showSeparator={false}>
<Box $padding={{ horizontal: 'sm' }}>
<Box
$css={css`
padding: ${spacing['2xs']};
border-radius: 4px;
width: 100%;
background-color: ${rootIsSelected
? 'var(--c--theme--colors--greyscale-100)'
: 'transparent'};
&:hover {
background-color: var(--c--theme--colors--greyscale-100);
}
.doc-tree-root-item-actions {
display: 'flex';
opacity: 0;
&:has(.isOpen) {
opacity: 1;
}
}
&:hover {
.doc-tree-root-item-actions {
opacity: 1;
}
}
`}
>
{store.root !== null && (
<StyledLink
$css={css`
width: 100%;
`}
href={`/docs/${store.root.id}`}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
treeData?.setSelectedNode(store.root ?? undefined);
router.push(`/docs/${store.root?.id}`);
}}
>
<Box $direction="row" $align="center" $width="100%">
<SimpleDocItem doc={store.root} showAccesses={true} />
<div className="doc-tree-root-item-actions">
<DocTreeItemActions
doc={store.root}
treeData={treeData}
onCreateSuccess={(createdDoc) => {
const newDoc = {
...createdDoc,
children: [],
childrenCount: 0,
parentId: store.root?.id ?? undefined,
};
treeData?.addChild(null, newDoc);
}}
/>
</div>
</Box>
</StyledLink>
)}
</Box>
</Box>
</SeparatedSection>
<button
onClick={() => {
const children = data?.children ?? [];
const newDoc = {
id: '1',
children: [],
childrenCount: 0,
title: 'TITI',
creator: 'test',
is_favorite: false,
};
// Ajout des propriétés manquantes pour correspondre au type Doc
const completeDoc = {
...newDoc,
link_reach: LinkReach.PUBLIC,
link_role: LinkRole.EDITOR,
user_roles: [],
// Ajoutez ici les autres propriétés requises par le type Doc
};
children.push(completeDoc);
buildDocTree(data);
}}
>
Refresh
</button>
{initialOpenState && treeData.nodes.length > 0 && (
<TreeView
handleMove={handleMove}
initialOpenState={initialOpenState}
selectedNodeId={
treeData.selectedNode?.id ?? store.initialTargetId ?? undefined
}
treeData={treeData.nodes ?? []}
rootNodeId={store.root?.id ?? ''}
renderNode={(props) => {
if (treeData === undefined) {
return null;
}
return (
<DocSubPageItem
{...props}
treeData={treeData}
doc={props.node.data.value as Doc}
loadChildren={(node) => treeData.handleLoadChildren(node.id)}
setSelectedNode={(node) => treeData.setSelectedNode(node)}
/>
);
}}
/>
)}
</Box>
);
};

View File

@@ -0,0 +1,129 @@
import {
DropdownMenu,
DropdownMenuOption,
useTree,
} from '@gouvfr-lasuite/ui-kit';
import { useModal } from '@openfun/cunningham-react';
import { useRouter } from 'next/navigation';
import { Fragment, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { Box, BoxButton, Icon } from '@/components';
import { useLeftPanelStore } from '@/features/left-panel';
import { Doc, ModalRemoveDoc } from '../../doc-management';
import { useCreateChildrenDoc } from '../api/useCreateChildren';
import { useDocTreeStore } from '../context/DocTreeContext';
type DocTreeItemActionsProps = {
doc: Doc;
parentId?: string | null;
treeData: ReturnType<typeof useTree<Doc>>;
onCreateSuccess?: (newDoc: Doc) => void;
};
export const DocTreeItemActions = ({
doc,
parentId,
onCreateSuccess,
treeData,
}: DocTreeItemActionsProps) => {
const [isOpen, setIsOpen] = useState(false);
const router = useRouter();
const treeStore = useDocTreeStore();
const { t } = useTranslation();
const deleteModal = useModal();
const { togglePanel } = useLeftPanelStore();
const options: DropdownMenuOption[] = [
{
label: t('Delete'),
icon: <Icon iconName="delete" />,
callback: deleteModal.open,
},
];
const { mutate: createChildrenDoc } = useCreateChildrenDoc({
onSuccess: (doc) => {
onCreateSuccess?.(doc);
togglePanel();
treeData.setSelectedNode(doc);
router.push(`/docs/${doc.id}`);
},
});
const afterDelete = () => {
if (parentId) {
router.push(`/docs/${parentId}`);
treeData?.selectNodeById(parentId);
treeData?.deleteNode(doc.id);
void treeData?.refreshNode(parentId);
} else if (doc.id === treeStore.root?.id && !parentId) {
router.push(`/docs/`);
} else if (treeStore.root) {
router.push(`/docs/${treeStore.root.id}`);
treeData?.deleteNode(doc.id);
treeData?.setSelectedNode(treeStore.root);
}
};
return (
<Fragment>
<Box
$direction="row"
$align="center"
className={` ${isOpen ? 'isOpen' : ''}`}
$css={css`
gap: var(--c--theme----c--theme--spacings--xs);
`}
>
<DropdownMenu
options={options}
isOpen={isOpen}
onOpenChange={setIsOpen}
>
<Icon
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
setIsOpen(!isOpen);
}}
iconName="more_horiz"
$isMaterialIcon="filled"
$theme="primary"
$variation="600"
/>
</DropdownMenu>
<BoxButton
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
createChildrenDoc({
title: t('Untitled page'),
parentId: doc.id,
});
}}
color="primary"
>
<Icon
$variation="800"
$theme="primary"
$isMaterialIcon="filled"
iconName="add_box"
/>
</BoxButton>
</Box>
{deleteModal.isOpen && (
<ModalRemoveDoc
onClose={deleteModal.onClose}
doc={doc}
afterDelete={afterDelete}
/>
)}
</Fragment>
);
};

View File

@@ -0,0 +1,37 @@
import { TreeViewDataType } from '@gouvfr-lasuite/ui-kit';
import { create } from 'zustand';
import { Doc } from '../../doc-management';
export const subPageToTree = (
children: Doc[],
callback?: (doc: Doc) => void,
): TreeViewDataType<Doc>[] => {
children.forEach((child) => {
child.childrenCount = child.numchild ?? 0;
callback?.(child);
subPageToTree(child.children ?? [], callback);
});
return children;
};
interface DocTreeStore {
initialTargetId?: string | null;
initialRootId?: string | null;
setRoot: (doc: Doc | null) => void;
root: Doc | null;
setInitialTargetId: (id: string) => void;
setSelectedNode: (node: Doc | null) => void;
selectedNode: Doc | null;
}
export const useDocTreeStore = create<DocTreeStore>((set) => ({
root: null,
selectedNode: null,
initialTargetId: undefined,
initialRootId: undefined,
setRoot: (doc) => set({ root: doc }),
setInitialTargetId: (id) => set({ initialTargetId: id }),
setSelectedNode: (node) => set({ selectedNode: node }),
}));

View File

@@ -0,0 +1,189 @@
import {
DndContext,
DragEndEvent,
DragOverlay,
DragStartEvent,
KeyboardSensor,
Modifier,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import { getEventCoordinates } from '@dnd-kit/utilities';
import { TreeViewMoveModeEnum } from '@gouvfr-lasuite/ui-kit';
import { useQueryClient } from '@tanstack/react-query';
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, Text } from '@/components';
import { Doc, KEY_LIST_DOC, Role } from '../../doc-management';
import { useMoveDoc } from '../../doc-tree/api/useMove';
import { DocsGridItem } from './DocsGridItem';
import { Draggable } from './dnd/Draggable';
import { Droppable } from './dnd/Droppable';
const activationConstraint = {
distance: 20,
};
type DocGridContentListProps = {
docs: Doc[];
};
export const DocGridContentList = ({ docs }: DocGridContentListProps) => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const [selectedDoc, setSelectedDoc] = useState<Doc>();
const canDrag = selectedDoc?.user_roles.some((role) => role === Role.OWNER);
const [canDrop, setCanDrop] = useState<boolean>();
const { mutate: handleMove } = useMoveDoc();
const mouseSensor = useSensor(MouseSensor, {
activationConstraint,
});
const touchSensor = useSensor(TouchSensor, {
activationConstraint,
});
const keyboardSensor = useSensor(KeyboardSensor, {});
const sensors = useSensors(mouseSensor, touchSensor, keyboardSensor);
const handleDragStart = (e: DragStartEvent) => {
document.body.style.cursor = 'grabbing';
if (e.active.data.current) {
setSelectedDoc(e.active.data.current as Doc);
}
};
const handleDragEnd = (e: DragEndEvent) => {
setSelectedDoc(undefined);
setCanDrop(undefined);
document.body.style.cursor = 'default';
if (!canDrag || !canDrop) {
return;
}
const { active, over } = e;
if (!over?.id || active.id === over?.id) {
return;
}
handleMove(
{
sourceDocumentId: active.id as string,
targetDocumentId: over.id as string,
position: TreeViewMoveModeEnum.FIRST_CHILD,
},
{
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: [KEY_LIST_DOC],
});
},
},
);
};
const overlayText = useMemo(() => {
if (!canDrag) {
return t('You must be the owner to move the document');
}
if (!canDrop) {
return t('You must be at least the editor of the target document');
}
return selectedDoc?.title || t('Unnamed document');
}, [canDrag, canDrop, selectedDoc, t]);
const overlayBgColor = useMemo(() => {
if (!canDrag) {
return 'var(--c--theme--colors--danger-600)';
}
if (canDrop !== undefined && !canDrop) {
return 'var(--c--theme--colors--danger-600)';
}
return '#5858D3';
}, [canDrag, canDrop]);
if (docs.length === 0) {
return null;
}
return (
<DndContext
sensors={sensors}
modifiers={[snapToTopLeft]}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
{docs.map((doc) => {
const canDropItem = doc.user_roles.some(
(role) =>
role === Role.ADMIN || role === Role.OWNER || role === Role.EDITOR,
);
return (
<Droppable
enabledDrop={canDrag}
canDrop={canDrag && canDropItem}
onOver={(isOver) => {
if (isOver) {
setCanDrop(canDropItem);
}
}}
key={doc.id}
id={doc.id}
data={doc}
>
<Draggable key={doc.id} id={doc.id} data={doc}>
<DocsGridItem dragMode={!!selectedDoc} doc={doc} key={doc.id} />
</Draggable>
</Droppable>
);
})}
<DragOverlay dropAnimation={null}>
<Box
$width="fit-content"
$padding={{ horizontal: 'xs', vertical: '3xs' }}
$radius="12px"
$background={overlayBgColor}
data-testid="drag-doc-overlay"
$height="auto"
>
<Text $size="xs" $variation="000" $weight="500">
{overlayText}
</Text>
</Box>
</DragOverlay>
</DndContext>
);
};
export const snapToTopLeft: Modifier = ({
activatorEvent,
draggingNodeRect,
transform,
}) => {
if (draggingNodeRect && activatorEvent) {
const activatorCoordinates = getEventCoordinates(activatorEvent);
if (!activatorCoordinates) {
return transform;
}
const offsetX = activatorCoordinates.x - draggingNodeRect.left;
const offsetY = activatorCoordinates.y - draggingNodeRect.top;
return {
...transform,
x: transform.x + offsetX - 3,
y: transform.y + offsetY - 3,
};
}
return transform;
};

View File

@@ -9,7 +9,7 @@ import { useResponsiveStore } from '@/stores';
import { useResponsiveDocGrid } from '../hooks/useResponsiveDocGrid';
import { DocsGridItem } from './DocsGridItem';
import { DocGridContentList } from './DocGridContentList';
import { DocsGridLoader } from './DocsGridLoader';
type DocsGridProps = {
@@ -37,6 +37,9 @@ export const DocsGrid = ({
is_creator_me: target === DocDefaultFilter.MY_DOCS,
}),
});
const docs = data?.pages.flatMap((page) => page.results) ?? [];
const loading = isFetching || isLoading;
const hasDocs = data?.pages.some((page) => page.results.length > 0);
const loadMore = (inView: boolean) => {
@@ -114,11 +117,7 @@ export const DocsGrid = ({
)}
</Box>
{data?.pages.map((currentPage) => {
return currentPage.results.map((doc) => (
<DocsGridItem doc={doc} key={doc.id} />
));
})}
<DocGridContentList docs={docs} />
{hasNextPage && !loading && (
<InView

View File

@@ -16,8 +16,9 @@ import { DocsGridItemSharedButton } from './DocsGridItemSharedButton';
import { SimpleDocItem } from './SimpleDocItem';
type DocsGridItemProps = {
doc: Doc;
dragMode?: boolean;
};
export const DocsGridItem = ({ doc }: DocsGridItemProps) => {
export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => {
const { t } = useTranslation();
const { isDesktop } = useResponsiveStore();
const { flexLeft, flexRight } = useResponsiveDocGrid();
@@ -46,7 +47,9 @@ export const DocsGridItem = ({ doc }: DocsGridItemProps) => {
cursor: pointer;
border-radius: 4px;
&:hover {
background-color: var(--c--theme--colors--greyscale-100);
background-color: ${dragMode
? 'none'
: 'var(--c--theme--colors--greyscale-100)'};
}
`}
>
@@ -79,25 +82,35 @@ export const DocsGridItem = ({ doc }: DocsGridItemProps) => {
: undefined
}
>
<Tooltip
content={
<Text $textAlign="center" $variation="000">
{isPublic
? t('Accessible to anyone')
: t('Accessible to authenticated users')}
</Text>
}
placement="top"
>
<div>
<Icon
$theme="greyscale"
$variation="600"
$size="14px"
iconName={isPublic ? 'public' : 'vpn_lock'}
/>
</div>
</Tooltip>
{dragMode && (
<Icon
$theme="greyscale"
$variation="600"
$size="14px"
iconName={isPublic ? 'public' : 'vpn_lock'}
/>
)}
{!dragMode && (
<Tooltip
content={
<Text $textAlign="center" $variation="000">
{isPublic
? t('Accessible to anyone')
: t('Accessible to authenticated users')}
</Text>
}
placement="top"
>
<div>
<Icon
$theme="greyscale"
$variation="600"
$size="14px"
iconName={isPublic ? 'public' : 'vpn_lock'}
/>
</div>
</Tooltip>
)}
</Box>
)}
</Box>

View File

@@ -38,7 +38,7 @@ export const SimpleDocItem = ({
const { untitledDocument } = useTrans();
return (
<Box $direction="row" $gap={spacings.sm} $overflow="auto">
<Box $direction="row" $gap={spacings.sm} $overflow="auto" $width="100%">
<Box
$direction="row"
$align="center"

View File

@@ -0,0 +1,25 @@
import { Data, useDraggable } from '@dnd-kit/core';
type DraggableProps<T> = {
id: string;
data?: Data<T>;
children: React.ReactNode;
};
export const Draggable = <T,>(props: DraggableProps<T>) => {
const { attributes, listeners, setNodeRef } = useDraggable({
id: props.id,
data: props.data,
});
return (
<div
ref={setNodeRef}
{...listeners}
{...attributes}
data-testid={`draggable-doc-${props.id}`}
>
{props.children}
</div>
);
};

View File

@@ -0,0 +1,48 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { Data, useDroppable } from '@dnd-kit/core';
import { useEffect } from 'react';
import { css } from 'styled-components';
import { Box } from '@/components';
import { Doc } from '@/features/docs/doc-management';
type DroppableProps = {
id: string;
onOver?: (isOver: boolean, data?: Data<Doc>) => void;
data?: Data<Doc>;
children: React.ReactNode;
enabledDrop?: boolean;
canDrop?: boolean;
};
export const Droppable = (props: DroppableProps) => {
const { isOver, setNodeRef } = useDroppable({
id: props.id,
data: props.data,
});
const enableHover = props.canDrop && isOver;
useEffect(() => {
props.onOver?.(isOver, props.data);
}, [isOver, props.data, props.onOver]);
return (
<Box
ref={setNodeRef}
data-testid={`droppable-doc-${props.id}`}
$css={css`
border-radius: 4px;
background-color: ${enableHover
? 'var(--c--theme--colors--primary-100)'
: 'transparent'};
border: 1.5px solid
${enableHover
? 'var(--c--theme--colors--primary-500)'
: 'transparent'};
`}
>
{props.children}
</Box>
);
};

View File

@@ -8,7 +8,6 @@ export const Title = () => {
const { t } = useTranslation();
const theme = useCunninghamTheme();
const spacings = theme.spacingsTokens();
const colors = theme.colorsTokens();
return (
<Box $direction="row" $align="center" $gap={spacings['2xs']}>
@@ -36,7 +35,8 @@ export const Title = () => {
`}
$width="40px"
$height="16px"
$background={colors['primary-200']}
$background="#ECECFF"
$color="#5958D3"
>
BETA
</Text>

View File

@@ -104,7 +104,7 @@ export function HomeContent() {
</Text>
<Text as="p" $display="inline">
<Trans t={t} i18nKey="home-content-open-source-part2">
You can easily self-hosted Docs (check our installation{' '}
You can easily self-host Docs (check our installation{' '}
<a
href="https://github.com/suitenumerique/docs/tree/main/docs"
target="_blank"

View File

@@ -1,15 +1,14 @@
import { css } from 'styled-components';
import { Box, SeparatedSection } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { Box } from '@/components';
import { useDocStore } from '@/docs/doc-management';
import { SimpleDocItem } from '@/docs/docs-grid';
import { DocTree } from '@/features/docs/doc-tree/components/DocTree';
import { useDocTreeStore } from '@/features/docs/doc-tree/context/DocTreeContext';
export const LeftPanelDocContent = () => {
const { currentDoc } = useDocStore();
const { spacingsTokens } = useCunninghamTheme();
const spacing = spacingsTokens();
if (!currentDoc) {
const treeStore = useDocTreeStore();
if (!currentDoc || !treeStore.initialTargetId) {
return null;
}
@@ -19,19 +18,9 @@ export const LeftPanelDocContent = () => {
$width="100%"
$css="width: 100%; overflow-y: auto; overflow-x: hidden;"
>
<SeparatedSection showSeparator={false}>
<Box $padding={{ horizontal: 'sm' }}>
<Box
$css={css`
padding: ${spacing['2xs']};
border-radius: 4px;
background-color: var(--c--theme--colors--greyscale-100);
`}
>
<SimpleDocItem doc={currentDoc} showAccesses={true} />
</Box>
</Box>
</SeparatedSection>
{treeStore.initialTargetId && (
<DocTree initialTargetId={treeStore.initialTargetId} />
)}
</Box>
);
};

View File

@@ -1,12 +1,15 @@
import { Button, ModalSize, useModal } from '@openfun/cunningham-react';
import { t } from 'i18next';
import { useRouter } from 'next/navigation';
import { useRouter } from 'next/router';
import { PropsWithChildren } from 'react';
import { Box, Icon, SeparatedSection } from '@/components';
import { useCreateDoc } from '@/docs/doc-management';
import { DocSearchModal } from '@/docs/doc-search';
import { useAuth } from '@/features/auth';
import { useCreateDoc, useDocStore } from '@/features/docs/doc-management';
import { DocSearchTarget } from '@/features/docs/doc-search/components/DocSearchFilters';
import { useCreateChildrenDoc } from '@/features/docs/doc-tree/api/useCreateChildren';
import { useDocTreeStore } from '@/features/docs/doc-tree/context/DocTreeContext';
import { useCmdK } from '@/hook/useCmdK';
import { useLeftPanelStore } from '../stores';
@@ -15,23 +18,52 @@ export const LeftPanelHeader = ({ children }: PropsWithChildren) => {
const router = useRouter();
const searchModal = useModal();
const { authenticated } = useAuth();
useCmdK(searchModal.open);
const treeStore = useDocTreeStore();
const { currentDoc } = useDocStore();
const isDoc = router.pathname === '/docs/[id]';
useCmdK(() => {
const isEditorToolbarOpen =
document.getElementsByClassName('bn-formatting-toolbar').length > 0;
if (isEditorToolbarOpen) {
return;
}
searchModal.open();
});
const { togglePanel } = useLeftPanelStore();
const { mutate: createDoc } = useCreateDoc({
onSuccess: (doc) => {
router.push(`/docs/${doc.id}`);
void router.push(`/docs/${doc.id}`);
togglePanel();
},
});
const { mutate: createChildrenDoc } = useCreateChildrenDoc({
onSuccess: (doc) => {
treeStore.treeData?.addRootNode(doc);
treeStore.treeData?.selectNodeById(doc.id);
void router.push(`/docs/${doc.id}`);
togglePanel();
},
});
const goToHome = () => {
router.push('/');
void router.push('/');
togglePanel();
};
const createNewDoc = () => {
createDoc();
if (treeStore.root && isDoc) {
createChildrenDoc({
title: t('Untitled page'),
parentId: treeStore.root.id,
});
} else {
createDoc();
}
};
return (
@@ -65,15 +97,29 @@ export const LeftPanelHeader = ({ children }: PropsWithChildren) => {
/>
)}
</Box>
{authenticated && (
<Button onClick={createNewDoc}>{t('New doc')}</Button>
<Button
color={!isDoc ? 'primary' : 'tertiary'}
onClick={createNewDoc}
disabled={currentDoc && !currentDoc.abilities.update}
>
{t(isDoc ? 'New page' : 'New doc')}
</Button>
)}
</Box>
</SeparatedSection>
{children}
</Box>
{searchModal.isOpen && (
<DocSearchModal {...searchModal} size={ModalSize.LARGE} />
<DocSearchModal
{...searchModal}
size={ModalSize.LARGE}
showFilters={isDoc}
defaultFilters={{
target: isDoc ? DocSearchTarget.CURRENT : undefined,
}}
/>
)}
</>
);

View File

@@ -175,6 +175,7 @@ export class ApiPlugin implements WorkboxPlugin {
is_favorite: false,
nb_accesses_direct: 1,
nb_accesses_ancestors: 1,
numchild: 0,
updated_at: new Date().toISOString(),
abilities: {
accesses_manage: true,
@@ -201,6 +202,7 @@ export class ApiPlugin implements WorkboxPlugin {
},
link_reach: LinkReach.RESTRICTED,
link_role: LinkRole.READER,
user_roles: [],
};
await DocsDB.cacheResponse(

View File

@@ -5,6 +5,7 @@ export const useCmdK = (callback: () => void) => {
const down = (e: KeyboardEvent) => {
if ((e.key === 'k' || e.key === 'K') && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
callback();
}
};

View File

@@ -1,6 +1,5 @@
import type { AppProps } from 'next/app';
import Head from 'next/head';
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { AppProvider } from '@/core/';
@@ -19,14 +18,6 @@ export default function App({ Component, pageProps }: AppPropsWithLayout) {
const getLayout = Component.getLayout ?? ((page) => page);
const { t } = useTranslation();
useEffect(() => {
console.log(
`%c
\r\n \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \r\n \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \r\n \u2588\u2588\u2551 \u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \r\n \u2588\u2588\u2551\u2588\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551 \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \r\n \u255A\u2588\u2588\u2588\u2554\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \r\n \u255A\u2550\u2550\u255D\u255A\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \r\n \r`,
'font-size: 11px;line-height:15px;background-image: linear-gradient(#000091, #005f91);color: transparent;background-clip: text;',
);
}, []);
return (
<>
<Head>

View File

@@ -3,17 +3,19 @@ import { useQueryClient } from '@tanstack/react-query';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, Text, TextErrors } from '@/components';
import { DocEditor } from '@/docs/doc-editor';
import { KEY_AUTH, setAuthUrl } from '@/features/auth';
import {
Doc,
KEY_DOC,
useCollaboration,
useDoc,
useDocStore,
} from '@/docs/doc-management/';
import { KEY_AUTH, setAuthUrl } from '@/features/auth';
} from '@/features/docs/doc-management/';
import { useDocTreeStore } from '@/features/docs/doc-tree/context/DocTreeContext';
import { MainLayout } from '@/layouts';
import { useBroadcastStore } from '@/stores';
import { NextPageWithLayout } from '@/types/next';
@@ -23,6 +25,14 @@ export function DocLayout() {
query: { id },
} = useRouter();
const treeStore = useDocTreeStore();
useEffect(() => {
if (typeof id === 'string' && !treeStore.initialTargetId) {
treeStore.setInitialTargetId(id);
}
}, [id, treeStore]);
if (typeof id !== 'string') {
return null;
}
@@ -34,7 +44,7 @@ export function DocLayout() {
</Head>
<MainLayout>
<DocPage id={id} />
{treeStore.initialTargetId && <DocPage id={id} />}
</MainLayout>
</>
);
@@ -55,6 +65,8 @@ const DocPage = ({ id }: DocProps) => {
{
staleTime: 0,
queryKey: [KEY_DOC, { id }],
refetchOnMount: false,
refetchOnWindowFocus: false,
},
);
@@ -64,14 +76,15 @@ const DocPage = ({ id }: DocProps) => {
const queryClient = useQueryClient();
const { replace } = useRouter();
useCollaboration(doc?.id, doc?.content);
const { t } = useTranslation();
useEffect(() => {
if (doc?.title) {
setTimeout(() => {
document.title = `${doc.title} - Docs`;
document.title = `${doc.title} - ${t('Docs')}`;
}, 100);
}
}, [doc?.title]);
}, [doc?.title, t]);
useEffect(() => {
if (!docQuery || isFetching) {
@@ -82,6 +95,13 @@ const DocPage = ({ id }: DocProps) => {
setCurrentDoc(docQuery);
}, [docQuery, setCurrentDoc, isFetching]);
useEffect(() => {
return () => {
setCurrentDoc(undefined);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
/**
* We add a broadcast task to reset the query cache
* when the document visibility changes.

View File

@@ -1,4 +1,7 @@
@import url('../cunningham/cunningham-style.css');
@import url('@gouvfr-lasuite/ui-kit/style');
@import url('../cunningham/cunningham-tokens.css');
@import url('../cunningham/cunningham-custom-tokens.css');
@import url('@fontsource/material-icons');
body {
margin: 0;
@@ -41,3 +44,29 @@ main ::-webkit-scrollbar-thumb:hover,
cursor: pointer;
outline: inherit;
}
.material-icons-filled {
font-family: 'Material Icons', sans-serif;
font-weight: normal;
font-style: normal;
font-size: 24px; /* Preferred icon size */
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
/* Support for all WebKit browsers. */
-webkit-font-smoothing: antialiased;
/* Support for Safari and Chrome. */
text-rendering: optimizelegibility;
/* Support for Firefox. */
-moz-osx-font-smoothing: grayscale;
/* Support for IE. */
font-feature-settings: 'liga';
}

View File

@@ -15,7 +15,7 @@ export const AppWrapper = ({ children }: PropsWithChildren) => {
return (
<QueryClientProvider client={queryClient}>
<CunninghamProvider theme="dsfr">{children}</CunninghamProvider>
<CunninghamProvider theme="default">{children}</CunninghamProvider>
</QueryClientProvider>
);
};

View File

@@ -1,6 +1,6 @@
{
"name": "impress",
"version": "2.5.0",
"version": "2.6.0",
"private": true,
"workspaces": {
"packages": [

View File

@@ -1,6 +1,6 @@
{
"name": "eslint-config-impress",
"version": "2.5.0",
"version": "2.6.0",
"license": "MIT",
"scripts": {
"lint": "eslint --ext .js ."

View File

@@ -1,6 +1,6 @@
{
"name": "packages-i18n",
"version": "2.5.0",
"version": "2.6.0",
"private": true,
"scripts": {
"extract-translation": "yarn extract-translation:impress",

View File

@@ -1,6 +1,6 @@
{
"name": "server-y-provider",
"version": "2.5.0",
"version": "2.6.0",
"description": "Y.js provider for docs",
"repository": "https://github.com/numerique-gouv/impress",
"license": "MIT",

View File

@@ -225,12 +225,12 @@
"@babel/types" "^7.25.9"
"@babel/helpers@^7.26.7":
version "7.26.10"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.26.10.tgz#6baea3cd62ec2d0c1068778d63cb1314f6637384"
integrity sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==
version "7.26.7"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.26.7.tgz#fd1d2a7c431b6e39290277aacfd8367857c576a4"
integrity sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==
dependencies:
"@babel/template" "^7.26.9"
"@babel/types" "^7.26.10"
"@babel/template" "^7.25.9"
"@babel/types" "^7.26.7"
"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.23.9", "@babel/parser@^7.25.9", "@babel/parser@^7.26.5", "@babel/parser@^7.26.7":
version "7.26.7"
@@ -239,13 +239,6 @@
dependencies:
"@babel/types" "^7.26.7"
"@babel/parser@^7.26.9":
version "7.26.10"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.10.tgz#e9bdb82f14b97df6569b0b038edd436839c57749"
integrity sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==
dependencies:
"@babel/types" "^7.26.10"
"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz#cc2e53ebf0a0340777fff5ed521943e253b4d8fe"
@@ -970,6 +963,13 @@
"@babel/plugin-transform-typescript" "^7.25.9"
"@babel/runtime@^7.0.0", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.13", "@babel/runtime@^7.23.2", "@babel/runtime@^7.24.5", "@babel/runtime@^7.25.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
version "7.26.7"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.7.tgz#f4e7fe527cd710f8dc0618610b61b4b060c3c341"
integrity sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==
dependencies:
regenerator-runtime "^0.14.0"
"@babel/runtime@^7.9.2":
version "7.26.10"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.10.tgz#a07b4d8fa27af131a633d7b3524db803eb4764c2"
integrity sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==
@@ -985,15 +985,6 @@
"@babel/parser" "^7.25.9"
"@babel/types" "^7.25.9"
"@babel/template@^7.26.9":
version "7.26.9"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.26.9.tgz#4577ad3ddf43d194528cff4e1fa6b232fa609bb2"
integrity sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==
dependencies:
"@babel/code-frame" "^7.26.2"
"@babel/parser" "^7.26.9"
"@babel/types" "^7.26.9"
"@babel/traverse@^7.25.9", "@babel/traverse@^7.26.5", "@babel/traverse@^7.26.7":
version "7.26.7"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.7.tgz#99a0a136f6a75e7fb8b0a1ace421e0b25994b8bb"
@@ -1015,14 +1006,6 @@
"@babel/helper-string-parser" "^7.25.9"
"@babel/helper-validator-identifier" "^7.25.9"
"@babel/types@^7.26.10", "@babel/types@^7.26.9":
version "7.26.10"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.10.tgz#396382f6335bd4feb65741eacfc808218f859259"
integrity sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==
dependencies:
"@babel/helper-string-parser" "^7.25.9"
"@babel/helper-validator-identifier" "^7.25.9"
"@bcoe/v8-coverage@^0.2.3":
version "0.2.3"
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
@@ -1242,6 +1225,45 @@
resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz#037817b574262134cabd68fc4ec1a454f168407b"
integrity sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==
"@dnd-kit/accessibility@^3.1.1":
version "3.1.1"
resolved "https://registry.yarnpkg.com/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz#3b4202bd6bb370a0730f6734867785919beac6af"
integrity sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==
dependencies:
tslib "^2.0.0"
"@dnd-kit/core@6.3.1":
version "6.3.1"
resolved "https://registry.yarnpkg.com/@dnd-kit/core/-/core-6.3.1.tgz#4c36406a62c7baac499726f899935f93f0e6d003"
integrity sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==
dependencies:
"@dnd-kit/accessibility" "^3.1.1"
"@dnd-kit/utilities" "^3.2.2"
tslib "^2.0.0"
"@dnd-kit/modifiers@9.0.0":
version "9.0.0"
resolved "https://registry.yarnpkg.com/@dnd-kit/modifiers/-/modifiers-9.0.0.tgz#96a0280c77b10c716ef79d9792ce7ad04370771d"
integrity sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==
dependencies:
"@dnd-kit/utilities" "^3.2.2"
tslib "^2.0.0"
"@dnd-kit/sortable@10.0.0":
version "10.0.0"
resolved "https://registry.yarnpkg.com/@dnd-kit/sortable/-/sortable-10.0.0.tgz#1f9382b90d835cd5c65d92824fa9dafb78c4c3e8"
integrity sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==
dependencies:
"@dnd-kit/utilities" "^3.2.2"
tslib "^2.0.0"
"@dnd-kit/utilities@^3.2.2":
version "3.2.2"
resolved "https://registry.yarnpkg.com/@dnd-kit/utilities/-/utilities-3.2.2.tgz#5a32b6af356dc5f74d61b37d6f7129a4040ced7b"
integrity sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==
dependencies:
tslib "^2.0.0"
"@dual-bundle/import-meta-resolve@^4.1.0":
version "4.1.0"
resolved "https://registry.yarnpkg.com/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz#519c1549b0e147759e7825701ecffd25e5819f7b"
@@ -1567,6 +1589,11 @@
resolved "https://registry.yarnpkg.com/@fontsource/material-icons-outlined/-/material-icons-outlined-5.1.1.tgz#3a659277f5029fb1ea5f64739d7ef48fd2112a8e"
integrity sha512-HjWe3anHu9RptoAvm2UF6MPXkwsbFbH+vkg48GRIhXPsXyfmZteGHVFZpPxaZnakrmnLXDxUnnKSj7zzqk+Rhg==
"@fontsource/material-icons@5.2.5":
version "5.2.5"
resolved "https://registry.yarnpkg.com/@fontsource/material-icons/-/material-icons-5.2.5.tgz#cdb9dd23c0da4b021c866dc529ed4c203414ec3c"
integrity sha512-9k0LBRVgResIeD+vC/epYmm/awN2k2L8twwEtUWQ3FHluMi+7PbISOpXqksvfqPn9FJy4/KEeWOhFTR/SrbhNw==
"@formatjs/ecma402-abstract@2.3.2":
version "2.3.2"
resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.2.tgz#0ee291effe7ee2c340742a6c95d92eacb5e6c00a"
@@ -1613,6 +1640,23 @@
resolved "https://registry.yarnpkg.com/@gouvfr-lasuite/integration/-/integration-1.0.2.tgz#ed0000f4b738c5a19bb60f5b80a9a2f5d9414234"
integrity sha512-npOotZQSyu6SffHiPP+jQVOkJ3qW2KE2cANhEK92sNLX9uZqQaCqljO5GhzsBmh0lB76fiXnrr9i8SIpnDUSZg==
"@gouvfr-lasuite/ui-kit@file:../../../design-system":
version "0.1.3"
dependencies:
"@dnd-kit/core" "6.3.1"
"@dnd-kit/modifiers" "9.0.0"
"@dnd-kit/sortable" "10.0.0"
"@openfun/cunningham-react" "3.0.0"
"@types/node" "22.10.7"
clsx "2.1.1"
cmdk "1.0.4"
react "19.0.0"
react-arborist "3.4.3"
react-aria-components "1.6.0"
react-dom "19.0.0"
react-resizable-panels "2.1.7"
react-stately "3.35.0"
"@gulpjs/to-absolute-glob@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@gulpjs/to-absolute-glob/-/to-absolute-glob-4.0.0.tgz#1fc2460d3953e1d9b9f2dfdb4bcc99da4710c021"
@@ -4049,6 +4093,21 @@
"@react-types/shared" "^3.27.0"
"@swc/helpers" "^0.5.0"
"@react-dnd/asap@^4.0.0":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-4.0.1.tgz#5291850a6b58ce6f2da25352a64f1b0674871aab"
integrity sha512-kLy0PJDDwvwwTXxqTFNAAllPHD73AycE9ypWeln/IguoGBEbvFcPDbCV03G52bEcC5E+YgupBE0VzHGdC8SIXg==
"@react-dnd/invariant@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@react-dnd/invariant/-/invariant-2.0.0.tgz#09d2e81cd39e0e767d7da62df9325860f24e517e"
integrity sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw==
"@react-dnd/shallowequal@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz#a3031eb54129f2c66b2753f8404266ec7bf67f0a"
integrity sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==
"@react-pdf/fns@3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@react-pdf/fns/-/fns-3.0.0.tgz#2e0137d48b14c531b2f6a9214cb36ea2a7aea3ba"
@@ -6273,7 +6332,7 @@
dependencies:
"@types/node" "*"
"@types/node@*", "@types/node@22.13.9", "@types/node@^22.7.5":
"@types/node@*", "@types/node@22.10.7", "@types/node@22.13.9", "@types/node@^22.7.5":
version "22.13.9"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.9.tgz#5d9a8f7a975a5bd3ef267352deb96fb13ec02eca"
integrity sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==
@@ -7547,7 +7606,7 @@ clone@^2.1.2:
resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==
clsx@^2.0.0, clsx@^2.1.1:
clsx@2.1.1, clsx@^2.0.0, clsx@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
@@ -8124,6 +8183,15 @@ dir-glob@^3.0.1:
dependencies:
path-type "^4.0.0"
dnd-core@14.0.1:
version "14.0.1"
resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-14.0.1.tgz#76d000e41c494983210fb20a48b835f81a203c2e"
integrity sha512-+PVS2VPTgKFPYWo3vAFEA8WPbTf7/xo43TifH9G8S1KqnrQu0o77A3unrF5yOugy4mIz7K5wAVFHUcha7wsz6A==
dependencies:
"@react-dnd/asap" "^4.0.0"
"@react-dnd/invariant" "^2.0.0"
redux "^4.1.1"
doctrine@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
@@ -11484,6 +11552,11 @@ media-typer@0.3.0:
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==
"memoize-one@>=3.1.1 <6":
version "5.2.1"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
memoize-one@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045"
@@ -12930,6 +13003,17 @@ raw-body@2.5.2:
iconv-lite "0.4.24"
unpipe "1.0.0"
react-arborist@3.4.3:
version "3.4.3"
resolved "https://registry.yarnpkg.com/react-arborist/-/react-arborist-3.4.3.tgz#982791a07d1e279f279be88162c920112f3cee90"
integrity sha512-yFnq1nIQhT2uJY4TZVz2tgAiBb9lxSyvF4vC3S8POCK8xLzjGIxVv3/4dmYquQJ7AHxaZZArRGHiHKsEewKdTQ==
dependencies:
react-dnd "^14.0.3"
react-dnd-html5-backend "^14.0.3"
react-window "^1.8.11"
redux "^5.0.0"
use-sync-external-store "^1.2.0"
react-aria-components@1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/react-aria-components/-/react-aria-components-1.5.0.tgz#9c51ba8427e827d1192f965249e16ff7d34cbb55"
@@ -13146,6 +13230,24 @@ react-aria@^3.37.0:
"@react-aria/visually-hidden" "^3.8.19"
"@react-types/shared" "^3.27.0"
react-dnd-html5-backend@^14.0.3:
version "14.1.0"
resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-14.1.0.tgz#b35a3a0c16dd3a2bfb5eb7ec62cf0c2cace8b62f"
integrity sha512-6ONeqEC3XKVf4eVmMTe0oPds+c5B9Foyj8p/ZKLb7kL2qh9COYxiBHv3szd6gztqi/efkmriywLUVlPotqoJyw==
dependencies:
dnd-core "14.0.1"
react-dnd@^14.0.3:
version "14.0.5"
resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-14.0.5.tgz#ecf264e220ae62e35634d9b941502f3fca0185ed"
integrity sha512-9i1jSgbyVw0ELlEVt/NkCUkxy1hmhJOkePoCH713u75vzHGyXhPDm28oLfc2NMSBjZRM1Y+wRjHXJT3sPrTy+A==
dependencies:
"@react-dnd/invariant" "^2.0.0"
"@react-dnd/shallowequal" "^2.0.0"
dnd-core "14.0.1"
fast-deep-equal "^3.1.3"
hoist-non-react-statics "^3.3.2"
react-dom@*, react-dom@19.0.0:
version "19.0.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.0.0.tgz#43446f1f01c65a4cd7f7588083e686a6726cfb57"
@@ -13230,6 +13332,11 @@ react-remove-scroll@^2.6.2:
use-callback-ref "^1.3.3"
use-sidecar "^1.1.3"
react-resizable-panels@2.1.7:
version "2.1.7"
resolved "https://registry.yarnpkg.com/react-resizable-panels/-/react-resizable-panels-2.1.7.tgz#afd29d8a3d708786a9f95183a38803c89f13c2e7"
integrity sha512-JtT6gI+nURzhMYQYsx8DKkx6bSoOGFp7A3CwMrOb8y5jFHFyqwo9m68UhmXRw57fRVJksFn1TSlm3ywEQ9vMgA==
react-select@5.10.1:
version "5.10.1"
resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.10.1.tgz#e858dd98358ccd864b65d53ab0fb682cd5e96b89"
@@ -13276,6 +13383,69 @@ react-stately@3.34.0:
"@react-stately/tree" "^3.8.6"
"@react-types/shared" "^3.26.0"
react-stately@3.35.0, react-stately@^3.35.0:
version "3.35.0"
resolved "https://registry.yarnpkg.com/react-stately/-/react-stately-3.35.0.tgz#92bfc83bb4f7626a57c6aeabe4d08aeaab1fa2f7"
integrity sha512-1BH21J/TOHpyZe7c+f1BU2bnRWaBDTjLH0WdBuzNfPOXu7RBG3ebPIRvqd7UkPaVfIcol2QJnxe8S0a314JWKA==
dependencies:
"@react-stately/calendar" "^3.7.0"
"@react-stately/checkbox" "^3.6.11"
"@react-stately/collections" "^3.12.1"
"@react-stately/color" "^3.8.2"
"@react-stately/combobox" "^3.10.2"
"@react-stately/data" "^3.12.1"
"@react-stately/datepicker" "^3.12.0"
"@react-stately/disclosure" "^3.0.1"
"@react-stately/dnd" "^3.5.1"
"@react-stately/form" "^3.1.1"
"@react-stately/list" "^3.11.2"
"@react-stately/menu" "^3.9.1"
"@react-stately/numberfield" "^3.9.9"
"@react-stately/overlays" "^3.6.13"
"@react-stately/radio" "^3.10.10"
"@react-stately/searchfield" "^3.5.9"
"@react-stately/select" "^3.6.10"
"@react-stately/selection" "^3.19.0"
"@react-stately/slider" "^3.6.1"
"@react-stately/table" "^3.13.1"
"@react-stately/tabs" "^3.7.1"
"@react-stately/toggle" "^3.8.1"
"@react-stately/tooltip" "^3.5.1"
"@react-stately/tree" "^3.8.7"
"@react-types/shared" "^3.27.0"
react-stately@3.36.1:
version "3.36.1"
resolved "https://registry.yarnpkg.com/react-stately/-/react-stately-3.36.1.tgz#605c18e6aa7a900f19b066699b5b35b7800cb759"
integrity sha512-H9kiGAylNec/iE5qk7qQLV1cvtSAIVq3mgt87zx2EA+f+/sYy2oBtchFPaDiBf/m7xMEKf0Fr9zSLU6G99xQ8g==
dependencies:
"@react-stately/calendar" "^3.7.1"
"@react-stately/checkbox" "^3.6.12"
"@react-stately/collections" "^3.12.2"
"@react-stately/color" "^3.8.3"
"@react-stately/combobox" "^3.10.3"
"@react-stately/data" "^3.12.2"
"@react-stately/datepicker" "^3.13.0"
"@react-stately/disclosure" "^3.0.2"
"@react-stately/dnd" "^3.5.2"
"@react-stately/form" "^3.1.2"
"@react-stately/list" "^3.12.0"
"@react-stately/menu" "^3.9.2"
"@react-stately/numberfield" "^3.9.10"
"@react-stately/overlays" "^3.6.14"
"@react-stately/radio" "^3.10.11"
"@react-stately/searchfield" "^3.5.10"
"@react-stately/select" "^3.6.11"
"@react-stately/selection" "^3.20.0"
"@react-stately/slider" "^3.6.2"
"@react-stately/table" "^3.14.0"
"@react-stately/tabs" "^3.8.0"
"@react-stately/toast" "^3.0.0"
"@react-stately/toggle" "^3.8.2"
"@react-stately/tooltip" "^3.5.2"
"@react-stately/tree" "^3.8.8"
"@react-types/shared" "^3.28.0"
react-stately@^3.34.0:
version "3.36.0"
resolved "https://registry.yarnpkg.com/react-stately/-/react-stately-3.36.0.tgz#1544f0a742145d9bc2d67a8c76af3648a9982fd6"
@@ -13308,37 +13478,6 @@ react-stately@^3.34.0:
"@react-stately/tree" "^3.8.8"
"@react-types/shared" "^3.28.0"
react-stately@^3.35.0:
version "3.35.0"
resolved "https://registry.yarnpkg.com/react-stately/-/react-stately-3.35.0.tgz#92bfc83bb4f7626a57c6aeabe4d08aeaab1fa2f7"
integrity sha512-1BH21J/TOHpyZe7c+f1BU2bnRWaBDTjLH0WdBuzNfPOXu7RBG3ebPIRvqd7UkPaVfIcol2QJnxe8S0a314JWKA==
dependencies:
"@react-stately/calendar" "^3.7.0"
"@react-stately/checkbox" "^3.6.11"
"@react-stately/collections" "^3.12.1"
"@react-stately/color" "^3.8.2"
"@react-stately/combobox" "^3.10.2"
"@react-stately/data" "^3.12.1"
"@react-stately/datepicker" "^3.12.0"
"@react-stately/disclosure" "^3.0.1"
"@react-stately/dnd" "^3.5.1"
"@react-stately/form" "^3.1.1"
"@react-stately/list" "^3.11.2"
"@react-stately/menu" "^3.9.1"
"@react-stately/numberfield" "^3.9.9"
"@react-stately/overlays" "^3.6.13"
"@react-stately/radio" "^3.10.10"
"@react-stately/searchfield" "^3.5.9"
"@react-stately/select" "^3.6.10"
"@react-stately/selection" "^3.19.0"
"@react-stately/slider" "^3.6.1"
"@react-stately/table" "^3.13.1"
"@react-stately/tabs" "^3.7.1"
"@react-stately/toggle" "^3.8.1"
"@react-stately/tooltip" "^3.5.1"
"@react-stately/tree" "^3.8.7"
"@react-types/shared" "^3.27.0"
react-style-singleton@^2.2.2, react-style-singleton@^2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz#4265608be69a4d70cfe3047f2c6c88b2c3ace388"
@@ -13366,6 +13505,14 @@ react-transition-group@^4.3.0:
loose-envify "^1.4.0"
prop-types "^15.6.2"
react-window@^1.8.11:
version "1.8.11"
resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.11.tgz#a857b48fa85bd77042d59cc460964ff2e0648525"
integrity sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ==
dependencies:
"@babel/runtime" "^7.0.0"
memoize-one ">=3.1.1 <6"
react@*, react@19.0.0:
version "19.0.0"
resolved "https://registry.yarnpkg.com/react/-/react-19.0.0.tgz#6e1969251b9f108870aa4bff37a0ce9ddfaaabdd"
@@ -13408,6 +13555,18 @@ redent@^3.0.0:
indent-string "^4.0.0"
strip-indent "^3.0.0"
redux@^4.1.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197"
integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==
dependencies:
"@babel/runtime" "^7.9.2"
redux@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/redux/-/redux-5.0.1.tgz#97fa26881ce5746500125585d5642c77b6e9447b"
integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==
reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9:
version "1.0.10"
resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz#c629219e78a3316d8b604c765ef68996964e7bf9"

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