@sia-ui/cli 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/LICENSE +21 -21
  2. package/dist/index.js +2 -0
  3. package/dist/registry/package/components/Alert/index.tsx +7 -0
  4. package/dist/registry/package/components/AmountDisplay/index.tsx +8 -0
  5. package/dist/registry/package/components/AppShell/index.tsx +413 -3
  6. package/dist/registry/package/components/AppShell/styles.css +259 -7
  7. package/dist/registry/package/components/AppShell/tab-bar.tsx +134 -0
  8. package/dist/registry/package/components/AsyncState/index.tsx +7 -0
  9. package/dist/registry/package/components/AuditMeta/index.tsx +8 -0
  10. package/dist/registry/package/components/AuthLayout/index.tsx +7 -0
  11. package/dist/registry/package/components/Autocomplete/index.tsx +7 -0
  12. package/dist/registry/package/components/Avatar/index.tsx +333 -12
  13. package/dist/registry/package/components/Avatar/styles.css +236 -5
  14. package/dist/registry/package/components/Badge/index.tsx +7 -0
  15. package/dist/registry/package/components/Button/index.tsx +7 -0
  16. package/dist/registry/package/components/Calendar/index.tsx +11 -0
  17. package/dist/registry/package/components/Card/index.tsx +8 -0
  18. package/dist/registry/package/components/Checkbox/index.tsx +8 -0
  19. package/dist/registry/package/components/ColorPicker/index.tsx +7 -0
  20. package/dist/registry/package/components/CommandPalette/index.tsx +7 -0
  21. package/dist/registry/package/components/ConfirmDialog/index.tsx +7 -0
  22. package/dist/registry/package/components/Container/index.tsx +7 -0
  23. package/dist/registry/package/components/Countdown/index.tsx +7 -0
  24. package/dist/registry/package/components/CrudPage/dialogs.tsx +230 -0
  25. package/dist/registry/package/components/CrudPage/index.tsx +546 -5
  26. package/dist/registry/package/components/CrudPage/styles.css +25 -1
  27. package/dist/registry/package/components/CurrencyInput/index.tsx +7 -0
  28. package/dist/registry/package/components/DataTable/index.tsx +548 -200
  29. package/dist/registry/package/components/DataTable/query.ts +68 -0
  30. package/dist/registry/package/components/DataTable/row-actions.tsx +158 -0
  31. package/dist/registry/package/components/DataTable/styles.css +285 -73
  32. package/dist/registry/package/components/DataTable/types.ts +194 -0
  33. package/dist/registry/package/components/DataTable/values.ts +28 -0
  34. package/dist/registry/package/components/DatePicker/index.tsx +7 -0
  35. package/dist/registry/package/components/DateRangeFilter/index.tsx +7 -0
  36. package/dist/registry/package/components/DateRangePicker/index.tsx +7 -0
  37. package/dist/registry/package/components/DateTimePicker/index.tsx +7 -0
  38. package/dist/registry/package/components/Descriptions/index.tsx +7 -0
  39. package/dist/registry/package/components/Divider/index.tsx +7 -0
  40. package/dist/registry/package/components/Drawer/index.tsx +8 -0
  41. package/dist/registry/package/components/DurationDisplay/index.tsx +6 -0
  42. package/dist/registry/package/components/EmptyState/index.tsx +7 -0
  43. package/dist/registry/package/components/EntityMeta/index.tsx +7 -0
  44. package/dist/registry/package/components/ErrorState/index.tsx +8 -0
  45. package/dist/registry/package/components/EventCalendar/index.tsx +7 -0
  46. package/dist/registry/package/components/Field/index.tsx +12 -0
  47. package/dist/registry/package/components/FileUpload/index.tsx +7 -0
  48. package/dist/registry/package/components/FiltersBar/index.tsx +7 -0
  49. package/dist/registry/package/components/HoverCard/index.tsx +14 -37
  50. package/dist/registry/package/components/IconButton/index.tsx +8 -0
  51. package/dist/registry/package/components/Icons/index.tsx +15 -0
  52. package/dist/registry/package/components/ImageUpload/index.tsx +7 -0
  53. package/dist/registry/package/components/Input/index.tsx +7 -0
  54. package/dist/registry/package/components/JsonEditor/index.tsx +7 -0
  55. package/dist/registry/package/components/KpiGrid/index.tsx +7 -0
  56. package/dist/registry/package/components/Label/index.tsx +7 -0
  57. package/dist/registry/package/components/MarkdownEditor/index.tsx +7 -0
  58. package/dist/registry/package/components/MiniCalendar/index.tsx +7 -0
  59. package/dist/registry/package/components/Modal/index.tsx +8 -0
  60. package/dist/registry/package/components/MonthPicker/index.tsx +7 -0
  61. package/dist/registry/package/components/MultiSelect/index.tsx +7 -0
  62. package/dist/registry/package/components/OtpInput/index.tsx +7 -0
  63. package/dist/registry/package/components/Overlay/index.tsx +34 -24
  64. package/dist/registry/package/components/PageHeader/index.tsx +7 -0
  65. package/dist/registry/package/components/PermissionGate/index.tsx +6 -0
  66. package/dist/registry/package/components/Popover/index.tsx +7 -0
  67. package/dist/registry/package/components/QrCode/index.tsx +7 -0
  68. package/dist/registry/package/components/RadioGroup/index.tsx +7 -0
  69. package/dist/registry/package/components/Rating/index.tsx +6 -0
  70. package/dist/registry/package/components/ReferenceSelect/index.tsx +7 -0
  71. package/dist/registry/package/components/RelativeTime/index.tsx +6 -0
  72. package/dist/registry/package/components/ResizablePanel/index.tsx +7 -0
  73. package/dist/registry/package/components/Resource/index.tsx +514 -0
  74. package/dist/registry/package/components/RichTextEditor/index.tsx +7 -0
  75. package/dist/registry/package/components/SearchInput/index.tsx +8 -0
  76. package/dist/registry/package/components/Select/index.tsx +11 -0
  77. package/dist/registry/package/components/Sidebar/index.tsx +489 -113
  78. package/dist/registry/package/components/Sidebar/styles.css +96 -4
  79. package/dist/registry/package/components/Skeleton/index.tsx +7 -0
  80. package/dist/registry/package/components/Spinner/index.tsx +7 -0
  81. package/dist/registry/package/components/StatCard/index.tsx +7 -0
  82. package/dist/registry/package/components/Statistic/index.tsx +7 -0
  83. package/dist/registry/package/components/Switch/index.tsx +7 -0
  84. package/dist/registry/package/components/Tabs/index.tsx +7 -0
  85. package/dist/registry/package/components/TagsInput/index.tsx +7 -0
  86. package/dist/registry/package/components/Textarea/index.tsx +7 -0
  87. package/dist/registry/package/components/TimePicker/index.tsx +7 -0
  88. package/dist/registry/package/components/TimeRangePicker/index.tsx +6 -0
  89. package/dist/registry/package/components/Timeline/index.tsx +7 -0
  90. package/dist/registry/package/components/Timer/index.tsx +7 -0
  91. package/dist/registry/package/components/Tour/index.tsx +7 -0
  92. package/dist/registry/package/components/Transfer/index.tsx +7 -0
  93. package/dist/registry/package/components/Tree/index.tsx +7 -0
  94. package/dist/registry/package/components/TreeSelect/index.tsx +7 -0
  95. package/dist/registry/package/components/Typography/index.tsx +8 -0
  96. package/dist/registry/package/components/UnsavedChangesGuard/index.tsx +8 -0
  97. package/dist/registry/package/components/WeekPicker/index.tsx +7 -0
  98. package/dist/registry/package/components/YearPicker/index.tsx +7 -0
  99. package/dist/registry/registry.json +69 -14
  100. package/package.json +14 -14
