@abyss-project/commons-front-core 9.4.1 → 9.4.3

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.
Files changed (68) hide show
  1. package/catalog/captures-plan.ts +5 -1
  2. package/catalog/catalog-entries.ts +7 -1
  3. package/catalog/pages/ExpressionSearch.tsx +65 -0
  4. package/catalog/pages/FacetPanel.tsx +81 -0
  5. package/catalog/pages/PageState.tsx +17 -0
  6. package/catalog/pages/PreviewDrawer.tsx +120 -0
  7. package/catalog/pages/ProductTour.tsx +9 -1
  8. package/catalog/pages/Section.tsx +10 -0
  9. package/catalog/pages/StatTile.tsx +13 -0
  10. package/catalog/pages/ValueMenu.tsx +40 -0
  11. package/catalog/pages/index.ts +8 -0
  12. package/catalog/specs/captures.spec.ts +7 -0
  13. package/catalog/specs/expression-search-scroll.spec.ts +33 -0
  14. package/catalog/specs/preview-drawer-trail.spec.ts +38 -0
  15. package/catalog/specs/write-capture-index.ts +1 -0
  16. package/dist/{AbyssCookieConsentProvider-L42RPb5f.js → AbyssCookieConsentProvider-B0_k-lsA.js} +406 -312
  17. package/dist/{CookieConsentSettings.component-CkhtQuGU.js → CookieConsentSettings.component-DPmXSN1B.js} +1 -1
  18. package/dist/components/data/FacetPanel/FacetPanel.d.ts +30 -0
  19. package/dist/components/data/FacetPanel/FacetPanel.spec.d.ts +1 -0
  20. package/dist/components/data/FacetPanel/facet-terms.d.ts +16 -0
  21. package/dist/components/data/StatTile/StatTile.d.ts +4 -1
  22. package/dist/components/data/index.d.ts +1 -0
  23. package/dist/components/inputs/ExpressionSearch/ExpressionSearch.d.ts +30 -0
  24. package/dist/components/inputs/ExpressionSearch/ExpressionSearch.spec.d.ts +1 -0
  25. package/dist/components/inputs/ExpressionSearch/suggestion-context.d.ts +20 -0
  26. package/dist/components/inputs/index.d.ts +1 -0
  27. package/dist/components/layers/PreviewDrawer/PreviewDrawer.d.ts +18 -0
  28. package/dist/components/layers/PreviewDrawer/PreviewDrawer.spec.d.ts +1 -0
  29. package/dist/components/layers/PreviewDrawer/usePreviewStack.d.ts +32 -0
  30. package/dist/components/layers/ProductTour/ProductTour.d.ts +3 -3
  31. package/dist/components/layers/ProductTour/product-tour-geometry.d.ts +11 -3
  32. package/dist/components/layers/index.d.ts +2 -0
  33. package/dist/components/meaning/ValueMenu/ValueMenu.d.ts +25 -0
  34. package/dist/components/meaning/ValueMenu/ValueMenu.spec.d.ts +1 -0
  35. package/dist/components/meaning/index.d.ts +1 -0
  36. package/dist/components/states/PageState/PageState.d.ts +9 -2
  37. package/dist/components/structure/Section/Section.d.ts +3 -1
  38. package/dist/cookie-consent.js +2 -2
  39. package/dist/{design-theme-C3DcD-kA.js → design-theme-BI47S5Y9.js} +1 -1
  40. package/dist/hooks/use-rich-intl.hook.d.ts +96 -2
  41. package/dist/{index-BowQxHAl.js → index-1OsboYW9.js} +2 -2
  42. package/dist/{index-Bi1ITcfD.js → index-B0QTV2Ec.js} +2 -2
  43. package/dist/{index-C0jOpBPq.js → index-B8mXWPXG.js} +2 -2
  44. package/dist/{index-BAO0d8HL.js → index-BICvHJrd.js} +4 -4
  45. package/dist/{index-CMTzmSRJ.js → index-BIJlYkrW.js} +4 -4
  46. package/dist/{index-DrSNFgIl.js → index-BP9uZ3Ld.js} +2 -2
  47. package/dist/{index-DZ5svQvp.js → index-BXceTJm5.js} +2 -2
  48. package/dist/{index-sI8PQFKF.js → index-BclTcBBA.js} +3 -3
  49. package/dist/{index-BhRgGLuD.js → index-CCHQ4bPG.js} +2 -2
  50. package/dist/{index-DXLyOn3D.js → index-CPsEeNaw.js} +3 -3
  51. package/dist/{index-5hIRSHTc.js → index-CYAUKJ3V.js} +2 -2
  52. package/dist/{index-BLN9Vvdr.js → index-Co3DPyY5.js} +2 -2
  53. package/dist/{index-B-D0LQjw.js → index-CorDlbIR.js} +3 -3
  54. package/dist/{index-gVg-FLmC.js → index-CzA0TJ1Y.js} +1 -1
  55. package/dist/{index-DBKoCefj.js → index-DPRuF7Ck.js} +4 -4
  56. package/dist/{index-Cny_hr56.js → index-DYZzZCrS.js} +2 -2
  57. package/dist/{index-DyhinvD5.js → index-Di-F_MA0.js} +3 -3
  58. package/dist/{index-Dyp_SF7H.js → index-DwuRDQtG.js} +2 -2
  59. package/dist/{index-CuOhAUX-.js → index-KRGYLUiH.js} +28 -28
  60. package/dist/{index-Y46rQdc3.js → index-MCJIM7Di.js} +2 -2
  61. package/dist/{index-yTDpH9la.js → index-PVQUqoxX.js} +2 -2
  62. package/dist/{index-CvT5zdz7.js → index-Q-bMUnXP.js} +2 -2
  63. package/dist/index.js +5428 -4432
  64. package/dist/legacy.js +4 -4
  65. package/dist/translations/en.d.ts +48 -1
  66. package/dist/translations/fr.d.ts +48 -1
  67. package/package.json +5 -5
  68. package/test-results/catalog/.last-run.json +0 -4
