@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 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';
@@ -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.13",
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.3",
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
- "lint": "eslint . --cache --cache-strategy content",
44
- "typecheck": "tsc -p tsconfig.typecheck.json --noEmit",
45
- "test:unit": "npx jest --coverage --maxWorkers=$(node ../../scripts/print-allocated-cpus.mjs) --ci"
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
  }