@@ -4,4 +4,11 @@ import "./styles.css";
4
4
 
5
5
  export interface CommandItem { id: string; label: string; description?: string; keywords?: string[]; icon?: ReactNode; disabled?: boolean; onSelect: () => void; }
6
6
  export interface CommandPaletteProps { open: boolean; onOpenChange: (open: boolean) => void; items: CommandItem[]; placeholder?: string; emptyLabel?: string; className?: string; }
7
+ /**
8
+ * Tout ce que l'application sait faire, au clavier.
9
+ *
10
+ * La recherche porte aussi sur des mots-clés invisibles : quelqu'un qui tape
11
+ * « facture » doit trouver « Nouvelle vente » sans avoir appris comment
12
+ * l'action s'appelle ici.
13
+ */
7
14
  export function CommandPalette({ open, onOpenChange, items, placeholder = "Rechercher une commande...", emptyLabel = "Aucune commande", className }: CommandPaletteProps) { const [query, setQuery] = useState(""); const [active, setActive] = useState(0); const inputRef = useRef<HTMLInputElement>(null); const filtered = useMemo(() => { const needle = query.trim().toLowerCase(); return items.filter((item) => !needle || [item.label, item.description, ...(item.keywords ?? [])].filter(Boolean).join(" ").toLowerCase().includes(needle)); }, [items, query]); useEffect(() => { if (!open) return; setQuery(""); setActive(0); requestAnimationFrame(() => inputRef.current?.focus()); }, [open]); if (!open) return null; const select = (item: CommandItem | undefined) => { if (!item || item.disabled) return; item.onSelect(); onOpenChange(false); }; return <div className="sia-command-palette__backdrop" onMouseDown={(event) => event.target === event.currentTarget && onOpenChange(false)}><section className={cn("sia-command-palette", className)} role="dialog" aria-modal="true" aria-label="Palette de commandes"><input ref={inputRef} value={query} placeholder={placeholder} aria-label={placeholder} onChange={(event) => { setQuery(event.target.value); setActive(0); }} onKeyDown={(event) => { if (event.key === "Escape") onOpenChange(false); if (event.key === "ArrowDown") { event.preventDefault(); setActive((value) => Math.min(value + 1, filtered.length - 1)); } if (event.key === "ArrowUp") { event.preventDefault(); setActive((value) => Math.max(value - 1, 0)); } if (event.key === "Enter") select(filtered[active]); }} /><div role="listbox">{filtered.length === 0 ? <p>{emptyLabel}</p> : filtered.map((item, index) => <button type="button" role="option" aria-selected={index === active} key={item.id} disabled={item.disabled} onMouseEnter={() => setActive(index)} onClick={() => select(item)}>{item.icon}<span><strong>{item.label}</strong>{item.description && <small>{item.description}</small>}</span></button>)}</div></section></div>; }
