@abyss-project/commons-front-core 8.1.0 → 8.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -93
- package/catalog/captures-plan.ts +5 -1
- package/catalog/catalog-entries.ts +4 -0
- package/catalog/pages/AccountMenu.tsx +62 -0
- package/catalog/pages/NotificationsMenu.tsx +116 -0
- package/catalog/pages/index.ts +4 -0
- package/catalog/specs/captures.spec.ts +7 -0
- package/catalog/specs/write-capture-index.ts +1 -0
- package/dist/{AbyssCookieConsentProvider-BhSzVu8P.js → AbyssCookieConsentProvider-TE46ywnd.js} +554 -514
- package/dist/{CookieConsentSettings.component-DMEAuCVS.js → CookieConsentSettings.component-BKsYgpMa.js} +1 -1
- package/dist/components/layers/CommandPalette/CommandPalette.d.ts +27 -6
- package/dist/components/layers/CommandPalette/command-registry.d.ts +39 -9
- package/dist/components/layers/Modal/Modal.d.ts +2 -1
- package/dist/components/states/PageState/PageState.d.ts +6 -1
- package/dist/components/structure/AccountMenu/AccountMenu.d.ts +27 -0
- package/dist/components/structure/AccountMenu/AccountMenu.spec.d.ts +1 -0
- package/dist/components/structure/AppShell/AppShell.d.ts +9 -3
- package/dist/components/structure/AppShell/ShellNavigation.d.ts +4 -2
- package/dist/components/structure/AppShell/app-shell.types.d.ts +38 -2
- package/dist/components/structure/NotificationsMenu/NotificationsMenu.d.ts +36 -0
- package/dist/components/structure/NotificationsMenu/NotificationsMenu.spec.d.ts +1 -0
- package/dist/components/structure/PageHeader/PageHeader.d.ts +2 -2
- package/dist/components/structure/index.d.ts +2 -0
- package/dist/components/structure/test-support/structure-test-harness.d.ts +6 -0
- package/dist/cookie-consent.js +2 -2
- package/dist/{design-theme-CvLzOz3p.js → design-theme-CN_6fRX3.js} +2317 -2278
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/use-rich-intl.hook.d.ts +40 -0
- package/dist/hooks/use-scroll-overflow-cue.hook.d.ts +9 -0
- package/dist/hooks/use-scroll-overflow-cue.hook.spec.d.ts +1 -0
- package/dist/{index-GoHiFeaw.js → index-4BMCQ_6u.js} +2 -2
- package/dist/{index-BkfBpYKR.js → index-B-Gq5Zeh.js} +3 -3
- package/dist/{index-BqIg55B3.js → index-BI8z_ZwK.js} +4 -4
- package/dist/{index-DixNft1u.js → index-BR66kEMM.js} +2 -2
- package/dist/{index-D_GWBYM2.js → index-BXs_bBcm.js} +1 -1
- package/dist/{index-6KdfKdSJ.js → index-BZfgg1Dp.js} +2 -2
- package/dist/{index-21vSFz6u.js → index-Bi3OW3pz.js} +2 -2
- package/dist/{index-D4Qh2Iww.js → index-Bu5LDz63.js} +3 -3
- package/dist/{index-ClLoBnCN.js → index-C-bs2P06.js} +2 -2
- package/dist/{index-Btnmn1iK.js → index-C4VmhCn7.js} +4 -4
- package/dist/{index-ASLTmD1Y.js → index-C6ZLgLu-.js} +2 -2
- package/dist/{index-BVS73U9E.js → index-CK9kVK6Y.js} +4 -4
- package/dist/{index-BTjzj7mm.js → index-D490I-Im.js} +2 -2
- package/dist/{index-7HAvRXgu.js → index-DC-vg2P2.js} +2 -2
- package/dist/{index-bR-mnNUA.js → index-DPxFwYmx.js} +2 -2
- package/dist/{index-D5e4uD4k.js → index-Dhz388yW.js} +2 -2
- package/dist/{index-C92C_oG5.js → index-DistZiPf.js} +2 -2
- package/dist/{index-LdiTmDFg.js → index-DjLWvtUt.js} +2 -2
- package/dist/{index-BtsnuEJC.js → index-Dm6584pR.js} +14734 -14782
- package/dist/{index-CHGRiRCe.js → index-FS21eF3a.js} +3 -3
- package/dist/{index-Ni4BmFs4.js → index-csvcp4rP.js} +2 -2
- package/dist/{index-l7FAjaVB.js → index-ltq6POQa.js} +3 -3
- package/dist/index.js +5579 -4478
- package/dist/legacy/theme/core-breakpoints-under-legacy-theme.spec.d.ts +1 -0
- package/dist/legacy.js +18 -18
- package/dist/theme/DesignCssVariables.d.ts +11 -0
- package/dist/theme/DesignCssVariables.spec.d.ts +1 -0
- package/dist/theme/components/shared.d.ts +4 -1
- package/dist/theme/css-variables.d.ts +7 -0
- package/dist/theme/design-media.d.ts +26 -0
- package/dist/theme/design-media.spec.d.ts +1 -0
- package/dist/theme/focus-ring.spec.d.ts +1 -0
- package/dist/theme/index.d.ts +4 -0
- package/dist/theme/use-design-breakpoint.d.ts +7 -0
- package/dist/translations/en.d.ts +20 -0
- package/dist/translations/fr.d.ts +20 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,93 +1,125 @@
|
|
|
1
|
-
# Abyss Commons Front Core
|
|
2
|
-
|
|
3
|
-
`@abyss-project/commons-front-core`
|
|
4
|
-
|
|
5
|
-
Composants React, hooks, design system et traductions partagés par les frontends Abyss.
|
|
6
|
-
|
|
7
|
-
_Shared React components, hooks, design system and translations for Abyss frontends._
|
|
8
|
-
|
|
9
|
-
## Installation
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
npm install @abyss-project/commons-front-core
|
|
13
|
-
# or
|
|
14
|
-
pnpm add @abyss-project/commons-front-core
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
## Liens
|
|
18
|
-
|
|
19
|
-
- **Documentation** : [https://docs.abyss-project.fr/abyss-project](https://docs.abyss-project.fr/abyss-project)
|
|
20
|
-
- **NPM** : [@abyss-project/commons-front-core](https://www.npmjs.com/package/@abyss-project/commons-front-core)
|
|
21
|
-
- **Application Abyss** : [app.abyss-project.fr](https://app.abyss-project.fr)
|
|
22
|
-
- **Statut des services** : [status.abyss-project.fr](https://status.abyss-project.fr)
|
|
23
|
-
|
|
24
|
-
## Usage
|
|
25
|
-
|
|
26
|
-
Voir la [documentation produit](https://docs.abyss-project.fr/abyss-project) pour les guides d'utilisation, la configuration du Core, les exemples d'appel et la référence des permissions.
|
|
27
|
-
|
|
28
|
-
## RUM
|
|
29
|
-
|
|
30
|
-
Le RUM navigateur d'un front (SDK `@abyss-project/monitor-browser`, peer `^1.2.0`) se démarre par `AbyssRumProvider`, monté **sous** `AbyssCookieConsentProvider` et, quand le front en a un, sous `AuthProvider` :
|
|
31
|
-
|
|
32
|
-
```tsx
|
|
33
|
-
<AbyssErrorBoundary appName="abyss-cloud">
|
|
34
|
-
<AuthProvider client={authClient}>
|
|
35
|
-
<AbyssCookieConsentProvider consentVersion={COOKIE_CONSENT_VERSION} isAuthenticated={isAuthenticated}>
|
|
36
|
-
<AbyssRumProvider config={buildRumOptions()}>
|
|
37
|
-
<App />
|
|
38
|
-
</AbyssRumProvider>
|
|
39
|
-
</AbyssCookieConsentProvider>
|
|
40
|
-
</AuthProvider>
|
|
41
|
-
</AbyssErrorBoundary>
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
- **`config` est requise** (raison de la majeure 8.0.0 : aucun front ne peut oublier son RUM). `config={null}` désactive le RUM explicitement : rien n'est installé. Elle reçoit les options de `AbyssRum.init` (`apiUrl`, `proxyUrl`, `applicationId`, `token`, `release`, `instrumentedOrigins`, `routeResolver`…), sauf `consent`, que le provider fournit.
|
|
45
|
-
- **Une seule initialisation.** `AbyssRum.init` est appelé au premier commit avec une `config` non nulle, une fois, StrictMode compris. Une `config` qui change ensuite est **ignorée** : le SDK vit autant que la page.
|
|
46
|
-
- **Consentement.** Le replay exige la catégorie analytics de `AbyssCookieConsentProvider`, relue à chaque décision du SDK : un consentement donné ou retiré après le démarrage est suivi. Sans provider de consentement, analytics vaut refusé.
|
|
47
|
-
- **Acteur.** Sous `AuthProvider`, l'acteur de la session est l'id de l'utilisateur connecté, `null` à la déconnexion, inchangé pendant le chargement de la session. Sans `AuthProvider`, aucun acteur.
|
|
48
|
-
- **`feedbackTicket`** (`intakeSlug`, `internalTicketSectionId`, `appName`, `sessionUrl(sessionId, offsetMs)`, `locale?()`) : chaque retour utilisateur enregistré par Monitor devient un ticket Internal Tickets, par l'intake public d'abyss-console (`fileInternalTicketIntake`, peer `@abyss-project/console` `^12.4.0`, configuré par `AbyssConsoleCore.setConfig` du front). Titre `[<appName>] Retour utilisateur` ; description : le message, l'e-mail, la route et le lien vers la session. Le déclarant est l'e-mail saisi, sinon celui de l'utilisateur connecté ; un retour anonyme sans e-mail ne crée aucun ticket et reste dans Monitor. Un échec de création est journalisé (`console.warn`) et avalé. `locale()` est relue à chaque retour : sa sous-étiquette primaire (`en-GB` → `en`) part comme `locale` de l'intake, la langue des courriels de suivi ; une langue que l'intake ne parle pas (hors `UserLanguage`) n'est pas envoyée.
|
|
49
|
-
- **`onFeedbackSent`** : rappel du front, appelé pour chaque retour enregistré, après le ticket (créé, sauté ou en échec).
|
|
50
|
-
- **Erreurs de rendu.** `AbyssErrorBoundary` envoie l'erreur qu'il attrape à `AbyssRum.captureError(error, { source: 'error-boundary' })` ; sans RUM démarré, l'appel ne fait rien.
|
|
51
|
-
|
|
52
|
-
### Construire les options : `buildAbyssRumProviderConfig`
|
|
53
|
-
|
|
54
|
-
Les fronts Abyss ne recopient pas leurs options : ils passent leurs valeurs de config brutes (chaîne vide = absente) au constructeur partagé, qui rend `AbyssRumProviderConfig | null`.
|
|
55
|
-
|
|
56
|
-
```ts
|
|
57
|
-
import { buildAbyssRumProviderConfig } from '@abyss-project/commons-front-core';
|
|
58
|
-
|
|
59
|
-
export const buildRumOptions = () =>
|
|
60
|
-
buildAbyssRumProviderConfig({
|
|
61
|
-
token: config.app.abyssMonitor.rumToken,
|
|
62
|
-
applicationId: config.app.abyssMonitor.applicationId,
|
|
63
|
-
monitorApiUrl: config.app.abyssMonitor.api,
|
|
64
|
-
apiUrl: config.app.abyssCloud.api, // l'API du front : relais `/_t` et origine instrumentée
|
|
65
|
-
mainApiUrl: config.app.abyss.api, // l'API d'abyss-main : origine instrumentée
|
|
66
|
-
release: config.app.abyssMonitor.release,
|
|
67
|
-
routeTemplates: CLOUD_ROUTE_TEMPLATES, // gabarits complets du routeur
|
|
68
|
-
appName: 'AbyssCloud',
|
|
69
|
-
mainFrontUrl: config.app.abyss.site, // la page de session vit dans le front d'abyss-main
|
|
70
|
-
feedbackIntakeSlug: config.app.abyssMonitor.feedbackIntakeSlug,
|
|
71
|
-
feedbackSectionId: config.app.abyssMonitor.feedbackSectionId,
|
|
72
|
-
organizationId: config.app.abyssMonitor.organizationId,
|
|
73
|
-
projectId: config.app.abyssMonitor.projectId,
|
|
74
|
-
locale: () => currentLanguage(),
|
|
75
|
-
});
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
- **`null`** sans jeton, application, API Monitor, API du front ou API d'abyss-main (ou si l'une des trois URL n'en est pas une) : rien n'est installé.
|
|
79
|
-
- **Relais et corrélation** : `proxyUrl` = origine de `apiUrl` + `/_t` ; `instrumentedOrigins` = origines de `apiUrl` et `mainApiUrl` (une seule fois si c'est la même), plus celle de Monitor avec `instrumentMonitorApi: true` (front d'abyss-main, qui appelle Monitor lui-même).
|
|
80
|
-
- **Routes** : `routeResolver` = `createRouteResolver(routeTemplates)`.
|
|
81
|
-
- **Ticket de retour** posé seulement quand l'intake, sa section, l'organisation, le projet et `mainFrontUrl` sont là ; son lien est `abyssMainRumSessionPath`, à l'instant du retour en secondes entières.
|
|
82
|
-
|
|
83
|
-
### Gabarits de route : `createRouteResolver(patterns)`
|
|
84
|
-
|
|
85
|
-
`createRouteResolver(patterns: readonly string[]): (location: Location) => string` donne au SDK le gabarit de la route (`/org/:orgId/boards/:boardId`), que le SDK masque ensuite comme toute URL. Les gabarits sont **complets** (routes imbriquées comprises) et classés comme react-router les classe (`matchRoutes`) : un segment statique l'emporte sur un paramètre, un paramètre sur un `*`, quel que soit l'ordre de la liste. Sans correspondance : `location.pathname`.
|
|
86
|
-
|
|
87
|
-
### Page d'une session : `abyssMainRumSessionPath`
|
|
88
|
-
|
|
89
|
-
`ABYSS_MAIN_RUM_SESSION_ROUTE` (`/org/:orgId/project/:projectId/application/:applicationId/monitor/rum/sessions/:sessionId`) est la seule source du chemin de la page d'une session dans le front d'abyss-main, qui le monte. `abyssMainRumSessionPath({ orgId, projectId, applicationId }, sessionId, t?)` le remplit (id de session encodé) et ajoute `?t=` en secondes entières, jamais négatif.
|
|
90
|
-
|
|
91
|
-
##
|
|
92
|
-
|
|
93
|
-
|
|
1
|
+
# Abyss Commons Front Core
|
|
2
|
+
|
|
3
|
+
`@abyss-project/commons-front-core`
|
|
4
|
+
|
|
5
|
+
Composants React, hooks, design system et traductions partagés par les frontends Abyss.
|
|
6
|
+
|
|
7
|
+
_Shared React components, hooks, design system and translations for Abyss frontends._
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @abyss-project/commons-front-core
|
|
13
|
+
# or
|
|
14
|
+
pnpm add @abyss-project/commons-front-core
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Liens
|
|
18
|
+
|
|
19
|
+
- **Documentation** : [https://docs.abyss-project.fr/abyss-project](https://docs.abyss-project.fr/abyss-project)
|
|
20
|
+
- **NPM** : [@abyss-project/commons-front-core](https://www.npmjs.com/package/@abyss-project/commons-front-core)
|
|
21
|
+
- **Application Abyss** : [app.abyss-project.fr](https://app.abyss-project.fr)
|
|
22
|
+
- **Statut des services** : [status.abyss-project.fr](https://status.abyss-project.fr)
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
Voir la [documentation produit](https://docs.abyss-project.fr/abyss-project) pour les guides d'utilisation, la configuration du Core, les exemples d'appel et la référence des permissions.
|
|
27
|
+
|
|
28
|
+
## RUM
|
|
29
|
+
|
|
30
|
+
Le RUM navigateur d'un front (SDK `@abyss-project/monitor-browser`, peer `^1.2.0`) se démarre par `AbyssRumProvider`, monté **sous** `AbyssCookieConsentProvider` et, quand le front en a un, sous `AuthProvider` :
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
<AbyssErrorBoundary appName="abyss-cloud">
|
|
34
|
+
<AuthProvider client={authClient}>
|
|
35
|
+
<AbyssCookieConsentProvider consentVersion={COOKIE_CONSENT_VERSION} isAuthenticated={isAuthenticated}>
|
|
36
|
+
<AbyssRumProvider config={buildRumOptions()}>
|
|
37
|
+
<App />
|
|
38
|
+
</AbyssRumProvider>
|
|
39
|
+
</AbyssCookieConsentProvider>
|
|
40
|
+
</AuthProvider>
|
|
41
|
+
</AbyssErrorBoundary>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- **`config` est requise** (raison de la majeure 8.0.0 : aucun front ne peut oublier son RUM). `config={null}` désactive le RUM explicitement : rien n'est installé. Elle reçoit les options de `AbyssRum.init` (`apiUrl`, `proxyUrl`, `applicationId`, `token`, `release`, `instrumentedOrigins`, `routeResolver`…), sauf `consent`, que le provider fournit.
|
|
45
|
+
- **Une seule initialisation.** `AbyssRum.init` est appelé au premier commit avec une `config` non nulle, une fois, StrictMode compris. Une `config` qui change ensuite est **ignorée** : le SDK vit autant que la page.
|
|
46
|
+
- **Consentement.** Le replay exige la catégorie analytics de `AbyssCookieConsentProvider`, relue à chaque décision du SDK : un consentement donné ou retiré après le démarrage est suivi. Sans provider de consentement, analytics vaut refusé.
|
|
47
|
+
- **Acteur.** Sous `AuthProvider`, l'acteur de la session est l'id de l'utilisateur connecté, `null` à la déconnexion, inchangé pendant le chargement de la session. Sans `AuthProvider`, aucun acteur.
|
|
48
|
+
- **`feedbackTicket`** (`intakeSlug`, `internalTicketSectionId`, `appName`, `sessionUrl(sessionId, offsetMs)`, `locale?()`) : chaque retour utilisateur enregistré par Monitor devient un ticket Internal Tickets, par l'intake public d'abyss-console (`fileInternalTicketIntake`, peer `@abyss-project/console` `^12.4.0`, configuré par `AbyssConsoleCore.setConfig` du front). Titre `[<appName>] Retour utilisateur` ; description : le message, l'e-mail, la route et le lien vers la session. Le déclarant est l'e-mail saisi, sinon celui de l'utilisateur connecté ; un retour anonyme sans e-mail ne crée aucun ticket et reste dans Monitor. Un échec de création est journalisé (`console.warn`) et avalé. `locale()` est relue à chaque retour : sa sous-étiquette primaire (`en-GB` → `en`) part comme `locale` de l'intake, la langue des courriels de suivi ; une langue que l'intake ne parle pas (hors `UserLanguage`) n'est pas envoyée.
|
|
49
|
+
- **`onFeedbackSent`** : rappel du front, appelé pour chaque retour enregistré, après le ticket (créé, sauté ou en échec).
|
|
50
|
+
- **Erreurs de rendu.** `AbyssErrorBoundary` envoie l'erreur qu'il attrape à `AbyssRum.captureError(error, { source: 'error-boundary' })` ; sans RUM démarré, l'appel ne fait rien.
|
|
51
|
+
|
|
52
|
+
### Construire les options : `buildAbyssRumProviderConfig`
|
|
53
|
+
|
|
54
|
+
Les fronts Abyss ne recopient pas leurs options : ils passent leurs valeurs de config brutes (chaîne vide = absente) au constructeur partagé, qui rend `AbyssRumProviderConfig | null`.
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { buildAbyssRumProviderConfig } from '@abyss-project/commons-front-core';
|
|
58
|
+
|
|
59
|
+
export const buildRumOptions = () =>
|
|
60
|
+
buildAbyssRumProviderConfig({
|
|
61
|
+
token: config.app.abyssMonitor.rumToken,
|
|
62
|
+
applicationId: config.app.abyssMonitor.applicationId,
|
|
63
|
+
monitorApiUrl: config.app.abyssMonitor.api,
|
|
64
|
+
apiUrl: config.app.abyssCloud.api, // l'API du front : relais `/_t` et origine instrumentée
|
|
65
|
+
mainApiUrl: config.app.abyss.api, // l'API d'abyss-main : origine instrumentée
|
|
66
|
+
release: config.app.abyssMonitor.release,
|
|
67
|
+
routeTemplates: CLOUD_ROUTE_TEMPLATES, // gabarits complets du routeur
|
|
68
|
+
appName: 'AbyssCloud',
|
|
69
|
+
mainFrontUrl: config.app.abyss.site, // la page de session vit dans le front d'abyss-main
|
|
70
|
+
feedbackIntakeSlug: config.app.abyssMonitor.feedbackIntakeSlug,
|
|
71
|
+
feedbackSectionId: config.app.abyssMonitor.feedbackSectionId,
|
|
72
|
+
organizationId: config.app.abyssMonitor.organizationId,
|
|
73
|
+
projectId: config.app.abyssMonitor.projectId,
|
|
74
|
+
locale: () => currentLanguage(),
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
- **`null`** sans jeton, application, API Monitor, API du front ou API d'abyss-main (ou si l'une des trois URL n'en est pas une) : rien n'est installé.
|
|
79
|
+
- **Relais et corrélation** : `proxyUrl` = origine de `apiUrl` + `/_t` ; `instrumentedOrigins` = origines de `apiUrl` et `mainApiUrl` (une seule fois si c'est la même), plus celle de Monitor avec `instrumentMonitorApi: true` (front d'abyss-main, qui appelle Monitor lui-même).
|
|
80
|
+
- **Routes** : `routeResolver` = `createRouteResolver(routeTemplates)`.
|
|
81
|
+
- **Ticket de retour** posé seulement quand l'intake, sa section, l'organisation, le projet et `mainFrontUrl` sont là ; son lien est `abyssMainRumSessionPath`, à l'instant du retour en secondes entières.
|
|
82
|
+
|
|
83
|
+
### Gabarits de route : `createRouteResolver(patterns)`
|
|
84
|
+
|
|
85
|
+
`createRouteResolver(patterns: readonly string[]): (location: Location) => string` donne au SDK le gabarit de la route (`/org/:orgId/boards/:boardId`), que le SDK masque ensuite comme toute URL. Les gabarits sont **complets** (routes imbriquées comprises) et classés comme react-router les classe (`matchRoutes`) : un segment statique l'emporte sur un paramètre, un paramètre sur un `*`, quel que soit l'ordre de la liste. Sans correspondance : `location.pathname`.
|
|
86
|
+
|
|
87
|
+
### Page d'une session : `abyssMainRumSessionPath`
|
|
88
|
+
|
|
89
|
+
`ABYSS_MAIN_RUM_SESSION_ROUTE` (`/org/:orgId/project/:projectId/application/:applicationId/monitor/rum/sessions/:sessionId`) est la seule source du chemin de la page d'une session dans le front d'abyss-main, qui le monte. `abyssMainRumSessionPath({ orgId, projectId, applicationId }, sessionId, t?)` le remplit (id de session encodé) et ajoute `?t=` en secondes entières, jamais négatif.
|
|
90
|
+
|
|
91
|
+
## Changements visibles
|
|
92
|
+
|
|
93
|
+
### 8.2.0
|
|
94
|
+
|
|
95
|
+
Shell et tableaux de bord du front principal sur le design system (carte #286, zone 1).
|
|
96
|
+
|
|
97
|
+
- **Nouveau** : `AppShell` prend `chromeless` (plein écran sans cadre), les entrées `external` et `action`,
|
|
98
|
+
l'action de section (`NavSection.action`) et les entrées internes du pied ; `NotificationsMenu`, `AccountMenu`,
|
|
99
|
+
`DesignCssVariables` et `focusRing` sont exportés ; `CommandPalette` gagne icônes, sous-titres, récents, sources
|
|
100
|
+
asynchrones et ouverture filtrée.
|
|
101
|
+
- **Paliers** : les composants du design system basculent à 768 et 1200 px (`designMedia`, `useDesignBreakpoint`)
|
|
102
|
+
quel que soit le provider. Sous le thème legacy, l'`AppShell` montre le rail dès 768 px (et non plus à 900), la
|
|
103
|
+
`Modal` une fenêtre dès 768 px, et la bannière MFA passe son bouton sous le texte quand il ne tient plus à côté.
|
|
104
|
+
- **Largeur de `Drawer` et de `Toast` sous le thème legacy** : la largeur par défaut passe de 600 à 480 px (palier
|
|
105
|
+
`sm` des jetons, et non plus le `sm` de MUI). Un hôte qui comptait sur 600 px passe `width` explicitement au
|
|
106
|
+
`Drawer`.
|
|
107
|
+
- **Largeur de `Modal` et de `CommandPalette` sous le thème legacy** : elle reste tirée des `theme.breakpoints` de
|
|
108
|
+
MUI (prop `size` de la `Modal` passée à `maxWidth`, `sm` pour la palette). Sous le thème legacy, `md` vaut 900 px
|
|
109
|
+
et `sm` 600 px, contre 768 et 480 sous `AbyssThemeProvider` : la même `Modal` est donc plus large chez un hôte
|
|
110
|
+
legacy. Suivi : `DS-LEGACY-WIDTHS` dans `docs/internal/TECH-DEBT.md` d'abyss-main.
|
|
111
|
+
- **Sélecteur d'espace** : un espace qui porte le nom de son type (« Espace personnel ») n'a plus de seconde ligne
|
|
112
|
+
répétant ce type.
|
|
113
|
+
- **Navigation** : la liste des sections garde une barre de défilement fine et visible quand elle dépasse la
|
|
114
|
+
hauteur de la fenêtre.
|
|
115
|
+
- **`PageState`** : `forbidden.message` remplace la description générique de l’accès refusé, pour nommer à qui
|
|
116
|
+
demander l’accès (organisation, projet).
|
|
117
|
+
- **`PageState`** : le bloc d’état passe son action sous le texte dès que son emplacement fait moins de 512 px
|
|
118
|
+
(colonne latérale), et plus seulement sous 768 px d’écran.
|
|
119
|
+
- **`PageHeader`** : de 768 px à la largeur du bureau, les actions passent sous le titre dès qu'elles ne tiennent
|
|
120
|
+
plus à côté d'un titre de 256 px.
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
## Support
|
|
124
|
+
|
|
125
|
+
Pour toute demande, ouvrir un ticket depuis [l'application Abyss](https://app.abyss-project.fr/dashboard/account/tickets/create).
|
package/catalog/captures-plan.ts
CHANGED
|
@@ -33,7 +33,8 @@ export type CaptureState =
|
|
|
33
33
|
| 'arrival'
|
|
34
34
|
| 'sheet-open'
|
|
35
35
|
| 'list-open'
|
|
36
|
-
| 'calendar-open'
|
|
36
|
+
| 'calendar-open'
|
|
37
|
+
| 'panel-open';
|
|
37
38
|
|
|
38
39
|
/** File name suffix of each state. */
|
|
39
40
|
const STATE_SUFFIX: Record<CaptureState, string> = {
|
|
@@ -41,6 +42,7 @@ const STATE_SUFFIX: Record<CaptureState, string> = {
|
|
|
41
42
|
'sheet-open': '-feuille',
|
|
42
43
|
'list-open': '-liste',
|
|
43
44
|
'calendar-open': '-calendrier',
|
|
45
|
+
'panel-open': '-ouvert',
|
|
44
46
|
};
|
|
45
47
|
|
|
46
48
|
/** The states shown beyond arrival: what a closed control hides (options, calendar, sheet). */
|
|
@@ -48,6 +50,8 @@ const EXTRA_STATES: Record<string, CaptureState> = {
|
|
|
48
50
|
SceneMobile: 'sheet-open',
|
|
49
51
|
RichSelect: 'list-open',
|
|
50
52
|
DateField: 'calendar-open',
|
|
53
|
+
NotificationsMenu: 'panel-open',
|
|
54
|
+
AccountMenu: 'panel-open',
|
|
51
55
|
};
|
|
52
56
|
|
|
53
57
|
export interface Capture {
|
|
@@ -51,6 +51,10 @@ export const CATALOG_ENTRIES: CatalogEntry[] = [
|
|
|
51
51
|
entry('Toolbar', 'structure', 'toolbar', RESPONSIVE),
|
|
52
52
|
entry('FilterBar', 'structure', 'filter-bar', RESPONSIVE),
|
|
53
53
|
entry('SplitView', 'structure', 'split-view', RESPONSIVE),
|
|
54
|
+
entry('NotificationsMenu', 'structure', 'notifications-menu', {
|
|
55
|
+
mobileCapture: true,
|
|
56
|
+
}),
|
|
57
|
+
entry('AccountMenu', 'structure', 'account-menu'),
|
|
54
58
|
entry('MobileShell', 'mobile', 'mobile-shell', RESPONSIVE),
|
|
55
59
|
entry('BottomSheet', 'mobile', 'bottom-sheet', RESPONSIVE),
|
|
56
60
|
entry('ActionBar', 'mobile', 'action-bar', RESPONSIVE),
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import PersonOutline from '@mui/icons-material/PersonOutline';
|
|
3
|
+
import SettingsOutlined from '@mui/icons-material/SettingsOutlined';
|
|
4
|
+
import CreditCardOutlined from '@mui/icons-material/CreditCardOutlined';
|
|
5
|
+
import ForumOutlined from '@mui/icons-material/ForumOutlined';
|
|
6
|
+
import { AccountMenu } from '../../src/components/structure/AccountMenu/AccountMenu';
|
|
7
|
+
import { ThemeMode } from '../../src/theme/tokens/primitives';
|
|
8
|
+
import { Example, LastEvent, Row, Showcase, useEventLog } from '../frame';
|
|
9
|
+
|
|
10
|
+
export const AccountMenuPage = () => {
|
|
11
|
+
const { log } = useEventLog();
|
|
12
|
+
const [mode, setMode] = useState(ThemeMode.Dark);
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<Showcase
|
|
16
|
+
family="Structure"
|
|
17
|
+
name="AccountMenu"
|
|
18
|
+
description="Avatar de la barre du haut : identité, entrées de l'hôte, thème sombre, déconnexion."
|
|
19
|
+
>
|
|
20
|
+
<Example title="Barre du haut">
|
|
21
|
+
<Row>
|
|
22
|
+
<AccountMenu
|
|
23
|
+
user={{ name: 'Mehdi Abyss', email: 'mehdi@abyss.example' }}
|
|
24
|
+
themeMode={mode}
|
|
25
|
+
onThemeModeChange={(next) => {
|
|
26
|
+
setMode(next);
|
|
27
|
+
log(`thème ${next}`);
|
|
28
|
+
}}
|
|
29
|
+
items={[
|
|
30
|
+
{
|
|
31
|
+
id: 'account',
|
|
32
|
+
label: 'Mon compte',
|
|
33
|
+
icon: <PersonOutline />,
|
|
34
|
+
href: '/compte',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'settings',
|
|
38
|
+
label: 'Paramètres',
|
|
39
|
+
icon: <SettingsOutlined />,
|
|
40
|
+
href: '/compte/parametres',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'billing',
|
|
44
|
+
label: 'Facturation',
|
|
45
|
+
icon: <CreditCardOutlined />,
|
|
46
|
+
href: '/compte/facturation',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'discord',
|
|
50
|
+
label: 'Discord',
|
|
51
|
+
icon: <ForumOutlined />,
|
|
52
|
+
onClick: () => log('discord'),
|
|
53
|
+
},
|
|
54
|
+
]}
|
|
55
|
+
onLogout={() => log('déconnexion')}
|
|
56
|
+
/>
|
|
57
|
+
</Row>
|
|
58
|
+
</Example>
|
|
59
|
+
<LastEvent />
|
|
60
|
+
</Showcase>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import Button from '@mui/material/Button';
|
|
3
|
+
import GroupAddOutlined from '@mui/icons-material/GroupAddOutlined';
|
|
4
|
+
import ChatBubbleOutlineOutlined from '@mui/icons-material/ChatBubbleOutlineOutlined';
|
|
5
|
+
import FileDownloadDoneOutlined from '@mui/icons-material/FileDownloadDoneOutlined';
|
|
6
|
+
import { NotificationsMenu } from '../../src/components/structure/NotificationsMenu/NotificationsMenu';
|
|
7
|
+
import type { NotificationItem } from '../../src/components/structure/NotificationsMenu/NotificationsMenu';
|
|
8
|
+
import { Example, LastEvent, Row, Showcase, useEventLog } from '../frame';
|
|
9
|
+
|
|
10
|
+
const MINUTE_MS = 60 * 1000;
|
|
11
|
+
|
|
12
|
+
const initialItems = (): NotificationItem[] => [
|
|
13
|
+
{
|
|
14
|
+
id: 'invite',
|
|
15
|
+
title: 'Ada Lovelace vous invite dans Atelier Nord',
|
|
16
|
+
body: 'Rejoignez l’organisation pour voir ses projets et ses tableaux.',
|
|
17
|
+
createdAt: new Date(Date.now() - 4 * MINUTE_MS),
|
|
18
|
+
read: false,
|
|
19
|
+
icon: <GroupAddOutlined />,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: 'comment',
|
|
23
|
+
title: 'Nouveau commentaire sur « Roadmap T4 »',
|
|
24
|
+
body: 'Grace : « On décale la refonte du tableau de bord à novembre ? »',
|
|
25
|
+
createdAt: new Date(Date.now() - 70 * MINUTE_MS),
|
|
26
|
+
read: false,
|
|
27
|
+
icon: <ChatBubbleOutlineOutlined />,
|
|
28
|
+
href: '/tableaux/roadmap',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'export',
|
|
32
|
+
title: 'Votre export de relevés est prêt',
|
|
33
|
+
createdAt: new Date(Date.now() - 2 * 24 * 60 * MINUTE_MS),
|
|
34
|
+
read: true,
|
|
35
|
+
icon: <FileDownloadDoneOutlined />,
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
export const NotificationsMenuPage = () => {
|
|
40
|
+
const { log } = useEventLog();
|
|
41
|
+
const [items, setItems] = useState(initialItems);
|
|
42
|
+
const [sound, setSound] = useState(true);
|
|
43
|
+
const unread = items.filter((item) => !item.read).length;
|
|
44
|
+
const markRead = (id: string) =>
|
|
45
|
+
setItems((current) =>
|
|
46
|
+
current.map((item) => (item.id === id ? { ...item, read: true } : item)),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Showcase
|
|
51
|
+
family="Structure"
|
|
52
|
+
name="NotificationsMenu"
|
|
53
|
+
description="Cloche de la barre du haut : pastille des non lues, panneau (feuille sous 768 px), tout marquer lu, son, préférences, actions par notification."
|
|
54
|
+
>
|
|
55
|
+
<Example title="Barre du haut">
|
|
56
|
+
<Row>
|
|
57
|
+
<NotificationsMenu
|
|
58
|
+
items={items}
|
|
59
|
+
unreadCount={unread}
|
|
60
|
+
loading={false}
|
|
61
|
+
onOpen={() => log('ouvrir')}
|
|
62
|
+
onItemClick={(id) => {
|
|
63
|
+
markRead(id);
|
|
64
|
+
log(`lire ${id}`);
|
|
65
|
+
}}
|
|
66
|
+
onMarkAllRead={() => {
|
|
67
|
+
setItems((current) =>
|
|
68
|
+
current.map((item) => ({ ...item, read: true })),
|
|
69
|
+
);
|
|
70
|
+
log('tout lire');
|
|
71
|
+
}}
|
|
72
|
+
soundEnabled={sound}
|
|
73
|
+
onToggleSound={() => {
|
|
74
|
+
setSound((value) => !value);
|
|
75
|
+
log('son');
|
|
76
|
+
}}
|
|
77
|
+
preferencesHref="/compte/notifications"
|
|
78
|
+
renderItemActions={(item) =>
|
|
79
|
+
item.id === 'invite' && !item.read ? (
|
|
80
|
+
<Button
|
|
81
|
+
size="small"
|
|
82
|
+
variant="outlined"
|
|
83
|
+
onClick={() => {
|
|
84
|
+
markRead(item.id);
|
|
85
|
+
log('accepter');
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
Accepter
|
|
89
|
+
</Button>
|
|
90
|
+
) : null
|
|
91
|
+
}
|
|
92
|
+
/>
|
|
93
|
+
</Row>
|
|
94
|
+
</Example>
|
|
95
|
+
<Example title="Liste vide et premier chargement">
|
|
96
|
+
<Row>
|
|
97
|
+
<NotificationsMenu
|
|
98
|
+
items={[]}
|
|
99
|
+
unreadCount={0}
|
|
100
|
+
loading={false}
|
|
101
|
+
onItemClick={(id) => log(`lire ${id}`)}
|
|
102
|
+
onMarkAllRead={() => log('tout lire')}
|
|
103
|
+
/>
|
|
104
|
+
<NotificationsMenu
|
|
105
|
+
items={[]}
|
|
106
|
+
unreadCount={0}
|
|
107
|
+
loading
|
|
108
|
+
onItemClick={(id) => log(`lire ${id}`)}
|
|
109
|
+
onMarkAllRead={() => log('tout lire')}
|
|
110
|
+
/>
|
|
111
|
+
</Row>
|
|
112
|
+
</Example>
|
|
113
|
+
<LastEvent />
|
|
114
|
+
</Showcase>
|
|
115
|
+
);
|
|
116
|
+
};
|
package/catalog/pages/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ComponentType } from 'react';
|
|
2
|
+
import { AccountMenuPage } from './AccountMenu';
|
|
2
3
|
import { ActionBarPage } from './ActionBar';
|
|
3
4
|
import { AppShellPage } from './AppShell';
|
|
4
5
|
import { BottomSheetPage } from './BottomSheet';
|
|
@@ -17,6 +18,7 @@ import { MenuPage } from './Menu';
|
|
|
17
18
|
import { MethodBadgePage } from './MethodBadge';
|
|
18
19
|
import { MobileShellPage } from './MobileShell';
|
|
19
20
|
import { ModalPage } from './Modal';
|
|
21
|
+
import { NotificationsMenuPage } from './NotificationsMenu';
|
|
20
22
|
import { PageHeaderPage } from './PageHeader';
|
|
21
23
|
import { PageStatePage } from './PageState';
|
|
22
24
|
import { RichSelectPage } from './RichSelect';
|
|
@@ -39,6 +41,7 @@ import { ToolbarPage } from './Toolbar';
|
|
|
39
41
|
|
|
40
42
|
/** Every catalog page, by the `component` URL parameter (`catalog-entries.ts`). */
|
|
41
43
|
export const CATALOG_PAGES: Record<string, ComponentType> = {
|
|
44
|
+
AccountMenu: AccountMenuPage,
|
|
42
45
|
ActionBar: ActionBarPage,
|
|
43
46
|
AppShell: AppShellPage,
|
|
44
47
|
BottomSheet: BottomSheetPage,
|
|
@@ -57,6 +60,7 @@ export const CATALOG_PAGES: Record<string, ComponentType> = {
|
|
|
57
60
|
MethodBadge: MethodBadgePage,
|
|
58
61
|
MobileShell: MobileShellPage,
|
|
59
62
|
Modal: ModalPage,
|
|
63
|
+
NotificationsMenu: NotificationsMenuPage,
|
|
60
64
|
PageHeader: PageHeaderPage,
|
|
61
65
|
PageState: PageStatePage,
|
|
62
66
|
RichSelect: RichSelectPage,
|
|
@@ -32,6 +32,13 @@ const REACH: Record<
|
|
|
32
32
|
.click();
|
|
33
33
|
await expect(page.getByRole('dialog')).toBeVisible();
|
|
34
34
|
},
|
|
35
|
+
// The first top bar trigger of the page: the notifications bell or the account avatar.
|
|
36
|
+
'panel-open': async (page) => {
|
|
37
|
+
await page.locator('[aria-haspopup]').first().click();
|
|
38
|
+
await expect(
|
|
39
|
+
page.getByRole('dialog').or(page.getByRole('menu')),
|
|
40
|
+
).toBeVisible();
|
|
41
|
+
},
|
|
35
42
|
};
|
|
36
43
|
|
|
37
44
|
for (const capture of CAPTURES) {
|
|
@@ -24,6 +24,7 @@ const STATE_LABEL: Record<CaptureState, string> = {
|
|
|
24
24
|
'sheet-open': ', feuille ouverte',
|
|
25
25
|
'list-open': ', liste ouverte',
|
|
26
26
|
'calendar-open': ', calendrier ouvert',
|
|
27
|
+
'panel-open': ', panneau ouvert',
|
|
27
28
|
};
|
|
28
29
|
|
|
29
30
|
const describe = (capture: Capture): string =>
|