@@ -34,7 +34,8 @@ export type CaptureState =
34
34
  | 'sheet-open'
35
35
  | 'list-open'
36
36
  | 'calendar-open'
37
- | 'panel-open';
37
+ | 'panel-open'
38
+ | 'menu-open';
38
39
 
39
40
  /** File name suffix of each state. */
40
41
  const STATE_SUFFIX: Record<CaptureState, string> = {
@@ -43,15 +44,18 @@ const STATE_SUFFIX: Record<CaptureState, string> = {
43
44
  'list-open': '-liste',
44
45
  'calendar-open': '-calendrier',
45
46
  'panel-open': '-ouvert',
47
+ 'menu-open': '-ouvert',
46
48
  };
47
49
 
48
50
  /** The states shown beyond arrival: what a closed control hides (options, calendar, sheet). */
49
51
  const EXTRA_STATES: Record<string, CaptureState> = {
50
52
  SceneMobile: 'sheet-open',
51
53
  RichSelect: 'list-open',
54
+ ExpressionSearch: 'list-open',
52
55
  DateField: 'calendar-open',
53
56
  NotificationsMenu: 'panel-open',
54
57
  AccountMenu: 'panel-open',
58
+ ValueMenu: 'menu-open',
55
59
  };
56
60
 
57
61
  export interface Capture {
@@ -63,7 +63,7 @@ export const CATALOG_ENTRIES: CatalogEntry[] = [
63
63
  entry('BottomSheet', 'mobile', 'bottom-sheet', RESPONSIVE),
64
64
  entry('ActionBar', 'mobile', 'action-bar', RESPONSIVE),
65
65
  entry('SwipeDeck', 'mobile', 'swipe-deck', RESPONSIVE),
66
- entry('PageState', 'states', 'page-state'),
66
+ entry('PageState', 'states', 'page-state', { mobileCapture: true }),
67
67
  entry('Callout', 'states', 'callout', RESPONSIVE),
68
68
  entry('DataTable', 'data', 'data-table', RESPONSIVE),
69
69
  entry('KeyValueList', 'data', 'key-value-list'),
@@ -74,12 +74,14 @@ export const CATALOG_ENTRIES: CatalogEntry[] = [
74
74
  entry('BarChart', 'data', 'bar-chart'),
75
75
  entry('ChartTheme', 'data', 'chart-theme'),
76
76
  entry('UsageMeter', 'data', 'usage-meter'),
77
+ entry('FacetPanel', 'data', 'facet-panel', { mobileCapture: true }),
77
78
  entry('StatusBadge', 'meaning', 'status-badge'),
78
79
  entry('MethodBadge', 'meaning', 'method-badge'),
79
80
  entry('SeverityBadge', 'meaning', 'severity-badge'),
80
81
  entry('ScanBadge', 'meaning', 'scan-badge'),
81
82
  entry('RoleBadge', 'meaning', 'role-badge'),
82
83
  entry('ToolIcon', 'meaning', 'tool-icon'),
84
+ entry('ValueMenu', 'meaning', 'value-menu', { mobileCapture: true }),
83
85
  entry('Avatar', 'meaning', 'avatar'),
84
86
  entry('AvatarGroup', 'meaning', 'avatar-group'),
85
87
  entry('TeamAvatar', 'meaning', 'team-avatar'),
@@ -93,6 +95,7 @@ export const CATALOG_ENTRIES: CatalogEntry[] = [
93
95
  entry('ConfirmDialog', 'layers', 'confirm-dialog'),
94
96
  entry('ConfirmDialogWarning', 'layers', 'confirm-dialog-warning'),
95
97
  entry('Drawer', 'layers', 'drawer'),
98
+ entry('PreviewDrawer', 'layers', 'preview-drawer', { mobileCapture: true }),
96
99
  entry('Menu', 'layers', 'menu'),
97
100
  entry('Toast', 'layers', 'toast'),
98
101
  entry('CommandPalette', 'layers', 'command-palette'),
@@ -112,6 +115,9 @@ export const CATALOG_ENTRIES: CatalogEntry[] = [
112
115
  entry('ContextSelector', 'inputs', 'context-selector', RESPONSIVE),
113
116
  entry('DropZone', 'inputs', 'drop-zone', RESPONSIVE),
114
117
  entry('ImportButton', 'inputs', 'import-button'),
118
+ entry('ExpressionSearch', 'inputs', 'expression-search', {
119
+ mobileCapture: true,
120
+ }),
115
121
  ];
116
122
 
117
123
  export const CATALOG_THEMES = [ThemeMode.Dark, ThemeMode.Light] as const;
@@ -0,0 +1,65 @@
1
+ import { useState } from 'react';
2
+ import { ExpressionSearch } from '../../src/components/inputs/ExpressionSearch/ExpressionSearch';
3
+ import { Example, LastEvent, Showcase, useEventLog } from '../frame';
4
+
5
+ const KEYS = [
6
+ { name: 'level', count: 18420 },
7
+ { name: 'env', count: 18420 },
8
+ { name: 'release', count: 17983 },
9
+ { name: 'status', count: 9214 },
10
+ { name: '@user.plan', count: 4120 },
11
+ ];
12
+
13
+ const VALUES: Record<string, { value: string; count: number }[]> = {
14
+ level: [
15
+ { value: 'ERROR', count: 1204 },
16
+ { value: 'WARN', count: 3310 },
17
+ { value: 'INFO', count: 13906 },
18
+ ],
19
+ env: [
20
+ { value: 'prod', count: 15012 },
21
+ { value: 'staging', count: 3408 },
22
+ ],
23
+ };
24
+
25
+ const fetchKeys = async (prefix: string) =>
26
+ KEYS.filter((key) => key.name.startsWith(prefix));
27
+
28
+ const fetchValues = async (field: string, prefix: string) =>
29
+ (VALUES[field] ?? []).filter((item) => item.value.startsWith(prefix));
30
+
31
+ export const ExpressionSearchPage = () => {
32
+ const { log } = useEventLog();
33
+ const [typed, setTyped] = useState('');
34
+ const [broken, setBroken] = useState('level:ERROR (env:prod OR env:staging');
35
+ return (
36
+ <Showcase
37
+ family="Saisie"
38
+ name="ExpressionSearch"
39
+ description="Champ de filtre en expression (level:ERROR -env:staging) : suggestions de clés puis de valeurs, erreur de syntaxe soulignée à sa position sans perdre la saisie."
40
+ >
41
+ <Example
42
+ title="Saisie avec suggestions de clés puis de valeurs"
43
+ maxWidth={720}
44
+ >
45
+ <ExpressionSearch
46
+ value={typed}
47
+ onChange={setTyped}
48
+ onSubmit={(value) => log(`filtrer : ${value || 'aucun filtre'}`)}
49
+ fetchKeys={fetchKeys}
50
+ fetchValues={fetchValues}
51
+ />
52
+ </Example>
53
+ <Example title="Expression incomplète" maxWidth={720}>
54
+ <ExpressionSearch
55
+ value={broken}
56
+ onChange={setBroken}
57
+ onSubmit={(value) => log(`filtrer : ${value}`)}
58
+ fetchKeys={fetchKeys}
59
+ fetchValues={fetchValues}
60
+ />
61
+ </Example>
62
+ <LastEvent />
63
+ </Showcase>
64
+ );
65
+ };
@@ -0,0 +1,81 @@
1
+ import { useState } from 'react';
2
+ import { FacetPanel } from '../../src/components/data/FacetPanel/FacetPanel';
3
+ import type { Facet } from '../../src/components/data/FacetPanel/FacetPanel';
4
+ import { px } from '../../src/theme/components/shared';
5
+ import { fontWeights } from '../../src/theme/tokens/primitives';
6
+ import { styled } from '@mui/material/styles';
7
+ import { Example, Showcase } from '../frame';
8
+
9
+ const FACETS: Facet[] = [
10
+ {
11
+ field: 'level',
12
+ label: 'Niveau',
13
+ values: [
14
+ { value: 'ERROR', count: 1204 },
15
+ { value: 'WARN', count: 3310 },
16
+ { value: 'INFO', count: 13906 },
17
+ ],
18
+ estimated: false,
19
+ loading: false,
20
+ },
21
+ {
22
+ field: 'env',
23
+ label: 'Environnement',
24
+ values: [
25
+ { value: 'prod', count: 15012 },
26
+ { value: 'staging', count: 3408 },
27
+ ],
28
+ estimated: true,
29
+ loading: false,
30
+ },
31
+ {
32
+ field: '@user.plan',
33
+ label: 'Plan (attribut)',
34
+ values: [],
35
+ estimated: false,
36
+ loading: true,
37
+ },
38
+ {
39
+ field: 'release',
40
+ label: 'Version',
41
+ values: [],
42
+ estimated: false,
43
+ loading: false,
44
+ error: 'Lecture interrompue après 5 s : réduisez la période.',
45
+ onRetry: () => undefined,
46
+ },
47
+ ];
48
+
49
+ const Expression = styled('code')(({ theme }) => ({
50
+ display: 'block',
51
+ fontFamily: theme.design.fonts.mono,
52
+ fontSize: px(theme.design.typeScale.small),
53
+ fontWeight: fontWeights.medium,
54
+ color: theme.design.tokens['text.secondary'],
55
+ overflowWrap: 'anywhere',
56
+ }));
57
+
58
+ export const FacetPanelPage = () => {
59
+ const [expression, setExpression] = useState('level:ERROR -env:staging');
60
+ return (
61
+ <Showcase
62
+ family="Données"
63
+ name="FacetPanel"
64
+ description="Valeurs les plus fréquentes de chaque champ : une coche ajoute la condition à l'expression, « Exclure » ajoute son contraire. L'expression reste la seule source de vérité."
65
+ >
66
+ <Example
67
+ title="Quatre facettes : prête, estimée, en chargement, en échec"
68
+ maxWidth={360}
69
+ >
70
+ <FacetPanel
71
+ facets={FACETS}
72
+ expression={expression}
73
+ onExpressionChange={setExpression}
74
+ />
75
+ </Example>
76
+ <Example title="Expression résultante" maxWidth={720}>
77
+ <Expression>{expression === '' ? '—' : expression}</Expression>
78
+ </Example>
79
+ </Showcase>
80
+ );
81
+ };
@@ -1,3 +1,4 @@
1
+ import { useState } from 'react';
1
2
  import Button from '@mui/material/Button';
2
3
  import Skeleton from '@mui/material/Skeleton';
3
4
  import CreateNewFolderOutlined from '@mui/icons-material/CreateNewFolderOutlined';
@@ -10,6 +11,9 @@ import { Columns, Example, LastEvent, Showcase, useEventLog } from '../frame';
10
11
  import { ACCOUNTS } from '../fixtures';
11
12
  import { AccountCard } from '../parts';
12
13
 
14
+ // Seconds the rate-limit example counts down from, long enough to be read on a capture.
15
+ const RATE_LIMIT_SECONDS = 42;
16
+
13
17
  // Height of an account card: 2 × 64 + 32.
14
18
  const CARD_HEIGHT = space[10] * 2 + space[7];
15
19
 
@@ -23,6 +27,9 @@ const AccountSkeleton = () => (
23
27
 
24
28
  export const PageStatePage = () => {
25
29
  const { log } = useEventLog();
30
+ const [retryAt] = useState(
31
+ () => new Date(Date.now() + RATE_LIMIT_SECONDS * 1000),
32
+ );
26
33
  const ready = (
27
34
  <Columns>
28
35
  {ACCOUNTS.map((account) => (
@@ -82,6 +89,16 @@ export const PageStatePage = () => {
82
89
  {ready}
83
90
  </PageState>
84
91
  </Example>
92
+ <Example title="Trop de requêtes, avec compte à rebours">
93
+ <PageState
94
+ status={PageStatus.RateLimited}
95
+ skeleton={<AccountSkeleton />}
96
+ retryAt={retryAt}
97
+ error={{ onRetry: () => log('réessayer après la limite') }}
98
+ >
99
+ {ready}
100
+ </PageState>
101
+ </Example>
85
102
  <Example title="Introuvable">
86
103
  <PageState status={PageStatus.NotFound} skeleton={<AccountSkeleton />}>
87
104
  {ready}
@@ -0,0 +1,120 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import Button from '@mui/material/Button';
3
+ import { PreviewDrawer } from '../../src/components/layers/PreviewDrawer/PreviewDrawer';
4
+ import { usePreviewStack } from '../../src/components/layers/PreviewDrawer/usePreviewStack';
5
+ import type { IPreviewRef } from '../../src/components/layers/PreviewDrawer/usePreviewStack';
6
+ import { KeyValueList } from '../../src/components/data/KeyValueList/KeyValueList';
7
+ import { Example, Row, Showcase } from '../frame';
8
+
9
+ const REQUEST: IPreviewRef = { type: 'request', id: '9f3c2a71' };
10
+ const SESSION: IPreviewRef = { type: 'session', id: 'b41d07ce' };
11
+ const LOG: IPreviewRef = { type: 'log', id: 'e7a90b14' };
12
+ const RELEASE: IPreviewRef = { type: 'release', id: '2.14.0' };
13
+
14
+ const TITLES: Record<string, string> = {
15
+ request: 'GET /api/invoices',
16
+ session: 'Session b41d07ce',
17
+ log: 'Log e7a90b14',
18
+ release: 'Version 2.14.0',
19
+ };
20
+
21
+ const titleOf = (ref: IPreviewRef) =>
22
+ TITLES[ref.type] ?? `${ref.type} ${ref.id}`;
23
+
24
+ const Content = ({ reference }: { reference: IPreviewRef }) => {
25
+ const { open } = usePreviewStack();
26
+ return (
27
+ <>
28
+ <KeyValueList
29
+ items={[
30
+ { id: 'id', label: 'Identifiant', value: reference.id, mono: true },
31
+ {
32
+ id: 'status',
33
+ label: 'Statut',
34
+ value: reference.type === 'request' ? '200' : 'terminée',
35
+ mono: true,
36
+ },
37
+ { id: 'duration', label: 'Durée', value: '184 ms', mono: true },
38
+ { id: 'env', label: 'Environnement', value: 'prod' },
39
+ ]}
40
+ />
41
+ {reference.type === 'request' && (
42
+ <Button variant="outlined" onClick={() => open(SESSION)}>
43
+ Ouvrir la session liée
44
+ </Button>
45
+ )}
46
+ {reference.type === 'session' && (
47
+ <Button variant="outlined" onClick={() => open(LOG)}>
48
+ Ouvrir le log lié
49
+ </Button>
50
+ )}
51
+ {reference.type === 'log' && (
52
+ <Button variant="outlined" onClick={() => open(RELEASE)}>
53
+ Ouvrir la version liée
54
+ </Button>
55
+ )}
56
+ </>
57
+ );
58
+ };
59
+
60
+ /** Steps of the opening sequence: nothing yet, then each preview opened on the previous one. */
61
+ enum Arrival {
62
+ Idle,
63
+ RequestOpened,
64
+ SessionOpened,
65
+ LogOpened,
66
+ Done,
67
+ }
68
+
69
+ export const PreviewDrawerPage = () => {
70
+ const { stack, open } = usePreviewStack();
71
+ const arrival = useRef(Arrival.Idle);
72
+
73
+ // Four previews stacked on arrival, so the page shows the trail without a click. Each one is
74
+ // opened once the stack of the previous render is in the URL.
75
+ useEffect(() => {
76
+ if (arrival.current === Arrival.Idle && stack.length === 0) {
77
+ arrival.current = Arrival.RequestOpened;
78
+ open(REQUEST);
79
+ } else if (
80
+ arrival.current === Arrival.RequestOpened &&
81
+ stack.length === 1
82
+ ) {
83
+ arrival.current = Arrival.SessionOpened;
84
+ open(SESSION);
85
+ } else if (
86
+ arrival.current === Arrival.SessionOpened &&
87
+ stack.length === 2
88
+ ) {
89
+ arrival.current = Arrival.LogOpened;
90
+ open(LOG);
91
+ } else if (arrival.current === Arrival.LogOpened && stack.length === 3) {
92
+ arrival.current = Arrival.Done;
93
+ open(RELEASE);
94
+ }
95
+ }, [stack, open]);
96
+
97
+ return (
98
+ <Showcase
99
+ family="Couches"
100
+ name="PreviewDrawer"
101
+ description="Aperçu empilable porté par l'URL (?preview=type:id,type:id, cinq au plus) : fil de toute la pile (chaque niveau ramène à lui, replié en « … » et les deux derniers sur téléphone), Échap ferme le dernier, lien vers la pleine page."
102
+ >
103
+ <Example title="Ouvrir un aperçu">
104
+ <Row>
105
+ <Button variant="outlined" onClick={() => open(REQUEST)}>
106
+ Requête GET /api/invoices
107
+ </Button>
108
+ <Button variant="outlined" onClick={() => open(SESSION)}>
109
+ Session b41d07ce
110
+ </Button>
111
+ </Row>
112
+ </Example>
113
+ <PreviewDrawer
114
+ titleOf={titleOf}
115
+ fullPageHref={(ref) => `/${ref.type}/${ref.id}`}
116
+ render={(ref) => <Content reference={ref} />}
117
+ />
118
+ </Showcase>
119
+ );
120
+ };
@@ -35,7 +35,7 @@ export const ProductTourPage = () => {
35
35
  <Showcase
36
36
  family="Couches"
37
37
  name="ProductTour"
38
- description="Visite guidée d'un service : une bulle ancrée sur un élément data-tour, le reste de la page voilé autour d'une découpe. Échap passe la visite, les flèches changent d'étape ; sous 768 px, une feuille du bas."
38
+ description="Visite guidée d'un service : une bulle ancrée sur un élément data-tour, le reste de la page voilé autour d'une découpe. « Étape n sur N » et une croix en en-tête, « Précédent » dès la deuxième étape ; Échap et la croix passent la visite, les flèches changent d'étape ; sous 768 px, une feuille du bas. Une cible cachée est ignorée."
39
39
  >
40
40
  <Example title="Première visite de Memories">
41
41
  <Row>
@@ -46,6 +46,14 @@ export const ProductTourPage = () => {
46
46
  >
47
47
  Importer
48
48
  </Button>
49
+ {/* A hidden twin of the first target: the tour points at the rendered one. */}
50
+ <Button
51
+ data-tour="catalog-create-event"
52
+ variant="contained"
53
+ sx={{ display: 'none' }}
54
+ >
55
+ Créer un événement
56
+ </Button>
49
57
  <Button
50
58
  data-tour="catalog-create-event"
51
59
  variant="contained"
@@ -4,6 +4,8 @@ import {
4
4
  Section,
5
5
  SectionTone,
6
6
  } from '../../src/components/structure/Section/Section';
7
+ import { ToolIcon } from '../../src/components/meaning/ToolIcon/ToolIcon';
8
+ import { ToolKind } from '../../src/theme/tokens/domain';
7
9
  import { KeyValueList } from '../../src/components/data/KeyValueList/KeyValueList';
8
10
  import { Columns, Example, LastEvent, Showcase, useEventLog } from '../frame';
9
11
  import { ACCOUNTS, LONG_NAV_LABEL, formatEuro } from '../fixtures';
@@ -38,6 +40,14 @@ export const SectionPage = () => {
38
40
  </Columns>
39
41
  </Section>
40
42
  </Example>
43
+ <Example title="Avec le logo d'un service">
44
+ <Section
45
+ title="Mes clouds"
46
+ icon={<ToolIcon tool={ToolKind.Cloud} size={20} />}
47
+ >
48
+ <LastEvent />
49
+ </Section>
50
+ </Example>
41
51
  <Example title="Sans action, contenu en liste clé-valeur">
42
52
  <Section title="Disponible à dépenser">
43
53
  <KeyValueList
@@ -1,4 +1,6 @@
1
1
  import { StatTile } from '../../src/components/data/StatTile/StatTile';
2
+ import { ToolIcon } from '../../src/components/meaning/ToolIcon/ToolIcon';
3
+ import { ToolKind } from '../../src/theme/tokens/domain';
2
4
  import { Columns, Example, Showcase } from '../frame';
3
5
  import {
4
6
  BANKING_TOTALS,
@@ -45,6 +47,17 @@ export const StatTilePage = () => (
45
47
  <Example title="Transactions d'octobre (la dernière tuile est un lien)">
46
48
  <BankingStatTiles />
47
49
  </Example>
50
+ <Example title="Avec le logo d'un service">
51
+ <Columns>
52
+ <StatTile
53
+ label="Stockage Cloud"
54
+ icon={<ToolIcon tool={ToolKind.Cloud} size={16} />}
55
+ value="3,2"
56
+ unit="Go"
57
+ delta={{ value: 'sur 10 Go', tone: 'text.muted' }}
58
+ />
59
+ </Columns>
60
+ </Example>
48
61
  <Example title="Avec unité">
49
62
  <Columns>
50
63
  <StatTile
@@ -0,0 +1,40 @@
1
+ import { ValueMenu } from '../../src/components/meaning/ValueMenu/ValueMenu';
2
+ import { Example, LastEvent, Row, Showcase, useEventLog } from '../frame';
3
+
4
+ export const ValueMenuPage = () => {
5
+ const { log } = useEventLog();
6
+ const handlers = (field: string, value: string) => ({
7
+ onFilter: () => log(`filtrer ${field}:${value}`),
8
+ onExclude: () => log(`exclure ${field}:${value}`),
9
+ onCopy: () => log(`copier ${value}`),
10
+ });
11
+ return (
12
+ <Showcase
13
+ family="Sens"
14
+ name="ValueMenu"
15
+ description="Rend une valeur cliquable : filtrer, exclure, copier, puis selon la page ajouter en colonne, voir l'évolution ou ouvrir l'objet désigné."
16
+ >
17
+ <Example title="Valeur d'un champ connu : trois actions" framed>
18
+ <ValueMenu field="level" value="ERROR" {...handlers('level', 'ERROR')}>
19
+ ERROR
20
+ </ValueMenu>
21
+ </Example>
22
+ <Example title="Valeur d'une liste : six actions" framed>
23
+ <Row>
24
+ <span>Requête</span>
25
+ <ValueMenu
26
+ field="requestId"
27
+ value="9f3c2a71"
28
+ {...handlers('requestId', '9f3c2a71')}
29
+ onAddColumn={() => log('ajouter requestId en colonne')}
30
+ onTrend={() => log('évolution de requestId')}
31
+ onOpen={() => log('ouvrir 9f3c2a71')}
32
+ >
33
+ 9f3c2a71
34
+ </ValueMenu>
35
+ </Row>
36
+ </Example>
37
+ <LastEvent />
38
+ </Showcase>
39
+ );
40
+ };
@@ -27,6 +27,8 @@ import { DateFieldPage } from './DateField';
27
27
  import { DateRangeFieldPage } from './DateRangeField';
28
28
  import { DrawerPage } from './Drawer';
29
29
  import { DropZonePage } from './DropZone';
30
+ import { ExpressionSearchPage } from './ExpressionSearch';
31
+ import { FacetPanelPage } from './FacetPanel';
30
32
  import { FilterBarPage } from './FilterBar';
31
33
  import { FormFieldPage } from './FormField';
32
34
  import { ImportButtonPage } from './ImportButton';
@@ -41,6 +43,7 @@ import { NotificationsMenuPage } from './NotificationsMenu';
41
43
  import { NumberFieldPage } from './NumberField';
42
44
  import { PageHeaderPage } from './PageHeader';
43
45
  import { PageStatePage } from './PageState';
46
+ import { PreviewDrawerPage } from './PreviewDrawer';
44
47
  import { ProductTourPage } from './ProductTour';
45
48
  import { QRCodeModalPage } from './QRCodeModal';
46
49
  import { RichSelectPage } from './RichSelect';
@@ -63,6 +66,7 @@ import { ToastPage } from './Toast';
63
66
  import { ToolbarPage } from './Toolbar';
64
67
  import { ToolIconPage } from './ToolIcon';
65
68
  import { UsageMeterPage } from './UsageMeter';
69
+ import { ValueMenuPage } from './ValueMenu';
66
70
 
67
71
  /** Every catalog page, by the `component` URL parameter (`catalog-entries.ts`). */
68
72
  export const CATALOG_PAGES: Record<string, ComponentType> = {
@@ -94,6 +98,8 @@ export const CATALOG_PAGES: Record<string, ComponentType> = {
94
98
  DateRangeField: DateRangeFieldPage,
95
99
  Drawer: DrawerPage,
96
100
  DropZone: DropZonePage,
101
+ ExpressionSearch: ExpressionSearchPage,
102
+ FacetPanel: FacetPanelPage,
97
103
  FilterBar: FilterBarPage,
98
104
  FormField: FormFieldPage,
99
105
  ImportButton: ImportButtonPage,
@@ -108,6 +114,7 @@ export const CATALOG_PAGES: Record<string, ComponentType> = {
108
114
  NumberField: NumberFieldPage,
109
115
  PageHeader: PageHeaderPage,
110
116
  PageState: PageStatePage,
117
+ PreviewDrawer: PreviewDrawerPage,
111
118
  ProductTour: ProductTourPage,
112
119
  QRCodeModal: QRCodeModalPage,
113
120
  RichSelect: RichSelectPage,
@@ -130,4 +137,5 @@ export const CATALOG_PAGES: Record<string, ComponentType> = {
130
137
  Toolbar: ToolbarPage,
131
138
  ToolIcon: ToolIconPage,
132
139
  UsageMeter: UsageMeterPage,
140
+ ValueMenu: ValueMenuPage,
133
141
  };
@@ -32,6 +32,13 @@ const REACH: Record<
32
32
  .click();
33
33
  await expect(page.getByRole('dialog')).toBeVisible();
34
34
  },
35
+ // The value of a list (six actions): the menu lists every action with its label.
36
+ 'menu-open': async (page) => {
37
+ await page.getByRole('button', { name: /Actions sur requestId/ }).click();
38
+ const menu = page.getByRole('menu').or(page.getByRole('dialog'));
39
+ await expect(menu).toBeVisible();
40
+ await expect(menu.getByRole('menuitem')).toHaveCount(6);
41
+ },
35
42
  // The first top bar trigger of the page: the notifications bell or the account avatar.
36
43
  'panel-open': async (page) => {
37
44
  await page.locator('[aria-haspopup]').first().click();
@@ -0,0 +1,33 @@
1
+ import { expect, test } from '@playwright/test';
2
+ import { ThemeMode } from '../../src/theme/tokens/primitives';
3
+ import { openCatalogPage } from './catalog-page';
4
+
5
+ // ExpressionSearch in a narrow field: the underlined character of a syntax error must be inside
6
+ // the visible part of the field when the error appears (jsdom has no layout, so this is checked
7
+ // in a real browser).
8
+
9
+ const PHONE = { width: 390, height: 844 };
10
+
11
+ test('the underlined error character is in view in a 390 px field', async ({
12
+ page,
13
+ }) => {
14
+ await openCatalogPage(page, 'ExpressionSearch', ThemeMode.Light, PHONE);
15
+ const mark = page.locator('[data-expression-error]').first();
16
+ const input = page.getByRole('combobox').nth(1);
17
+ await expect(mark).toBeAttached();
18
+
19
+ const box = async (locator: typeof mark) => {
20
+ const rect = await locator.evaluate((element) => {
21
+ const { left, right } = element.getBoundingClientRect();
22
+ return { left, right };
23
+ });
24
+ return rect;
25
+ };
26
+ const markBox = await box(mark);
27
+ const inputBox = await box(input);
28
+ expect(await input.evaluate((element) => element.scrollLeft)).toBeGreaterThan(
29
+ 0,
30
+ );
31
+ expect(markBox.left).toBeGreaterThanOrEqual(inputBox.left - 1);
32
+ expect(markBox.right).toBeLessThanOrEqual(inputBox.right + 1);
33
+ });
@@ -0,0 +1,38 @@
1
+ import { expect, test } from '@playwright/test';
2
+ import { ThemeMode } from '../../src/theme/tokens/primitives';
3
+ import { openCatalogPage } from './catalog-page';
4
+
5
+ // PreviewDrawer: the catalog opens four levels on arrival (request, session, log, release). The
6
+ // trail lists all four on a desktop and folds to « … » and the last two on a phone.
7
+
8
+ const trailItems = (page: Parameters<typeof openCatalogPage>[0]) =>
9
+ page
10
+ .getByRole('navigation', { name: 'Fil des aperçus' })
11
+ .getByRole('listitem');
12
+
13
+ test('the trail lists the four levels at 1440 px', async ({ page }) => {
14
+ await openCatalogPage(page, 'PreviewDrawer', ThemeMode.Light, {
15
+ width: 1440,
16
+ height: 900,
17
+ });
18
+ await expect(trailItems(page)).toHaveText([
19
+ 'GET /api/invoices',
20
+ 'Session b41d07ce',
21
+ 'Log e7a90b14',
22
+ 'Version 2.14.0',
23
+ ]);
24
+ });
25
+
26
+ test('the trail folds to an ellipsis and the last two levels at 390 px', async ({
27
+ page,
28
+ }) => {
29
+ await openCatalogPage(page, 'PreviewDrawer', ThemeMode.Light, {
30
+ width: 390,
31
+ height: 844,
32
+ });
33
+ await expect(trailItems(page)).toHaveText([
34
+ '…',
35
+ 'Log e7a90b14',
36
+ 'Version 2.14.0',
37
+ ]);
38
+ });
@@ -25,6 +25,7 @@ const STATE_LABEL: Record<CaptureState, string> = {
25
25
  'list-open': ', liste ouverte',
26
26
  'calendar-open': ', calendrier ouvert',
27
27
  'panel-open': ', panneau ouvert',
28
+ 'menu-open': ', menu ouvert',
28
29
  };
29
30
 
30
31
  const describe = (capture: Capture): string =>