@@ -16,6 +16,13 @@ export interface ConfirmDialogProps {
16
16
  onConfirm: () => void | Promise<void>;
17
17
  }
18
18
 
19
+ /**
20
+ * La question qu'on pose avant de détruire.
21
+ *
22
+ * `onConfirm` peut rendre une promesse : le bouton reste occupé tant qu'elle
23
+ * n'a pas abouti. Sans cela, la boîte se referme sur un appel encore en
24
+ * vol, et l'on ignore si la suppression a eu lieu.
25
+ */
19
26
  export function ConfirmDialog({ open, onOpenChange, title, description, confirmLabel: confirmLabelProp, cancelLabel: cancelLabelProp, tone = "primary", loading = false, onConfirm }: ConfirmDialogProps) {
20
27
  const locale = useSiaLocale();
21
28
  const cancelLabel = cancelLabelProp ?? locale.cancel;
@@ -2,6 +2,13 @@ import { type HTMLAttributes } from "react";
2
2
  import { cn } from "@sia-ui/utils";
3
3
  import "./styles.css";
4
4
 
5
+ /**
6
+ * La largeur de lecture, et l'espacement vertical.
7
+ *
8
+ * Deux réglages posés une fois pour toutes plutôt qu'une marge inventée
9
+ * écran par écran : des largeurs décidées page après page finissent par
10
+ * diverger, et personne ne sait plus laquelle fait foi.
11
+ */
5
12
  export function Container({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
6
13
  return <div className={cn("sia-container", className)} {...props} />;
7
14
  }
@@ -1,3 +1,10 @@
1
1
  import { Timer, type TimerProps } from "../Timer";
2
2
  export type CountdownProps = Omit<TimerProps, "direction">;
3
+ /**
4
+ * Un `Timer` qui descend.
5
+ *
6
+ * Le compte à rebours est assez fréquent — une session qui expire, un code
7
+ * à usage unique — pour mériter son nom plutôt qu'une prop à ne pas
8
+ * oublier.
9
+ */
3
10
  export function Countdown(props: CountdownProps) { return <Timer {...props} direction="down" />; }
@@ -0,0 +1,230 @@
1
+ import { type Key, type ReactNode } from "react";
2
+ import type { FormValidator } from "@sia-ui/headless";
3
+ import { Modal } from "../Modal";
4
+ import { Form, type FormEntry, type FormShape } from "../Form";
5
+ import { Descriptions } from "../Descriptions";
6
+ import type { DataTableColumn } from "../DataTable/types";
7
+ import { renduCellule } from "../DataTable/values";
8
+
9
+ /** Laquelle des trois boîtes est ouverte, et sur quelle ligne. */
10
+ export interface CrudDialogState<T> {
11
+ mode: "create" | "edit" | "view";
12
+ row?: T;
13
+ index?: number;
14
+ }
15
+
16
+ /** Les titres et la forme des boîtes. Tout est facultatif. */
17
+ export interface CrudDialogOptions {
18
+ createTitle?: ReactNode;
19
+ createDescription?: ReactNode;
20
+ editTitle?: ReactNode;
21
+ editDescription?: ReactNode;
22
+ viewTitle?: ReactNode;
23
+ viewDescription?: ReactNode;
24
+ submitText?: ReactNode;
25
+ /** Colonnes du formulaire. Deux, au-delà de quatre ou cinq champs. */
26
+ columns?: number;
27
+ /** Colonnes de la vue de détail. */
28
+ detailColumns?: 1 | 2 | 3;
29
+ }
30
+
31
+ /**
32
+ * Les noms de champs, groupes aplatis.
33
+ *
34
+ * Servent à fabriquer un formulaire vide : sans valeur initiale déclarée, un
35
+ * champ non contrôlé le devient à la première frappe, et React s'en plaint à
36
+ * juste titre.
37
+ */
38
+ function nomsDeChamps(fields: FormEntry[]): string[] {
39
+ const noms: string[] = [];
40
+ for (const entree of fields) {
41
+ if ("group" in entree) noms.push(...entree.fields.map((f) => f.name));
42
+ else noms.push(entree.name);
43
+ }
44
+ return noms;
45
+ }
46
+
47
+ export function valeursVides(fields: FormEntry[]): FormShape {
48
+ const valeurs: FormShape = {};
49
+ for (const nom of nomsDeChamps(fields)) valeurs[nom] = "";
50
+ return valeurs;
51
+ }
52
+
53
+ /**
54
+ * Ne garder d'une ligne que ce que le formulaire sait montrer.
55
+ *
56
+ * Une ligne porte souvent plus que ses champs — un identifiant, des dates de
57
+ * suivi, un objet imbriqué. Les passer au formulaire les renverrait tels
58
+ * quels à l'envoi, et `isDirty` les compterait comme modifiés.
59
+ */
60
+ export function valeursDeLigne<T>(row: T, fields: FormEntry[]): FormShape {
61
+ const source = row as Record<string, unknown>;
62
+ const valeurs: FormShape = {};
63
+ for (const nom of nomsDeChamps(fields)) {
64
+ const valeur = source[nom];
65
+ valeurs[nom] =
66
+ typeof valeur === "string" ||
67
+ typeof valeur === "number" ||
68
+ typeof valeur === "boolean" ||
69
+ Array.isArray(valeur)
70
+ ? (valeur as FormShape[string])
71
+ : "";
72
+ }
73
+ return valeurs;
74
+ }
75
+
76
+ export interface CrudDialogsProps<T> {
77
+ state: CrudDialogState<T> | null;
78
+ onClose: () => void;
79
+
80
+ columns: Array<DataTableColumn<T>>;
81
+ fields?: FormEntry[] | undefined;
82
+ validate?: FormValidator<FormShape> | undefined;
83
+ onSubmit?:
84
+ | ((
85
+ values: FormShape,
86
+ context: { mode: "create" | "edit"; row?: T; index?: number },
87
+ ) => void | Promise<void>)
88
+ | undefined;
89
+
90
+ createDefaults?: FormShape | undefined;
91
+ toFormValues?: ((row: T) => FormShape) | undefined;
92
+ renderDetail?: ((row: T, index: number) => ReactNode) | undefined;
93
+
94
+ getRowKey?: ((row: T, index: number) => Key) | undefined;
95
+ dialog?: CrudDialogOptions | undefined;
96
+ }
97
+
98
+ /**
99
+ * Les boîtes du CRUD : créer, modifier, consulter.
100
+ *
101
+ * Elles n'inventent rien — `Modal` pour la boîte, `Form` pour la saisie,
102
+ * `Descriptions` pour la lecture. Ce qu'elles apportent est de savoir
103
+ * laquelle ouvrir, avec quelles valeurs, et de refermer une fois l'envoi
104
+ * passé.
105
+ */
106
+ export function CrudDialogs<T>({
107
+ state,
108
+ onClose,
109
+ columns,
110
+ fields,
111
+ validate,
112
+ onSubmit,
113
+ createDefaults,
114
+ toFormValues,
115
+ renderDetail,
116
+ getRowKey,
117
+ dialog,
118
+ }: CrudDialogsProps<T>) {
119
+ const creation = state?.mode === "create";
120
+ const modification = state?.mode === "edit";
121
+ const consultation = state?.mode === "view";
122
+
123
+ /*
124
+ Une clé qui change d'une ouverture à l'autre.
125
+
126
+ `useLocalForm` fige ses valeurs initiales à la première passe — c'est ce
127
+ qui rend `isDirty` fiable. Sans remonter le formulaire, la deuxième
128
+ modification rouvrirait sur la première ligne.
129
+ */
130
+ const cle =
131
+ state === undefined || state === null
132
+ ? "vide"
133
+ : `${state.mode}-${
134
+ state.row !== undefined && state.index !== undefined && getRowKey
135
+ ? String(getRowKey(state.row, state.index))
136
+ : String(state.index ?? "")
137
+ }`;
138
+
139
+ const valeurs =
140
+ modification && state?.row !== undefined && fields
141
+ ? (toFormValues?.(state.row) ?? valeursDeLigne(state.row, fields))
142
+ : (createDefaults ?? (fields ? valeursVides(fields) : {}));
143
+
144
+ return (
145
+ <>
146
+ {fields && (creation || modification) && (
147
+ <Modal
148
+ open
149
+ onOpenChange={(ouvert) => {
150
+ if (!ouvert) onClose();
151
+ }}
152
+ title={
153
+ creation
154
+ ? (dialog?.createTitle ?? "Créer")
155
+ : (dialog?.editTitle ?? "Modifier")
156
+ }
157
+ {...(creation
158
+ ? dialog?.createDescription
159
+ ? { description: dialog.createDescription }
160
+ : {}
161
+ : dialog?.editDescription
162
+ ? { description: dialog.editDescription }
163
+ : {})}
164
+ className="sia-crud-dialog"
165
+ >
166
+ <Form
167
+ key={cle}
168
+ fields={fields}
169
+ defaultValues={valeurs}
170
+ {...(validate ? { validate } : {})}
171
+ columns={dialog?.columns ?? 1}
172
+ submitText={
173
+ dialog?.submitText ?? (creation ? "Créer" : "Enregistrer")
174
+ }
175
+ // Une création part de champs vides : exiger une modification
176
+ // bloquerait l'envoi d'un formulaire dont tous les défauts
177
+ // conviennent.
178
+ requireDirty={modification}
179
+ onSubmit={async (values) => {
180
+ await onSubmit?.(values, {
181
+ mode: creation ? "create" : "edit",
182
+ ...(state?.row !== undefined ? { row: state.row } : {}),
183
+ ...(state?.index !== undefined ? { index: state.index } : {}),
184
+ });
185
+ onClose();
186
+ }}
187
+ />
188
+ </Modal>
189
+ )}
190
+
191
+ {consultation && state?.row !== undefined && (
192
+ <Modal
193
+ open
194
+ onOpenChange={(ouvert) => {
195
+ if (!ouvert) onClose();
196
+ }}
197
+ title={dialog?.viewTitle ?? "Détail"}
198
+ {...(dialog?.viewDescription
199
+ ? { description: dialog.viewDescription }
200
+ : {})}
201
+ className="sia-crud-dialog"
202
+ >
203
+ {renderDetail?.(state.row, state.index ?? 0) ?? (
204
+ /*
205
+ À défaut de rendu à soi, les colonnes du tableau.
206
+
207
+ Elles disent déjà quoi montrer et comment : un montant y est
208
+ formaté, un statut y est une pastille. Redemander la même chose
209
+ sous une autre forme ferait deux descriptions du même objet.
210
+ */
211
+ <Descriptions
212
+ columns={dialog?.detailColumns ?? 2}
213
+ items={columns
214
+ .filter((colonne) => colonne.card !== "hidden")
215
+ .map((colonne) => ({
216
+ key: colonne.key,
217
+ label: colonne.header,
218
+ value: renduCellule(
219
+ state.row as T,
220
+ state.index ?? 0,
221
+ colonne,
222
+ ),
223
+ }))}
224
+ />
225
+ )}
226
+ </Modal>
227
+ )}
228
+ </>
229
+ );
230
+ }