@alveole/storybook 2.0.0-alpha → 2.0.0-alpha.2
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/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/pages/FormulairesPage.d.ts +8 -0
- package/dist/pages/FormulairesPage.d.ts.map +1 -0
- package/dist/pages/FormulairesPage.js +16 -0
- package/dist/pages/MigrationV2Page.d.ts +8 -0
- package/dist/pages/MigrationV2Page.d.ts.map +1 -0
- package/dist/pages/MigrationV2Page.js +12 -0
- package/dist/pages/formulaires.d.ts +12 -0
- package/dist/pages/formulaires.d.ts.map +1 -0
- package/dist/pages/formulaires.js +99 -0
- package/dist/pages/formulaires.stories.d.ts +8 -0
- package/dist/pages/formulaires.stories.d.ts.map +1 -0
- package/dist/pages/formulaires.stories.js +42 -0
- package/dist/pages/formulaires.stories.sources.d.ts +14 -0
- package/dist/pages/formulaires.stories.sources.d.ts.map +1 -0
- package/dist/pages/formulaires.stories.sources.js +14 -0
- package/dist/pages/migrationV2.d.ts +12 -0
- package/dist/pages/migrationV2.d.ts.map +1 -0
- package/dist/pages/migrationV2.js +612 -0
- package/package.json +8 -7
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,9 @@ export * from './components/JsonBlock';
|
|
|
3
3
|
export * from './components/UIKitColumn';
|
|
4
4
|
export * from './components/UIKitTopBar';
|
|
5
5
|
export * from './navigation';
|
|
6
|
+
export * from './pages/FormulairesPage';
|
|
6
7
|
export * from './pages/InstallationPage';
|
|
8
|
+
export * from './pages/MigrationV2Page';
|
|
7
9
|
export * from './pages/PhilosophyPage';
|
|
8
10
|
export * from './pages/UIKitHomePage';
|
|
9
11
|
export * from './pages/UIKitPage';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,cAAc,CAAC;AAC7B,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAClC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,qCAAqC,CAAC;AACpD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,mCAAmC,CAAC;AAClD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,iCAAiC,CAAC;AAChD,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,cAAc,CAAC;AAC7B,cAAc,yBAAyB,CAAC;AACxC,cAAc,0BAA0B,CAAC;AACzC,cAAc,yBAAyB,CAAC;AACxC,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAClC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,qCAAqC,CAAC;AACpD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,mCAAmC,CAAC;AAClD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,iCAAiC,CAAC;AAChD,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,9 @@ export * from './components/JsonBlock';
|
|
|
3
3
|
export * from './components/UIKitColumn';
|
|
4
4
|
export * from './components/UIKitTopBar';
|
|
5
5
|
export * from './navigation';
|
|
6
|
+
export * from './pages/FormulairesPage';
|
|
6
7
|
export * from './pages/InstallationPage';
|
|
8
|
+
export * from './pages/MigrationV2Page';
|
|
7
9
|
export * from './pages/PhilosophyPage';
|
|
8
10
|
export * from './pages/UIKitHomePage';
|
|
9
11
|
export * from './pages/UIKitPage';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export type FormulairesPageProps = {
|
|
3
|
+
beforeContent?: React.ReactNode;
|
|
4
|
+
sidebar?: React.ReactNode;
|
|
5
|
+
footerContent?: React.ReactNode;
|
|
6
|
+
};
|
|
7
|
+
export declare const FormulairesPage: ({ beforeContent, sidebar, footerContent }: FormulairesPageProps) => React.JSX.Element;
|
|
8
|
+
//# sourceMappingURL=FormulairesPage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FormulairesPage.d.ts","sourceRoot":"","sources":["../../src/pages/FormulairesPage.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAC;AA0B1B,MAAM,MAAM,oBAAoB,GAAG;IACjC,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAChC,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CACjC,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,2CAA2C,oBAAoB,sBAiB9F,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { AnchorHeading, Box, MarkdownDescription } from '@alveole/components';
|
|
3
|
+
import { useTheme } from '@alveole/theme';
|
|
4
|
+
import { EcranDeCatalogue } from '../components/EcranDeCatalogue';
|
|
5
|
+
import { JsonBlock } from '../components/JsonBlock';
|
|
6
|
+
import { SECTIONS_DE_FORMULAIRES } from './formulaires';
|
|
7
|
+
import { Anatomie, Clavier, Fichiers, FormulaireDeBase, Sources, Validation } from './formulaires.stories';
|
|
8
|
+
const DEMOS = { Anatomie, Clavier, Fichiers, FormulaireDeBase, Validation };
|
|
9
|
+
const SectionDeFormulairesView = ({ titre, markdown, demo }) => {
|
|
10
|
+
const { text } = useTheme();
|
|
11
|
+
const Demo = demo ? DEMOS[demo] : null;
|
|
12
|
+
return (_jsxs(Box, { display: "flex", gap: 8, mb: '400', children: [_jsx(AnchorHeading, { style: text.Titres['H4 - SM'], children: titre }), _jsx(MarkdownDescription, { children: markdown }), demo && Demo ? (_jsxs(Box, { display: "flex", gap: 8, children: [_jsx(Demo, {}), _jsx(JsonBlock, { language: "tsx", value: Sources.storySources[demo]() })] })) : null] }));
|
|
13
|
+
};
|
|
14
|
+
export const FormulairesPage = ({ beforeContent, sidebar, footerContent }) => {
|
|
15
|
+
return (_jsx(EcranDeCatalogue, { title: "Formulaires", description: "Construire un formulaire avec les champs Alveole", sidebar: sidebar, beforeContent: beforeContent, footerContent: footerContent, sommaire: SECTIONS_DE_FORMULAIRES.map(({ titre }) => titre), children: _jsx(Box, { display: "flex", gap: 0, children: SECTIONS_DE_FORMULAIRES.map(section => (_jsx(SectionDeFormulairesView, { ...section }, section.titre))) }) }));
|
|
16
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export type MigrationV2PageProps = {
|
|
3
|
+
beforeContent?: React.ReactNode;
|
|
4
|
+
sidebar?: React.ReactNode;
|
|
5
|
+
footerContent?: React.ReactNode;
|
|
6
|
+
};
|
|
7
|
+
export declare const MigrationV2Page: ({ beforeContent, sidebar, footerContent }: MigrationV2PageProps) => React.JSX.Element;
|
|
8
|
+
//# sourceMappingURL=MigrationV2Page.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MigrationV2Page.d.ts","sourceRoot":"","sources":["../../src/pages/MigrationV2Page.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAC;AAe1B,MAAM,MAAM,oBAAoB,GAAG;IACjC,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAChC,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CACjC,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,2CAA2C,oBAAoB,sBAoB9F,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { AnchorHeading, Box, MarkdownDescription } from '@alveole/components';
|
|
3
|
+
import { useTheme } from '@alveole/theme';
|
|
4
|
+
import { EcranDeCatalogue } from '../components/EcranDeCatalogue';
|
|
5
|
+
import { INTRODUCTION_DE_MIGRATION_V2, SECTIONS_DE_MIGRATION_V2 } from './migrationV2';
|
|
6
|
+
const SectionDeMigrationV2 = ({ titre, markdown }) => {
|
|
7
|
+
const { text } = useTheme();
|
|
8
|
+
return (_jsxs(Box, { display: "flex", gap: 8, mb: '400', children: [_jsx(AnchorHeading, { style: text.Titres['H4 - SM'], children: titre }), _jsx(MarkdownDescription, { children: markdown })] }));
|
|
9
|
+
};
|
|
10
|
+
export const MigrationV2Page = ({ beforeContent, sidebar, footerContent }) => {
|
|
11
|
+
return (_jsx(EcranDeCatalogue, { title: "Migration v2", description: "Migrer une application d'Alveole 1.x vers 2.0", sidebar: sidebar, beforeContent: beforeContent, footerContent: footerContent, sommaire: SECTIONS_DE_MIGRATION_V2.map(({ titre }) => titre), children: _jsxs(Box, { display: "flex", gap: 0, children: [_jsx(Box, { mb: '400', children: _jsx(MarkdownDescription, { children: INTRODUCTION_DE_MIGRATION_V2 }) }), SECTIONS_DE_MIGRATION_V2.map(section => (_jsx(SectionDeMigrationV2, { ...section }, section.titre)))] }) }));
|
|
12
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Le texte de la page Formulaires. Chaque section peut nommer une démo de
|
|
3
|
+
* formulaires.stories.tsx : la page la rend en direct, avec son code dessous.
|
|
4
|
+
*/
|
|
5
|
+
import type { StorySourceName } from './formulaires.stories.sources';
|
|
6
|
+
export type SectionDeFormulaires = {
|
|
7
|
+
titre: string;
|
|
8
|
+
markdown: string;
|
|
9
|
+
demo?: StorySourceName;
|
|
10
|
+
};
|
|
11
|
+
export declare const SECTIONS_DE_FORMULAIRES: readonly SectionDeFormulaires[];
|
|
12
|
+
//# sourceMappingURL=formulaires.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formulaires.d.ts","sourceRoot":"","sources":["../../src/pages/formulaires.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAErE,MAAM,MAAM,oBAAoB,GAAG;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,eAAe,CAAC;CACxB,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAE,SAAS,oBAAoB,EAkGlE,CAAC"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export const SECTIONS_DE_FORMULAIRES = [
|
|
2
|
+
{
|
|
3
|
+
titre: 'Anatomie d’un champ',
|
|
4
|
+
markdown: `Un **champ** est un \`FormControl\` qui enveloppe un **contrôle de saisie** (\`TextInput\`, \`Select\`,
|
|
5
|
+
\`DateInput\`, \`FileInput\`…). Le libellé (\`label\`), l'aide (\`hint\`), l'astérisque (\`required\`) et le
|
|
6
|
+
message (\`error\` / \`success\`) vont sur \`FormControl\` ; le contrôle reste nu.
|
|
7
|
+
|
|
8
|
+
\`error\` est passé **aux deux** : \`FormControl\` affiche le message sous le champ, le contrôle colore sa
|
|
9
|
+
bordure. \`FormControl\` relie aussi le libellé au contrôle pour les lecteurs d'écran : ne pas
|
|
10
|
+
reconstruire un champ à la main avec \`FormControlLabel\` et \`FormControlCaption\`.
|
|
11
|
+
|
|
12
|
+
Les contrôles sont **contrôlés** : \`value\` + \`onChangeText\` pour les textes, \`value\` + \`onChange\`
|
|
13
|
+
pour les autres, \`checked\` + \`onCheckedChange\` pour \`Checkbox\`.`,
|
|
14
|
+
demo: 'Anatomie',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
titre: 'Formulaire de base',
|
|
18
|
+
markdown: `Tout formulaire est enveloppé dans \`Form\`. Sur mobile, il ferme le clavier au toucher en dehors
|
|
19
|
+
d'un champ ; sur le web, il rend un vrai \`<form>\` : **Entrée** déclenche le bouton \`type="submit"\`.
|
|
20
|
+
|
|
21
|
+
Pendant l'envoi, passer \`isLoading\` au bouton plutôt que de le masquer : il garde sa place et
|
|
22
|
+
empêche le double envoi. Les champs sont espacés par un \`Box\` en colonne.`,
|
|
23
|
+
demo: 'FormulaireDeBase',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
titre: 'Validation et erreurs',
|
|
27
|
+
markdown: `Afficher l'erreur **après la première soumission**, pas à la première frappe : on ne reproche pas
|
|
28
|
+
un champ que l'utilisateur n'a pas encore fini de remplir. \`success\` confirme une saisie vérifiée.
|
|
29
|
+
|
|
30
|
+
Une erreur renvoyée par le serveur pour un champ précis va sur **ce champ** (\`error\`), pas dans une
|
|
31
|
+
alerte globale. Une erreur sans champ (réseau, droits) va dans une \`Alert\` au-dessus du bouton.`,
|
|
32
|
+
demo: 'Validation',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
titre: 'Fichiers',
|
|
36
|
+
markdown: `\`FileInput\` ouvre le sélecteur (galerie, appareil photo, documents) ; \`type\` restreint les
|
|
37
|
+
fichiers acceptés (\`image/*\`, \`application/pdf\`). Sur le web, \`FileDropZone\` autour du formulaire
|
|
38
|
+
permet de déposer les fichiers n'importe où : elle partage la même \`value\` que le \`FileInput\`.
|
|
39
|
+
Une seule \`FileDropZone\` par formulaire, liée à un seul champ de fichier.`,
|
|
40
|
+
demo: 'Fichiers',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
titre: 'Clavier sur mobile',
|
|
44
|
+
markdown: `Placer le formulaire dans une \`Page scrollable\` : elle remonte le champ actif au-dessus du
|
|
45
|
+
clavier. Enchaîner les champs avec \`returnKeyType="next"\` et \`onSubmitEditing\` qui donne le focus
|
|
46
|
+
au suivant ; le dernier champ prend \`returnKeyType="done"\`. Choisir le bon \`type\` (\`email\`, \`tel\`,
|
|
47
|
+
\`password\`) donne le bon clavier et l'autoremplissage.`,
|
|
48
|
+
demo: 'Clavier',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
titre: 'Recommandation : react-hook-form',
|
|
52
|
+
markdown: `**Recommandé** : react-hook-form, utilisé dans les applications construites avec Alveole. Alveole
|
|
53
|
+
n'en dépend pas, à installer dans l'application. Un \`Controller\` relie
|
|
54
|
+
chaque champ ; le message vient de \`fieldState.error\` et va, comme toujours, sur \`FormControl\` et
|
|
55
|
+
sur le contrôle.
|
|
56
|
+
|
|
57
|
+
\`\`\`tsx
|
|
58
|
+
import { Controller, useForm } from 'react-hook-form';
|
|
59
|
+
import { Button, Form, FormControl, TextInput } from '@alveole/components';
|
|
60
|
+
|
|
61
|
+
type Contact = { email: string };
|
|
62
|
+
|
|
63
|
+
export const ContactForm = ({ onSave }: { onSave: (c: Contact) => Promise<void> }) => {
|
|
64
|
+
const { control, handleSubmit, setError, formState } = useForm<Contact>({ defaultValues: { email: '' } });
|
|
65
|
+
|
|
66
|
+
const soumettre = handleSubmit(async contact => {
|
|
67
|
+
try {
|
|
68
|
+
await onSave(contact);
|
|
69
|
+
} catch {
|
|
70
|
+
// Une erreur serveur rattachée à un champ s'affiche sur ce champ.
|
|
71
|
+
setError('email', { message: 'Adresse déjà utilisée' });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<Form>
|
|
77
|
+
<Controller
|
|
78
|
+
control={control}
|
|
79
|
+
name="email"
|
|
80
|
+
rules={{ required: 'Adresse requise' }}
|
|
81
|
+
render={({ field, fieldState }) => (
|
|
82
|
+
<FormControl label="Adresse email" error={fieldState.error?.message} required>
|
|
83
|
+
<TextInput
|
|
84
|
+
type="email"
|
|
85
|
+
value={field.value}
|
|
86
|
+
onChangeText={field.onChange}
|
|
87
|
+
onBlur={field.onBlur}
|
|
88
|
+
error={fieldState.error?.message}
|
|
89
|
+
/>
|
|
90
|
+
</FormControl>
|
|
91
|
+
)}
|
|
92
|
+
/>
|
|
93
|
+
<Button type="submit" variant="primary" title="Enregistrer" isLoading={formState.isSubmitting} onPress={soumettre} />
|
|
94
|
+
</Form>
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
\`\`\``,
|
|
98
|
+
},
|
|
99
|
+
];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export declare const Anatomie: () => React.JSX.Element;
|
|
3
|
+
export declare const FormulaireDeBase: () => React.JSX.Element;
|
|
4
|
+
export declare const Validation: () => React.JSX.Element;
|
|
5
|
+
export declare const Fichiers: () => React.JSX.Element;
|
|
6
|
+
export declare const Clavier: () => React.JSX.Element;
|
|
7
|
+
export * as Sources from './formulaires.stories.sources';
|
|
8
|
+
//# sourceMappingURL=formulaires.stories.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formulaires.stories.d.ts","sourceRoot":"","sources":["../../src/pages/formulaires.stories.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAmB,MAAM,OAAO,CAAC;AAKxC,eAAO,MAAM,QAAQ,yBAQpB,CAAC;AAOF,eAAO,MAAM,gBAAgB,yBAyB5B,CAAC;AAEF,eAAO,MAAM,UAAU,yBAiBtB,CAAC;AAEF,eAAO,MAAM,QAAQ,yBAWpB,CAAC;AAEF,eAAO,MAAM,OAAO,yBAcnB,CAAC;AAEF,OAAO,KAAK,OAAO,MAAM,+BAA+B,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Button, Checkbox, FileDropZone, FileInput, Form, FormControl, Select, TextInput, } from '@alveole/components';
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
// Les démos de la page Formulaires. Le code affiché sous chaque démo est extrait de ce fichier
|
|
5
|
+
// par generate:sources : ce que la page montre est ce qui tourne.
|
|
6
|
+
export const Anatomie = () => {
|
|
7
|
+
const [email, setEmail] = useState('jean@');
|
|
8
|
+
const error = email.includes('.') ? undefined : 'Adresse email invalide';
|
|
9
|
+
return (_jsx(FormControl, { label: "Adresse email", hint: "Votre email professionnel", error: error, required: true, children: _jsx(TextInput, { type: "email", value: email, onChangeText: setEmail, error: error }) }));
|
|
10
|
+
};
|
|
11
|
+
const PAYS = [
|
|
12
|
+
{ value: 'fr', label: 'France' },
|
|
13
|
+
{ value: 'be', label: 'Belgique' },
|
|
14
|
+
];
|
|
15
|
+
export const FormulaireDeBase = () => {
|
|
16
|
+
const [nom, setNom] = useState('');
|
|
17
|
+
const [pays, setPays] = useState(null);
|
|
18
|
+
const [cgu, setCgu] = useState(false);
|
|
19
|
+
const [envoi, setEnvoi] = useState(false);
|
|
20
|
+
const soumettre = () => {
|
|
21
|
+
setEnvoi(true);
|
|
22
|
+
setTimeout(() => setEnvoi(false), 1500);
|
|
23
|
+
};
|
|
24
|
+
return (_jsx(Form, { children: _jsxs(Box, { display: "flex", flexDirection: "column", gap: 16, children: [_jsx(FormControl, { label: "Nom", required: true, children: _jsx(TextInput, { value: nom, onChangeText: setNom }) }), _jsx(FormControl, { label: "Pays", children: _jsx(Select, { placeholder: "Choisir...", options: PAYS, value: pays, onChange: setPays }) }), _jsx(Checkbox, { label: "J'accepte les conditions", checked: cgu, onCheckedChange: c => setCgu(c === true) }), _jsx(Button, { type: "submit", variant: "primary", title: "Envoyer", isLoading: envoi, disabled: !cgu, onPress: soumettre })] }) }));
|
|
25
|
+
};
|
|
26
|
+
export const Validation = () => {
|
|
27
|
+
const [email, setEmail] = useState('');
|
|
28
|
+
const [soumis, setSoumis] = useState(false);
|
|
29
|
+
// L'erreur n'apparaît qu'après la première soumission : on ne reproche pas un champ pas encore rempli.
|
|
30
|
+
const error = soumis && !email.includes('@') ? 'Adresse email invalide' : undefined;
|
|
31
|
+
const success = soumis && !error ? 'Adresse valide' : undefined;
|
|
32
|
+
return (_jsx(Form, { children: _jsxs(Box, { display: "flex", flexDirection: "column", gap: 16, children: [_jsx(FormControl, { label: "Adresse email", error: error, success: success, required: true, children: _jsx(TextInput, { type: "email", value: email, onChangeText: setEmail, error: error, success: success }) }), _jsx(Button, { type: "submit", variant: "primary", title: "Valider", onPress: () => setSoumis(true) })] }) }));
|
|
33
|
+
};
|
|
34
|
+
export const Fichiers = () => {
|
|
35
|
+
const [pieces, setPieces] = useState(null);
|
|
36
|
+
return (_jsx(FileDropZone, { value: pieces, onChange: setPieces, multiple: true, children: _jsx(Box, { p: 16, children: _jsx(FormControl, { label: "Pi\u00E8ces jointes", hint: "Glissez des fichiers n'importe o\u00F9 sur le formulaire.", children: _jsx(FileInput, { value: pieces, onChange: setPieces, multiple: true }) }) }) }));
|
|
37
|
+
};
|
|
38
|
+
export const Clavier = () => {
|
|
39
|
+
const prenom = React.useRef(null);
|
|
40
|
+
return (_jsx(Form, { children: _jsxs(Box, { display: "flex", flexDirection: "column", gap: 16, children: [_jsx(FormControl, { label: "Nom", children: _jsx(TextInput, { returnKeyType: "next", submitBehavior: "submit", onSubmitEditing: () => prenom.current?.focus() }) }), _jsx(FormControl, { label: "Pr\u00E9nom", children: _jsx(TextInput, { ref: prenom, returnKeyType: "done" }) })] }) }));
|
|
41
|
+
};
|
|
42
|
+
export * as Sources from './formulaires.stories.sources';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const Anatomie: () => string;
|
|
2
|
+
export declare const FormulaireDeBase: () => string;
|
|
3
|
+
export declare const Validation: () => string;
|
|
4
|
+
export declare const Fichiers: () => string;
|
|
5
|
+
export declare const Clavier: () => string;
|
|
6
|
+
export declare const storySources: {
|
|
7
|
+
readonly Anatomie: () => string;
|
|
8
|
+
readonly FormulaireDeBase: () => string;
|
|
9
|
+
readonly Validation: () => string;
|
|
10
|
+
readonly Fichiers: () => string;
|
|
11
|
+
readonly Clavier: () => string;
|
|
12
|
+
};
|
|
13
|
+
export type StorySourceName = keyof typeof storySources;
|
|
14
|
+
//# sourceMappingURL=formulaires.stories.sources.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formulaires.stories.sources.d.ts","sourceRoot":"","sources":["../../src/pages/formulaires.stories.sources.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,QAAQ,cAA8Y,CAAC;AAEpa,eAAO,MAAM,gBAAgB,cAAk+B,CAAC;AAEhgC,eAAO,MAAM,UAAU,cAAq1B,CAAC;AAE72B,eAAO,MAAM,QAAQ,cAAmc,CAAC;AAEzd,eAAO,MAAM,OAAO,cAAsiB,CAAC;AAE3jB,eAAO,MAAM,YAAY;;;;;;CAMf,CAAC;AAEX,MAAM,MAAM,eAAe,GAAG,MAAM,OAAO,YAAY,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// This file is generated. Do not edit manually.
|
|
2
|
+
// Source: src/pages/formulaires.stories.tsx
|
|
3
|
+
export const Anatomie = () => "export const Anatomie = () => {\n const [email, setEmail] = useState('jean@');\n const error = email.includes('.') ? undefined : 'Adresse email invalide';\n return (\n <FormControl label=\"Adresse email\" hint=\"Votre email professionnel\" error={error} required>\n <TextInput type=\"email\" value={email} onChangeText={setEmail} error={error} />\n </FormControl>\n );\n};";
|
|
4
|
+
export const FormulaireDeBase = () => "export const FormulaireDeBase = () => {\n const [nom, setNom] = useState('');\n const [pays, setPays] = useState<string | null>(null);\n const [cgu, setCgu] = useState(false);\n const [envoi, setEnvoi] = useState(false);\n\n const soumettre = () => {\n setEnvoi(true);\n setTimeout(() => setEnvoi(false), 1500);\n };\n\n return (\n <Form>\n <Box display=\"flex\" flexDirection=\"column\" gap={16}>\n <FormControl label=\"Nom\" required>\n <TextInput value={nom} onChangeText={setNom} />\n </FormControl>\n <FormControl label=\"Pays\">\n <Select placeholder=\"Choisir...\" options={PAYS} value={pays} onChange={setPays} />\n </FormControl>\n <Checkbox label=\"J'accepte les conditions\" checked={cgu} onCheckedChange={c => setCgu(c === true)} />\n <Button type=\"submit\" variant=\"primary\" title=\"Envoyer\" isLoading={envoi} disabled={!cgu} onPress={soumettre} />\n </Box>\n </Form>\n );\n};";
|
|
5
|
+
export const Validation = () => "export const Validation = () => {\n const [email, setEmail] = useState('');\n const [soumis, setSoumis] = useState(false);\n // L'erreur n'apparaît qu'après la première soumission : on ne reproche pas un champ pas encore rempli.\n const error = soumis && !email.includes('@') ? 'Adresse email invalide' : undefined;\n const success = soumis && !error ? 'Adresse valide' : undefined;\n\n return (\n <Form>\n <Box display=\"flex\" flexDirection=\"column\" gap={16}>\n <FormControl label=\"Adresse email\" error={error} success={success} required>\n <TextInput type=\"email\" value={email} onChangeText={setEmail} error={error} success={success} />\n </FormControl>\n <Button type=\"submit\" variant=\"primary\" title=\"Valider\" onPress={() => setSoumis(true)} />\n </Box>\n </Form>\n );\n};";
|
|
6
|
+
export const Fichiers = () => "export const Fichiers = () => {\n const [pieces, setPieces] = useState<FileInputValue>(null);\n return (\n <FileDropZone value={pieces} onChange={setPieces} multiple>\n <Box p={16}>\n <FormControl label=\"Pièces jointes\" hint=\"Glissez des fichiers n'importe où sur le formulaire.\">\n <FileInput value={pieces} onChange={setPieces} multiple />\n </FormControl>\n </Box>\n </FileDropZone>\n );\n};";
|
|
7
|
+
export const Clavier = () => "export const Clavier = () => {\n const prenom = React.useRef<React.ComponentRef<typeof TextInput>>(null);\n return (\n <Form>\n <Box display=\"flex\" flexDirection=\"column\" gap={16}>\n <FormControl label=\"Nom\">\n <TextInput returnKeyType=\"next\" submitBehavior=\"submit\" onSubmitEditing={() => prenom.current?.focus()} />\n </FormControl>\n <FormControl label=\"Prénom\">\n <TextInput ref={prenom} returnKeyType=\"done\" />\n </FormControl>\n </Box>\n </Form>\n );\n};";
|
|
8
|
+
export const storySources = {
|
|
9
|
+
Anatomie,
|
|
10
|
+
FormulaireDeBase,
|
|
11
|
+
Validation,
|
|
12
|
+
Fichiers,
|
|
13
|
+
Clavier,
|
|
14
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Le guide de migration 1.x → 2.0. Il est écrit pour être donné tel quel à un agent IA : la page
|
|
3
|
+
* le rend en Markdown, une section par titre de niveau 2, pour que le sommaire et les ancres
|
|
4
|
+
* suivent le texte sans rien déclarer.
|
|
5
|
+
*/
|
|
6
|
+
export type SectionDeMigration = {
|
|
7
|
+
titre: string;
|
|
8
|
+
markdown: string;
|
|
9
|
+
};
|
|
10
|
+
export declare const INTRODUCTION_DE_MIGRATION_V2 = "Guide destin\u00E9 \u00E0 \u00EAtre donn\u00E9 tel quel \u00E0 un agent IA (ou suivi \u00E0 la main) pour migrer une application\nde `@alveole/*` 1.10 vers 2.0. Chaque section donne : **comment trouver** les appels, **comment les\nr\u00E9\u00E9crire**, et les **pi\u00E8ges** qui font \u00E9chouer le typecheck ou changent le comportement.\n\nAucun alias n'est conserv\u00E9 : tout ce qui est retir\u00E9 casse au typecheck. Le typecheck est donc la\nliste de travail de r\u00E9f\u00E9rence.";
|
|
11
|
+
export declare const SECTIONS_DE_MIGRATION_V2: readonly SectionDeMigration[];
|
|
12
|
+
//# sourceMappingURL=migrationV2.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrationV2.d.ts","sourceRoot":"","sources":["../../src/pages/migrationV2.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,eAAO,MAAM,4BAA4B,gfAKV,CAAC;AAEhC,eAAO,MAAM,wBAAwB,EAAE,SAAS,kBAAkB,EA6lBjE,CAAC"}
|
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
export const INTRODUCTION_DE_MIGRATION_V2 = `Guide destiné à être donné tel quel à un agent IA (ou suivi à la main) pour migrer une application
|
|
2
|
+
de \`@alveole/*\` 1.10 vers 2.0. Chaque section donne : **comment trouver** les appels, **comment les
|
|
3
|
+
réécrire**, et les **pièges** qui font échouer le typecheck ou changent le comportement.
|
|
4
|
+
|
|
5
|
+
Aucun alias n'est conservé : tout ce qui est retiré casse au typecheck. Le typecheck est donc la
|
|
6
|
+
liste de travail de référence.`;
|
|
7
|
+
export const SECTIONS_DE_MIGRATION_V2 = [
|
|
8
|
+
{
|
|
9
|
+
titre: 'Procédure',
|
|
10
|
+
markdown: `1. Mettre à jour les paquets en version exacte :
|
|
11
|
+
\`@alveole/components\`, \`@alveole/core\`, \`@alveole/theme\`, \`@alveole/storybook\`,
|
|
12
|
+
\`@alveole/eslint-config\` → \`2.0.0\`.
|
|
13
|
+
2. Mettre à jour les peer dependencies (section [Dépendances](#1-dependances)).
|
|
14
|
+
3. Lancer les recherches \`git grep\` de chaque section, appliquer les réécritures **section par
|
|
15
|
+
section** (l'ordre du document est l'ordre conseillé : les champs avant les icônes, car un même
|
|
16
|
+
appel est souvent concerné par les deux).
|
|
17
|
+
4. Lancer le typecheck du projet. Chaque erreur restante correspond à une ligne d'un tableau
|
|
18
|
+
ci-dessous.
|
|
19
|
+
5. Lancer les tests et vérifier visuellement les formulaires (libellé, erreur, bordure rouge).
|
|
20
|
+
|
|
21
|
+
Règle générale à retenir : **un champ ne porte plus son libellé**. Le libellé, l'aide (\`hint\`) et
|
|
22
|
+
le message (\`error\` / \`success\`) vont sur \`FormControl\`, le contrôle nu va dedans.
|
|
23
|
+
|
|
24
|
+
\`\`\`tsx
|
|
25
|
+
<FormControl label="…" hint="…" error={error} required>
|
|
26
|
+
<Controle … error={error} />
|
|
27
|
+
</FormControl>
|
|
28
|
+
\`\`\`
|
|
29
|
+
|
|
30
|
+
\`error\` est passé **aux deux** : \`FormControl\` affiche le message sous le champ, le contrôle colore
|
|
31
|
+
sa bordure. Avant, le \`*Field\` faisait les deux tout seul.`,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
titre: '1. Dépendances',
|
|
35
|
+
markdown: `\`\`\`bash
|
|
36
|
+
git grep -nE "lucide-react-native|@lucide/lab|@react-native-picker/picker|expo-linear-gradient|react-native-code-highlighter" -- package.json
|
|
37
|
+
\`\`\`
|
|
38
|
+
|
|
39
|
+
| Paquet | Action |
|
|
40
|
+
| ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
|
|
41
|
+
| \`expo-web-browser\` | **Ajouter** (\`npx expo install expo-web-browser\`) : requis par \`A\` / \`Link\` (\`target\`). |
|
|
42
|
+
| \`expo-image-picker\` | **Ajouter** (\`npx expo install expo-image-picker\`) : requis par \`FileInput\` (galerie, appareil photo). |
|
|
43
|
+
| \`lucide-react-native\`, \`@lucide/lab\`, \`@react-native-picker/picker\`, \`expo-linear-gradient\`, \`react-native-code-highlighter\` | Plus requis par Alveole. Retirer seulement si l'application ne les importe pas elle-même. |
|
|
44
|
+
| \`eslint\` | \`@alveole/eslint-config\` accepte maintenant ESLint 9 **ou** 10. |
|
|
45
|
+
|
|
46
|
+
Vérifier qu'un paquet est encore importé avant de le retirer :
|
|
47
|
+
|
|
48
|
+
\`\`\`bash
|
|
49
|
+
git grep -nE "from '(lucide-react-native|@lucide/lab|@react-native-picker/picker|expo-linear-gradient)'"
|
|
50
|
+
\`\`\``,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
titre: '2. Champs de saisie *Field → FormControl + contrôle',
|
|
54
|
+
markdown: `\`\`\`bash
|
|
55
|
+
git grep -nE "\\b(TextField|TextareaField|EmailField|EmailInput|PasswordField|PhoneField|PhoneInput|NumberField|OtpField|FileField|FormControlNumberInput|FormControlOtpInput|FormControlFileInput)\\b"
|
|
56
|
+
\`\`\`
|
|
57
|
+
|
|
58
|
+
| Avant | Après (dans un \`FormControl\`) |
|
|
59
|
+
| -------------------------------------------------- | ----------------------------------------------------------------------- |
|
|
60
|
+
| \`TextField\` | \`TextInput\` |
|
|
61
|
+
| \`TextareaField\` | \`TextareaInput\` |
|
|
62
|
+
| \`EmailField\`, \`EmailInput\` | \`TextInput type="email"\` |
|
|
63
|
+
| \`PasswordField\` | \`TextInput type="password"\` |
|
|
64
|
+
| \`PhoneField\`, \`PhoneInput\` | \`TextInput type="tel"\` |
|
|
65
|
+
| \`NumberField\`, \`FormControlNumberInput\` | \`NumberInput\` |
|
|
66
|
+
| \`OtpField\`, \`FormControlOtpInput\` | \`OtpInput\` |
|
|
67
|
+
| \`FileField\`, \`FormControlFileInput\` | \`FileInput\` |
|
|
68
|
+
| type \`FileFieldValue\`, \`FormControlFileInputValue\` | \`FileInputValue\` |
|
|
69
|
+
| types \`*FieldProps\` | \`TextInputProps\`, \`NumberInputProps\`, \`OtpInputProps\`, \`FileInputProps\` |
|
|
70
|
+
|
|
71
|
+
Props qui **montent sur \`FormControl\`** : \`label\`, \`labelRight\`, \`hint\`, \`error\`, \`success\`,
|
|
72
|
+
\`required\`, \`disabled\` (et \`disabled\` reste aussi sur le contrôle).
|
|
73
|
+
Tout le reste reste sur le contrôle.
|
|
74
|
+
|
|
75
|
+
### Piège n°1 : \`onChange\` des champs texte
|
|
76
|
+
|
|
77
|
+
Les anciens \`TextField\`, \`TextareaField\`, \`EmailField\`, \`PasswordField\`, \`PhoneField\`, \`PhoneInput\`
|
|
78
|
+
avaient un alias \`onChange(text: string)\`. **Il disparaît** : \`TextInput\` reprend les props de
|
|
79
|
+
React Native, où \`onChange\` reçoit un **événement**, pas une chaîne. Le typecheck ne le signale
|
|
80
|
+
pas toujours (ex. \`onChange={setValue}\` avec un \`setState\` non typé).
|
|
81
|
+
|
|
82
|
+
➡️ Sur \`TextInput\` / \`TextareaInput\` : **renommer systématiquement \`onChange\` en \`onChangeText\`.**
|
|
83
|
+
|
|
84
|
+
Ne concerne **pas** \`NumberInput\`, \`OtpInput\`, \`FileInput\`, \`Select\`, \`DateInput\`, \`TimeInput\`,
|
|
85
|
+
\`DurationInput\` : leur \`onChange\` reçoit toujours la valeur.
|
|
86
|
+
|
|
87
|
+
### Piège n°1 bis : \`FileInput\` affiche désormais une liste de fichiers
|
|
88
|
+
|
|
89
|
+
\`\`\`bash
|
|
90
|
+
git grep -nE "<(FileInput|DragAndDropFile)\\b"
|
|
91
|
+
\`\`\`
|
|
92
|
+
|
|
93
|
+
| Avant | Après |
|
|
94
|
+
| ------------------------------------------ | ---------------------------------------------------------------------------------- |
|
|
95
|
+
| \`placeholder="…"\` | \`labels={{ addFile: '…' }}\` (tous les libellés se règlent par \`labels\`) |
|
|
96
|
+
| \`previewURL={url}\` | un **fichier enregistré** dans \`value\` : \`{ id, uri: url, name, persisted: true }\` |
|
|
97
|
+
| \`hideFilename\`, \`canChange\`, \`forcePlaceholder\` | retirer |
|
|
98
|
+
|
|
99
|
+
Changements de comportement :
|
|
100
|
+
|
|
101
|
+
- Le champ montre un bouton « Ajouter un fichier » puis la **liste** des fichiers (aperçu, ouverture,
|
|
102
|
+
retrait).
|
|
103
|
+
- Sur natif, le bouton ouvre une feuille **Galerie / Appareil photo / Document** (ajouter
|
|
104
|
+
\`expo-image-picker\`, voir [Dépendances](#1-dependances)).
|
|
105
|
+
- \`multiple\` : une nouvelle sélection ou un dépôt **s'ajoute** aux fichiers déjà présents au lieu de
|
|
106
|
+
les remplacer.
|
|
107
|
+
- Champ simple : le bouton est **désactivé** tant qu'un fichier est affiché ; le retirer d'abord.
|
|
108
|
+
- Retirer un fichier ajouté l'enlève de \`value\` ; retirer un **fichier enregistré** le garde, marqué
|
|
109
|
+
\`removed: true\`, pour le supprimer côté serveur à l'envoi.
|
|
110
|
+
- Annuler le sélecteur n'appelle plus \`onChange(null)\` : la valeur reste inchangée.
|
|
111
|
+
|
|
112
|
+
### \`DragAndDropFile\` → \`FileDropZone\`
|
|
113
|
+
|
|
114
|
+
\`DragAndDropFile\` est supprimé. La **zone de dépôt** \`FileDropZone\` enveloppe désormais toute une
|
|
115
|
+
région (souvent le formulaire entier) et se branche sur le même état que le \`FileInput\`. Web
|
|
116
|
+
uniquement : sur natif, elle rend simplement ses enfants. Elle n'ouvre plus le sélecteur au clic :
|
|
117
|
+
c'est le bouton du \`FileInput\` qui s'en charge.
|
|
118
|
+
|
|
119
|
+
\`\`\`tsx
|
|
120
|
+
// Avant
|
|
121
|
+
<DragAndDropFile label="Pièces jointes" hint="PDF ou image" multiple value={f} onChange={setF} />
|
|
122
|
+
|
|
123
|
+
// Après
|
|
124
|
+
<FileDropZone value={f} onChange={setF} multiple>
|
|
125
|
+
<FormControl label="Pièces jointes" hint="PDF ou image">
|
|
126
|
+
<FileInput value={f} onChange={setF} multiple />
|
|
127
|
+
</FormControl>
|
|
128
|
+
</FileDropZone>
|
|
129
|
+
\`\`\`
|
|
130
|
+
|
|
131
|
+
\`label\`, \`hint\`, \`error\`, \`success\` montent sur \`FormControl\` ; \`leading\` disparaît. Pour couvrir
|
|
132
|
+
tout le formulaire, placer \`FileDropZone\` autour du formulaire plutôt qu'autour du seul champ.
|
|
133
|
+
|
|
134
|
+
### Piège n°2 : réglages imposés retirés
|
|
135
|
+
|
|
136
|
+
\`TextField\` forçait \`autoComplete="off"\`, \`textContentType="none"\`, \`autoCapitalize="sentences"\`.
|
|
137
|
+
\`TextInput\` ne force plus rien. Si un champ doit rester sans remplissage automatique, l'écrire
|
|
138
|
+
explicitement : \`<TextInput autoComplete="off" textContentType="none" />\`.
|
|
139
|
+
\`type="text"\` et \`type="hidden"\` de \`TextField\` n'avaient aucun effet :
|
|
140
|
+
retirer \`type="text"\` (valeur par défaut de \`TextInput\`) et \`type="hidden"\`, sans autre changement.
|
|
141
|
+
\`type="hidden"\` n'a jamais masqué le champ (ignoré en natif, réécrit en \`type="text"\` par react-native-web) :
|
|
142
|
+
si un appel comptait dessus pour cacher une valeur, ne pas rendre le champ (\`{visible && <TextInput … />}\`).
|
|
143
|
+
|
|
144
|
+
### Exemples
|
|
145
|
+
|
|
146
|
+
\`\`\`tsx
|
|
147
|
+
// Avant
|
|
148
|
+
<TextField
|
|
149
|
+
label="Nom"
|
|
150
|
+
hint="Tel qu'il apparaît sur la pièce d'identité"
|
|
151
|
+
placeholder="Jean Dupont"
|
|
152
|
+
value={nom}
|
|
153
|
+
onChange={setNom}
|
|
154
|
+
error={errors.nom}
|
|
155
|
+
/>
|
|
156
|
+
|
|
157
|
+
// Après
|
|
158
|
+
<FormControl label="Nom" hint="Tel qu'il apparaît sur la pièce d'identité" error={errors.nom}>
|
|
159
|
+
<TextInput placeholder="Jean Dupont" value={nom} onChangeText={setNom} error={errors.nom} />
|
|
160
|
+
</FormControl>
|
|
161
|
+
\`\`\`
|
|
162
|
+
|
|
163
|
+
\`\`\`tsx
|
|
164
|
+
// Avant
|
|
165
|
+
<EmailField label="Email" value={email} onChange={setEmail} />
|
|
166
|
+
<PasswordField label="Mot de passe" value={password} onChange={setPassword} />
|
|
167
|
+
<PhoneField label="Téléphone" value={tel} onChange={setTel} />
|
|
168
|
+
|
|
169
|
+
// Après
|
|
170
|
+
<FormControl label="Email">
|
|
171
|
+
<TextInput type="email" value={email} onChangeText={setEmail} />
|
|
172
|
+
</FormControl>
|
|
173
|
+
<FormControl label="Mot de passe">
|
|
174
|
+
<TextInput type="password" value={password} onChangeText={setPassword} />
|
|
175
|
+
</FormControl>
|
|
176
|
+
<FormControl label="Téléphone">
|
|
177
|
+
<TextInput type="tel" value={tel} onChangeText={setTel} />
|
|
178
|
+
</FormControl>
|
|
179
|
+
\`\`\`
|
|
180
|
+
|
|
181
|
+
\`\`\`tsx
|
|
182
|
+
// Avant
|
|
183
|
+
<TextareaField label="Message" numberOfLines={4} value={msg} onChange={setMsg} />
|
|
184
|
+
|
|
185
|
+
// Après
|
|
186
|
+
<FormControl label="Message">
|
|
187
|
+
<TextareaInput numberOfLines={4} value={msg} onChangeText={setMsg} />
|
|
188
|
+
</FormControl>
|
|
189
|
+
\`\`\`
|
|
190
|
+
|
|
191
|
+
\`\`\`tsx
|
|
192
|
+
// Avant
|
|
193
|
+
<NumberField label="Montant" value={montant} onChange={setMontant} error={err} />
|
|
194
|
+
|
|
195
|
+
// Après — onChange inchangé : (value: number | null) => void
|
|
196
|
+
<FormControl label="Montant" error={err}>
|
|
197
|
+
<NumberInput value={montant} onChange={setMontant} error={err} />
|
|
198
|
+
</FormControl>
|
|
199
|
+
\`\`\`
|
|
200
|
+
|
|
201
|
+
\`\`\`tsx
|
|
202
|
+
// Avant
|
|
203
|
+
<OtpField label="Code reçu" numberOfDigits={6} onChange={setCode} />
|
|
204
|
+
|
|
205
|
+
// Après — onChange inchangé
|
|
206
|
+
<FormControl label="Code reçu">
|
|
207
|
+
<OtpInput numberOfDigits={6} onChange={setCode} />
|
|
208
|
+
</FormControl>
|
|
209
|
+
\`\`\`
|
|
210
|
+
|
|
211
|
+
\`\`\`tsx
|
|
212
|
+
// Avant
|
|
213
|
+
const [file, setFile] = useState<FileFieldValue>(null);
|
|
214
|
+
<FileField label="Justificatif" type="application/pdf" value={file} onChange={setFile} />;
|
|
215
|
+
|
|
216
|
+
// Après — la vérification du type de fichier (alerte « Type de fichier incorrect ») est conservée dans FileInput
|
|
217
|
+
const [file, setFile] = useState<FileInputValue>(null);
|
|
218
|
+
<FormControl label="Justificatif">
|
|
219
|
+
<FileInput type="application/pdf" value={file} onChange={setFile} />
|
|
220
|
+
</FormControl>;
|
|
221
|
+
\`\`\`
|
|
222
|
+
|
|
223
|
+
Si un \`ref\` était passé au \`*Field\`, le passer au contrôle (\`<TextInput ref={ref} />\`).
|
|
224
|
+
|
|
225
|
+
### \`TextareaInput\` (le composant existe toujours)
|
|
226
|
+
|
|
227
|
+
En 1.10, son type reprenait celui de \`TextField\` : \`label\` y était **obligatoire**, et \`hint\`,
|
|
228
|
+
\`labelRight\`, \`optional\`, \`onChange\` acceptés… mais **aucun n'était affiché ni branché**. Les appels
|
|
229
|
+
existants passent donc presque tous un \`label\` invisible. Ces props n'existent plus.
|
|
230
|
+
|
|
231
|
+
\`\`\`bash
|
|
232
|
+
git grep -nE "<TextareaInput\\b"
|
|
233
|
+
\`\`\`
|
|
234
|
+
|
|
235
|
+
- \`label\` / \`hint\` : les déplacer sur un \`FormControl\` autour. ⚠️ Changement visuel : le libellé
|
|
236
|
+
devient visible. S'il ne doit pas l'être (libellé déjà affiché ailleurs), supprimer simplement la
|
|
237
|
+
prop.
|
|
238
|
+
- \`onChange\` : ne fonctionnait pas avec une chaîne (React Native lui passait un événement) ;
|
|
239
|
+
renommer en \`onChangeText\`.
|
|
240
|
+
|
|
241
|
+
\`\`\`tsx
|
|
242
|
+
// Avant
|
|
243
|
+
<TextareaInput label="Commentaire" value={c} onChangeText={setC} />
|
|
244
|
+
|
|
245
|
+
// Après (libellé visible)
|
|
246
|
+
<FormControl label="Commentaire">
|
|
247
|
+
<TextareaInput value={c} onChangeText={setC} />
|
|
248
|
+
</FormControl>
|
|
249
|
+
\`\`\`
|
|
250
|
+
|
|
251
|
+
### \`FormControlLabel\` et \`RadioGroup\` : \`optional\` / \`optionalText\` supprimés
|
|
252
|
+
|
|
253
|
+
La mention « (optionnel) » disparaît au profit de l'astérisque sur les champs **requis**.
|
|
254
|
+
|
|
255
|
+
\`\`\`bash
|
|
256
|
+
git grep -nE "\\boptional(Text)?[=:]"
|
|
257
|
+
\`\`\`
|
|
258
|
+
|
|
259
|
+
| Avant | Après |
|
|
260
|
+
| -------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
|
261
|
+
| \`optional\` / \`optionalText="…"\` | retirer ; marquer à l'inverse les champs obligatoires : \`<FormControl required>\` |
|
|
262
|
+
| \`<FormControlLabel error="…" success="…" />\` | retirer \`error\`/\`success\` (le libellé ne se colore plus) ; message via \`FormControl error\` |
|
|
263
|
+
| \`<RadioGroup label=… optional />\` | retirer \`optional\` (\`label\` devient facultatif) |`,
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
titre: '3. DateInput, TimeInput, DurationInput : libellé retiré',
|
|
267
|
+
markdown: `\`\`\`bash
|
|
268
|
+
git grep -nE "<(DateInput|TimeInput|DurationInput)\\b"
|
|
269
|
+
\`\`\`
|
|
270
|
+
|
|
271
|
+
Ces composants existent toujours mais n'acceptent plus \`label\`, \`labelRight\`, \`hint\` (ni la légende
|
|
272
|
+
\`error\`/\`success\` affichée sous le champ pour \`DateInput\`). Même règle : on les entoure.
|
|
273
|
+
|
|
274
|
+
\`\`\`tsx
|
|
275
|
+
// Avant
|
|
276
|
+
<DateInput label="Date de naissance" hint="JJ/MM/AAAA" value={date} onChange={setDate} error={err} />
|
|
277
|
+
<TimeInput label="Heure" value={heure} onChange={setHeure} />
|
|
278
|
+
|
|
279
|
+
// Après — onChange inchangé
|
|
280
|
+
<FormControl label="Date de naissance" hint="JJ/MM/AAAA" error={err}>
|
|
281
|
+
<DateInput value={date} onChange={setDate} error={err} />
|
|
282
|
+
</FormControl>
|
|
283
|
+
<FormControl label="Heure">
|
|
284
|
+
<TimeInput value={heure} onChange={setHeure} />
|
|
285
|
+
</FormControl>
|
|
286
|
+
\`\`\``,
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
titre: '4. Select, SelectMultiple, Autocomplete, AutocompleteAddress → Select',
|
|
290
|
+
markdown: `\`\`\`bash
|
|
291
|
+
git grep -nE "\\b(Select|SelectMultiple|SelectInput|NativePicker|Autocomplete|AutocompleteChip|AutocompleteAddress|AutocompleteOption\\w*|SelectMultipleOption)\\b"
|
|
292
|
+
\`\`\`
|
|
293
|
+
|
|
294
|
+
Le nouveau \`Select\` couvre tous les cas : simple, multiple, recherche (locale ou distante),
|
|
295
|
+
création. Il est **nu** (dans un \`FormControl\`) et **strictement contrôlé**.
|
|
296
|
+
|
|
297
|
+
Types exportés : \`SelectOption\` (\`{ value: string; label: string; leading?; group?; disabled? }\`),
|
|
298
|
+
\`SelectProps\`, \`SelectRef\`, \`SelectItem\`. Supprimés : \`NativePicker\`, \`SelectInput\`,
|
|
299
|
+
\`SelectMultipleOption\`, \`AutocompleteOption*\`, \`isAutocompleteOptionWithGroup*\`.
|
|
300
|
+
|
|
301
|
+
### 4.1 \`Select\` simple
|
|
302
|
+
|
|
303
|
+
\`\`\`tsx
|
|
304
|
+
// Avant
|
|
305
|
+
<Select label="Pays" hint="…" options={pays} value={code} onChange={setCode} error={err} />
|
|
306
|
+
|
|
307
|
+
// Après
|
|
308
|
+
<FormControl label="Pays" hint="…" error={err}>
|
|
309
|
+
<Select options={pays} value={code} onChange={setCode} error={err} />
|
|
310
|
+
</FormControl>
|
|
311
|
+
\`\`\`
|
|
312
|
+
|
|
313
|
+
\`value: string | null\`, \`onChange(value: string | null)\` : inchangés. Ajouter \`clearable\` si
|
|
314
|
+
l'utilisateur doit pouvoir revenir à « aucune valeur ». Le titre du bottom sheet mobile est par
|
|
315
|
+
défaut le \`label\` du \`FormControl\` (sinon \`sheetTitle\`).
|
|
316
|
+
|
|
317
|
+
### 4.2 \`SelectMultiple\` → \`Select multiple\`
|
|
318
|
+
|
|
319
|
+
\`\`\`tsx
|
|
320
|
+
// Avant
|
|
321
|
+
<SelectMultiple label="Rôles" options={roles} value={ids} onChange={setIds} />
|
|
322
|
+
|
|
323
|
+
// Après
|
|
324
|
+
<FormControl label="Rôles">
|
|
325
|
+
<Select multiple options={roles} value={ids} onChange={setIds} />
|
|
326
|
+
</FormControl>
|
|
327
|
+
\`\`\`
|
|
328
|
+
|
|
329
|
+
Piège : \`value\` doit être un \`string[]\` (plus de \`number\`) et **obligatoire** (passer \`[]\`, pas
|
|
330
|
+
\`undefined\`). Les \`value\` numériques des options deviennent des chaînes :
|
|
331
|
+
\`options.map(o => ({ ...o, value: String(o.value) }))\`, et reconvertir dans \`onChange\` si besoin.
|
|
332
|
+
|
|
333
|
+
### 4.3 \`Autocomplete\` → \`Select\`
|
|
334
|
+
|
|
335
|
+
| Autocomplete | Select |
|
|
336
|
+
| ----------------------------- | --------------------------------------------------------------------------------------------- |
|
|
337
|
+
| \`value: AutocompleteOption[]\` | \`value: string \\| null\` (simple) / \`string[]\` (\`multiple\`) — **des valeurs, plus des objets** |
|
|
338
|
+
| \`isMulti\` | \`multiple\` |
|
|
339
|
+
| \`isSearchable\` | \`searchable\` |
|
|
340
|
+
| \`disabledFilterSearch\` | \`localFilter={false}\` |
|
|
341
|
+
| \`disabledSearch\` | retirer \`searchable\` |
|
|
342
|
+
| \`allowEmpty\` | \`clearable\` |
|
|
343
|
+
| \`allowCreate\` | \`creatable\` (implique \`searchable\`) |
|
|
344
|
+
| \`onCreateOption(option)\` | \`onCreateOption(query: string)\` — l'appelant crée l'option |
|
|
345
|
+
| \`createOptionLabel\` | \`createLabel\` |
|
|
346
|
+
| \`createOptionValue\` | supprimé : l'appelant fabrique \`value\` dans \`onCreateOption\` |
|
|
347
|
+
| \`onChange(options, meta)\` | \`onChange(value)\` — plus de second argument \`meta\` |
|
|
348
|
+
| \`onSearchChange\` | \`onSearchChange\` (émis après 300 ms, jamais au montage) |
|
|
349
|
+
| \`label\`, \`hint\`, \`error\` | sur \`FormControl\` (et \`error\` aussi sur \`Select\`) |
|
|
350
|
+
| \`autoFocus\` | \`ref\` (\`SelectRef.open()\` / \`focus()\`) dans un \`useEffect\` |
|
|
351
|
+
| option \`{ group }\` | inchangé : options consécutives de même \`group\` regroupées |
|
|
352
|
+
|
|
353
|
+
Conversion de la valeur, le point qui demande le plus d'attention :
|
|
354
|
+
|
|
355
|
+
\`\`\`tsx
|
|
356
|
+
// Avant : l'état contenait des objets option
|
|
357
|
+
const [selected, setSelected] = useState<AutocompleteOption[]>([]);
|
|
358
|
+
<Autocomplete label="Client" isSearchable options={clients} value={selected} onChange={setSelected} />;
|
|
359
|
+
// lecture : selected[0]?.value
|
|
360
|
+
|
|
361
|
+
// Après : l'état contient la valeur
|
|
362
|
+
const [clientId, setClientId] = useState<string | null>(null);
|
|
363
|
+
<FormControl label="Client">
|
|
364
|
+
<Select searchable options={clients} value={clientId} onChange={setClientId} />
|
|
365
|
+
</FormControl>;
|
|
366
|
+
// lecture : clientId
|
|
367
|
+
// besoin du libellé : clients.find(c => c.value === clientId)?.label
|
|
368
|
+
\`\`\`
|
|
369
|
+
|
|
370
|
+
\`\`\`tsx
|
|
371
|
+
// Avant : multi + création
|
|
372
|
+
<Autocomplete
|
|
373
|
+
label="Étiquettes"
|
|
374
|
+
isMulti
|
|
375
|
+
allowCreate
|
|
376
|
+
options={tags}
|
|
377
|
+
value={selected}
|
|
378
|
+
onChange={(value, meta) => {
|
|
379
|
+
setSelected(value);
|
|
380
|
+
meta?.created.forEach(save);
|
|
381
|
+
}}
|
|
382
|
+
/>;
|
|
383
|
+
|
|
384
|
+
// Après
|
|
385
|
+
const [tags, setTags] = useState<SelectOption[]>(initialTags);
|
|
386
|
+
const [values, setValues] = useState<string[]>([]);
|
|
387
|
+
<FormControl label="Étiquettes">
|
|
388
|
+
<Select
|
|
389
|
+
multiple
|
|
390
|
+
creatable
|
|
391
|
+
options={tags}
|
|
392
|
+
value={values}
|
|
393
|
+
onChange={setValues}
|
|
394
|
+
onCreateOption={query => {
|
|
395
|
+
// obligatoire : ajouter l'option PUIS la sélectionner, sinon rien ne s'affiche
|
|
396
|
+
setTags(current => [...current, { label: query, value: query }]);
|
|
397
|
+
setValues(current => [...current, query]);
|
|
398
|
+
save(query);
|
|
399
|
+
}}
|
|
400
|
+
/>
|
|
401
|
+
</FormControl>;
|
|
402
|
+
\`\`\`
|
|
403
|
+
|
|
404
|
+
Recherche distante (anciennement \`disabledFilterSearch\` + \`onSearchChange\`) :
|
|
405
|
+
|
|
406
|
+
\`\`\`tsx
|
|
407
|
+
<FormControl label="Recherche">
|
|
408
|
+
<Select
|
|
409
|
+
searchable
|
|
410
|
+
localFilter={false}
|
|
411
|
+
loading={loading}
|
|
412
|
+
emptyMessage="Saisir au moins 3 caractères"
|
|
413
|
+
options={resultats}
|
|
414
|
+
value={value}
|
|
415
|
+
onChange={setValue}
|
|
416
|
+
onSearchChange={rechercher}
|
|
417
|
+
/>
|
|
418
|
+
</FormControl>
|
|
419
|
+
\`\`\`
|
|
420
|
+
|
|
421
|
+
### 4.4 \`AutocompleteAddress\` → \`Select\`
|
|
422
|
+
|
|
423
|
+
| AutocompleteAddress | Select |
|
|
424
|
+
| -------------------------- | --------------------------------------- |
|
|
425
|
+
| \`suggestions\` | \`options\` |
|
|
426
|
+
| \`onPlaceSelected(placeId)\` | \`onChange(value)\` avec \`value !== null\` |
|
|
427
|
+
| \`onCustomAddress(address)\` | \`onCreateOption(query)\` |
|
|
428
|
+
| \`onClear()\` | \`onChange(null)\` (avec \`clearable\`) |
|
|
429
|
+
| \`disabledCreation\` | retirer \`creatable\` |
|
|
430
|
+
| \`label\` (défaut "Adresse") | \`FormControl label="Adresse"\` |
|
|
431
|
+
|
|
432
|
+
\`\`\`tsx
|
|
433
|
+
// Avant
|
|
434
|
+
<AutocompleteAddress
|
|
435
|
+
value={value}
|
|
436
|
+
suggestions={suggestions}
|
|
437
|
+
error={err}
|
|
438
|
+
onSearchChange={search}
|
|
439
|
+
onPlaceSelected={placeId => selectPlace(placeId)}
|
|
440
|
+
onCustomAddress={address => setCustom(address)}
|
|
441
|
+
onClear={reset}
|
|
442
|
+
/>
|
|
443
|
+
|
|
444
|
+
// Après
|
|
445
|
+
<FormControl label="Adresse" error={err}>
|
|
446
|
+
<Select
|
|
447
|
+
placeholder="ex : 3 rue Robespierre"
|
|
448
|
+
searchable
|
|
449
|
+
localFilter={false}
|
|
450
|
+
creatable
|
|
451
|
+
clearable
|
|
452
|
+
emptyMessage="Saisir au moins 3 caractères"
|
|
453
|
+
createLabel={query => \`Utiliser « \${query} »\`}
|
|
454
|
+
options={suggestions}
|
|
455
|
+
value={placeId}
|
|
456
|
+
error={err}
|
|
457
|
+
onSearchChange={search}
|
|
458
|
+
onChange={value => (value === null ? reset() : selectPlace(value))}
|
|
459
|
+
onCreateOption={address => {
|
|
460
|
+
setSuggestions(current => [...current, { label: address, value: address }]);
|
|
461
|
+
setPlaceId(address);
|
|
462
|
+
setCustom(address);
|
|
463
|
+
}}
|
|
464
|
+
/>
|
|
465
|
+
</FormControl>
|
|
466
|
+
\`\`\`
|
|
467
|
+
|
|
468
|
+
Attention : quand la recherche distante remplace \`options\`, l'option sélectionnée doit rester dans
|
|
469
|
+
\`options\`, sinon son libellé n'est plus affiché dans le champ fermé. Garder l'option retenue en tête
|
|
470
|
+
de la liste : \`options={[...selectedOption ? [selectedOption] : [], ...suggestions]}\`.`,
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
titre: '5. Icônes → leading / trailing',
|
|
474
|
+
markdown: `Détail et justification : [leading-trailing.md](https://github.com/Captive-Studio/alveole-js/blob/main/docs/migrations/leading-trailing.md).
|
|
475
|
+
|
|
476
|
+
\`\`\`bash
|
|
477
|
+
git grep -nE "startIcon|endIcon|\\bicon[=:]"
|
|
478
|
+
\`\`\`
|
|
479
|
+
|
|
480
|
+
| Composant | Avant | Après |
|
|
481
|
+
| ------------------------------------- | ----------------------- | ---------------------- |
|
|
482
|
+
| \`Button\` | \`startIcon\` / \`endIcon\` | \`leading\` / \`trailing\` |
|
|
483
|
+
| \`AccordionItem\` | \`startIcon\` | \`leading\` |
|
|
484
|
+
| \`Tag\` | \`icon\` | \`leading\` |
|
|
485
|
+
| \`Tabs\` (élément de \`tabs\`), \`TabsTab\` | \`icon\` | \`leading\` |
|
|
486
|
+
| \`SidebarItem\` | \`icon\` | \`leading\` |
|
|
487
|
+
| \`ActionMenu.Item\` | \`icon\` | \`leading\` |
|
|
488
|
+
| \`RadioGroupCard\` | \`icon\` | \`leading\` |
|
|
489
|
+
| \`SelectOption\` | \`{ icon }\` | \`{ leading }\` |
|
|
490
|
+
| \`ToastView\`, \`toast.present(…, opts)\` | \`icon\` | \`leading\` |
|
|
491
|
+
|
|
492
|
+
**Ne pas toucher** \`icon\` sur \`ButtonIcon\`, \`InputButtonAdornment\`, \`DocumentViewerToolbarButton\`,
|
|
493
|
+
ni \`name\` sur \`LucideIcon\`. Renommage mécanique : la valeur (nom d'icône) ne change pas.
|
|
494
|
+
|
|
495
|
+
\`\`\`tsx
|
|
496
|
+
<Button variant="secondary" title="Filtres" startIcon="Plus" endIcon="ChevronDown" />
|
|
497
|
+
// →
|
|
498
|
+
<Button variant="secondary" title="Filtres" leading="Plus" trailing="ChevronDown" />
|
|
499
|
+
\`\`\`
|
|
500
|
+
|
|
501
|
+
\`leading={null}\` retire l'icône par défaut de \`Toast\`.
|
|
502
|
+
|
|
503
|
+
### Noms d'icônes retirés
|
|
504
|
+
|
|
505
|
+
Le catalogue d'icônes est désormais embarqué dans Alveole. Ces noms ne sont plus acceptés
|
|
506
|
+
(\`name\`, \`icon\`, \`leading\`, \`trailing\`, \`IconProps.name\`…) :
|
|
507
|
+
|
|
508
|
+
| Nom | Remplacement |
|
|
509
|
+
| ---------------------------------------------------------------- | --------------------------------------------------------------- |
|
|
510
|
+
| \`broom\`, \`carton\`, \`cartonOff\`, \`houses\`, \`textSquare\` (Lab) | choisir une icône proche dans le catalogue (story \`LucideIcon\`) |
|
|
511
|
+
| \`Icon\`, \`LucideProvider\`, \`createLucideIcon\`, \`useLucideContext\` | ce n'étaient pas des icônes : retirer |
|
|
512
|
+
|
|
513
|
+
\`\`\`bash
|
|
514
|
+
git grep -nE "['\\"](broom|carton|cartonOff|houses|textSquare|Icon|LucideProvider|createLucideIcon|useLucideContext)['\\"]"
|
|
515
|
+
\`\`\``,
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
titre: '6. Button, ButtonIcon, Badge',
|
|
519
|
+
markdown: `\`\`\`bash
|
|
520
|
+
git grep -nE "variant=['\\"]link['\\"]|size=['\\"]xs['\\"]|\\b(noPadding|borderNone|leftAlign)\\b|<Button\\b[^>]*\\bactive\\b"
|
|
521
|
+
\`\`\`
|
|
522
|
+
|
|
523
|
+
| Avant | Après |
|
|
524
|
+
| ------------------------------------------------------------------ | -------------------------------------------------------------------------------- |
|
|
525
|
+
| \`<Button size="xs" …>\` | \`size="sm"\` |
|
|
526
|
+
| \`<Button variant="link" title="…" onPress={go} />\` qui **navigue** | \`<Link href="/chemin">…</Link>\` (ou \`A\`) |
|
|
527
|
+
| \`<Button variant="link" …>\` qui **agit** sur place | \`variant="tertiary"\` |
|
|
528
|
+
| \`<Button leftAlign …>\` | \`contentAlign="start"\` |
|
|
529
|
+
| \`<Button noPadding …>\`, \`<Button borderNone …>\` | retirer ; si l'effet était voulu : \`variant="tertiary"\` ou un \`Pressable\` maison |
|
|
530
|
+
| \`<Button active={open} …>\` | \`expanded={open}\` (\`active\` est déprécié, pas encore supprimé) |
|
|
531
|
+
| \`<Button startIcon="X" />\` **sans \`title\`** | \`<ButtonIcon variant="…" icon="X" accessibilityLabel="Action décrite" />\` |
|
|
532
|
+
| \`<ButtonIcon icon="X" />\` sans \`accessibilityLabel\` | ajouter \`accessibilityLabel="…"\` (obligatoire) |
|
|
533
|
+
| \`<Badge variant="disabled" …>\` | \`variant="default"\` |
|
|
534
|
+
|
|
535
|
+
\`title\` est désormais **obligatoire** sur \`Button\` : toute erreur « Property 'title' is missing »
|
|
536
|
+
est un bouton-icône à convertir en \`ButtonIcon\`. \`ButtonIcon\` accepte seulement les variantes
|
|
537
|
+
\`primary\`, \`secondary\`, \`tertiary\`.
|
|
538
|
+
|
|
539
|
+
\`\`\`tsx
|
|
540
|
+
// Avant
|
|
541
|
+
<Button variant="tertiary" startIcon="Trash" onPress={remove} />
|
|
542
|
+
<Button variant="link" title="Voir le dossier" onPress={() => router.push(\`/dossiers/\${id}\`)} />
|
|
543
|
+
|
|
544
|
+
// Après
|
|
545
|
+
<ButtonIcon variant="tertiary" icon="Trash" accessibilityLabel="Supprimer" onPress={remove} />
|
|
546
|
+
<Link href={\`/dossiers/\${id}\`}>Voir le dossier</Link>
|
|
547
|
+
\`\`\`
|
|
548
|
+
|
|
549
|
+
\`Link\` n'accepte qu'une chaîne en \`children\`, et \`href\` + \`target\` (\`target="_blank"\` pour un lien
|
|
550
|
+
externe dans un nouvel onglet web / navigateur intégré en natif).`,
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
titre: '6 bis. Tag, ToolbarTop',
|
|
554
|
+
markdown: `\`\`\`bash
|
|
555
|
+
git grep -nE "<Tag\\b[^>]*\\bcolor=|\\b(onActions|actionsIcon)\\b|onNavigate="
|
|
556
|
+
\`\`\`
|
|
557
|
+
|
|
558
|
+
| Avant | Après |
|
|
559
|
+
| ------------------------------------------ | --------------------------------------------------------------------------------------------- |
|
|
560
|
+
| \`<Tag color="default" …>\` | retirer \`color\` (le Tag est toujours gris) |
|
|
561
|
+
| \`<Tag color="action" …>\` | une couleur porteuse de sens → \`Badge\` ; un compteur → \`Counter\` |
|
|
562
|
+
| \`ToolbarTop onActions={…} actionsIcon="X"\` | supprimés : passer le bouton dans \`actions\` |
|
|
563
|
+
| \`ToolbarTop onNavigate={…}\` | \`navigationLabel\` devient **obligatoire** avec \`onNavigate\` (nom accessible du bouton retour) |
|
|
564
|
+
|
|
565
|
+
\`\`\`tsx
|
|
566
|
+
// Avant
|
|
567
|
+
<ToolbarTop title="Dossier" onNavigate={back} onActions={openMenu} actionsIcon="EllipsisVertical" />
|
|
568
|
+
|
|
569
|
+
// Après
|
|
570
|
+
<ToolbarTop
|
|
571
|
+
title="Dossier"
|
|
572
|
+
onNavigate={back}
|
|
573
|
+
navigationLabel="Retour"
|
|
574
|
+
actions={<ButtonIcon variant="tertiary" icon="EllipsisVertical" accessibilityLabel="Plus d'actions" onPress={openMenu} />}
|
|
575
|
+
/>
|
|
576
|
+
\`\`\`
|
|
577
|
+
|
|
578
|
+
Nouveautés du \`Tag\`, si utiles : \`selected\`, \`interactive\`, \`closable\` + \`onClose\`, \`leading\`.`,
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
titre: '7. @alveole/theme',
|
|
582
|
+
markdown: `\`\`\`bash
|
|
583
|
+
git grep -nE "\\b(Mandarine|Shakespeare|Pacifika|ButteryWhite|CoralRed|Lochmara|BigStone|LimeGreen|Purple)\\b|Inter|Barlow|generateDefaultThemeCSS|generateFontSmoothingCSS|color-(sun|950)\\b|illustration"
|
|
584
|
+
\`\`\`
|
|
585
|
+
|
|
586
|
+
| Avant | Après |
|
|
587
|
+
| ------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
588
|
+
| Rampes \`Mandarine\`, \`Shakespeare\`, \`Pacifika\`, \`ButteryWhite\`, \`CoralRed\`, \`Lochmara\`, \`BigStone\`, \`LimeGreen\`, \`Purple\` | supprimées : choisir la rampe restante la plus proche (ou copier la valeur hex localement) |
|
|
589
|
+
| Polices Inter (corps) et Barlow | tout est en **Geist** : \`'Inter-Light' / 'Inter-Regular' / 'Inter-Medium' / 'Inter-Bold'\` et \`'Barlow-*'\` → \`'Geist-Light' / 'Geist-Regular' / 'Geist-Medium' / 'Geist-Bold'\` (clés de \`FontsMap\`, \`FontWeightMap\`, \`fontStyle(…)\`) ; retirer \`@expo-google-fonts/inter\` / \`barlow\` si plus importés |
|
|
590
|
+
| Palette \`illustration\` imbriquée (\`illustration["color-sun"].default.yellow\`, idem \`color-950\`) | clés aplaties : \`color-sun-default-yellow\`, \`color-950-default-yellow\` |
|
|
591
|
+
| \`generateDefaultThemeCSS()\` | \`generateThemeCSSParts(theme).join('\\n')\` |
|
|
592
|
+
| \`generateFontSmoothingCSS()\` | supprimé : plus d'antialiasing global. Le recréer côté app seulement si nécessaire. |
|
|
593
|
+
| \`injectVariableCSS(theme: Theme)\`, \`generateCSSVariables(theme: Theme)\` | même appel ; le paramètre est typé \`SourceDuCSS\` (un \`Theme\` reste accepté s'il fournit les mêmes clés) |
|
|
594
|
+
| \`Radius\` / \`Box.borderRadius\` : \`number\` | \`number \\| 'var(--radius-…)'\` sur web : ne plus faire de calcul sur un rayon du thème |
|
|
595
|
+
|
|
596
|
+
Les clés de spacing dépréciées existent toujours (non cassant) mais leurs usages sont à migrer vers
|
|
597
|
+
l'échelle V/W quand on y passe.`,
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
titre: '8. Vérification finale',
|
|
601
|
+
markdown: `\`\`\`bash
|
|
602
|
+
git grep -nE "\\b(TextField|TextareaField|EmailField|EmailInput|PasswordField|PhoneField|PhoneInput|NumberField|OtpField|FileField|SelectMultiple|Autocomplete\\w*|NativePicker|SelectInput)\\b|startIcon|endIcon|variant=['\\"]link['\\"]|size=['\\"]xs['\\"]|noPadding|borderNone|leftAlign|\\boptional(Text)?[=:]|onActions|actionsIcon|<Tag\\b[^>]*\\bcolor=|Inter-|Barlow-"
|
|
603
|
+
\`\`\`
|
|
604
|
+
|
|
605
|
+
Doit ne rien renvoyer. Puis : typecheck, lint, tests. Points à contrôler à l'œil :
|
|
606
|
+
|
|
607
|
+
- chaque champ en erreur a **le message** (\`FormControl error\`) **et la bordure rouge** (\`error\` du contrôle) ;
|
|
608
|
+
- aucun \`TextInput\` avec \`onChange=\` (doit être \`onChangeText=\`) :
|
|
609
|
+
\`git grep -nE "<(TextInput|TextareaInput)\\b[^>]*\\bonChange="\` ;
|
|
610
|
+
- les \`Select\` multiples et créables affichent bien la valeur sélectionnée après création.`,
|
|
611
|
+
},
|
|
612
|
+
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alveole/storybook",
|
|
3
|
-
"version": "2.0.0-alpha",
|
|
3
|
+
"version": "2.0.0-alpha.2",
|
|
4
4
|
"description": "Catalog screens and helpers for Alveole stories and theme tokens.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@alveole/eslint-config": "file:../eslint-config",
|
|
22
22
|
"@testing-library/react": "16.3.3",
|
|
23
|
-
"babel-preset-expo": "57.0.
|
|
23
|
+
"babel-preset-expo": "57.0.14",
|
|
24
24
|
"eslint": "10.10.0",
|
|
25
25
|
"jest": "29.7.0",
|
|
26
26
|
"jest-environment-jsdom": "30.5.2",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"react": "19.2.3",
|
|
29
29
|
"react-dom": "19.2.3",
|
|
30
30
|
"react-native": "0.86.3",
|
|
31
|
-
"react-native-web": "0.21.
|
|
31
|
+
"react-native-web": "0.21.4",
|
|
32
32
|
"tamagui": "1.144.4"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
@@ -39,9 +39,10 @@
|
|
|
39
39
|
"react-native": "*"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
|
-
"build": "tsc -p tsconfig.build.json",
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
42
|
+
"build": "pnpm run generate:sources && tsc -p tsconfig.build.json",
|
|
43
|
+
"generate:sources": "node ../components/scripts/generate-stories-sources.mjs",
|
|
44
|
+
"lint": "pnpm run generate:sources && eslint . --cache --cache-strategy content",
|
|
45
|
+
"typecheck": "pnpm run generate:sources && tsc -p tsconfig.typecheck.json --noEmit",
|
|
46
|
+
"test:unit": "pnpm run generate:sources && npx jest --coverage --maxWorkers=$(node ../../scripts/print-allocated-cpus.mjs) --ci"
|
|
46
47
|
}
|
|
47
48
|
}
|