@sia-ui/cli 0.7.0 → 0.8.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/CHANGELOG.md +12 -0
  2. package/README.md +12 -0
  3. package/dist/registry/package/components/AccountMenu/index.tsx +107 -0
  4. package/dist/registry/package/components/AccountMenu/styles.css +30 -0
  5. package/dist/registry/package/components/ActivityLog/index.tsx +96 -25
  6. package/dist/registry/package/components/AmountDisplay/index.tsx +5 -2
  7. package/dist/registry/package/components/AppShell/index.tsx +44 -8
  8. package/dist/registry/package/components/AppShell/tab-bar.tsx +6 -3
  9. package/dist/registry/package/components/AsyncState/index.tsx +29 -7
  10. package/dist/registry/package/components/AuditMeta/index.tsx +12 -2
  11. package/dist/registry/package/components/Avatar/index.tsx +4 -9
  12. package/dist/registry/package/components/Badge/index.tsx +3 -2
  13. package/dist/registry/package/components/Badge/styles.css +4 -4
  14. package/dist/registry/package/components/BrandIcon/index.tsx +100 -0
  15. package/dist/registry/package/components/BrandIcon/styles.css +31 -0
  16. package/dist/registry/package/components/Breadcrumb/index.tsx +14 -2
  17. package/dist/registry/package/components/Button/index.tsx +3 -2
  18. package/dist/registry/package/components/Button/styles.css +4 -4
  19. package/dist/registry/package/components/Calendar/index.tsx +7 -3
  20. package/dist/registry/package/components/Card/index.tsx +14 -4
  21. package/dist/registry/package/components/ColorModeToggle/index.tsx +44 -0
  22. package/dist/registry/package/components/CommandPalette/index.tsx +3 -2
  23. package/dist/registry/package/components/ConfirmDialog/index.tsx +24 -4
  24. package/dist/registry/package/components/CrudPage/dialogs.tsx +53 -8
  25. package/dist/registry/package/components/CrudPage/index.tsx +99 -25
  26. package/dist/registry/package/components/CurrencyInput/index.tsx +5 -2
  27. package/dist/registry/package/components/DataTable/index.tsx +53 -18
  28. package/dist/registry/package/components/DataTable/row-actions.tsx +20 -8
  29. package/dist/registry/package/components/DataTable/types.ts +100 -0
  30. package/dist/registry/package/components/DatePicker/index.tsx +14 -6
  31. package/dist/registry/package/components/DateRangeFilter/index.tsx +12 -3
  32. package/dist/registry/package/components/DateRangePicker/index.tsx +13 -4
  33. package/dist/registry/package/components/DateTimePicker/index.tsx +3 -1
  34. package/dist/registry/package/components/Drawer/index.tsx +16 -6
  35. package/dist/registry/package/components/DropdownMenu/index.tsx +37 -4
  36. package/dist/registry/package/components/DropdownMenu/styles.css +10 -0
  37. package/dist/registry/package/components/DurationDisplay/index.tsx +20 -2
  38. package/dist/registry/package/components/EntityMeta/index.tsx +14 -9
  39. package/dist/registry/package/components/ErrorState/index.tsx +9 -3
  40. package/dist/registry/package/components/ErrorState/styles.css +1 -1
  41. package/dist/registry/package/components/EventCalendar/index.tsx +657 -9
  42. package/dist/registry/package/components/EventCalendar/styles.css +360 -8
  43. package/dist/registry/package/components/EventManager/index.tsx +456 -0
  44. package/dist/registry/package/components/EventManager/styles.css +12 -0
  45. package/dist/registry/package/components/Field/index.tsx +74 -27
  46. package/dist/registry/package/components/FileUpload/index.tsx +13 -3
  47. package/dist/registry/package/components/FiltersBar/index.tsx +8 -3
  48. package/dist/registry/package/components/FiltersBar/styles.css +7 -0
  49. package/dist/registry/package/components/Form/index.tsx +118 -14
  50. package/dist/registry/package/components/Form/styles.css +3 -0
  51. package/dist/registry/package/components/HoverCard/index.tsx +8 -4
  52. package/dist/registry/package/components/Icons/index.tsx +18 -0
  53. package/dist/registry/package/components/ImageUpload/index.tsx +17 -2
  54. package/dist/registry/package/components/Input/index.tsx +3 -2
  55. package/dist/registry/package/components/JobProgress/index.tsx +289 -0
  56. package/dist/registry/package/components/JobProgress/styles.css +58 -0
  57. package/dist/registry/package/components/JsonEditor/index.tsx +18 -3
  58. package/dist/registry/package/components/JsonView/index.tsx +282 -0
  59. package/dist/registry/package/components/JsonView/styles.css +99 -0
  60. package/dist/registry/package/components/LiveIndicator/index.tsx +43 -0
  61. package/dist/registry/package/components/LiveIndicator/styles.css +41 -0
  62. package/dist/registry/package/components/LogStream/index.tsx +545 -0
  63. package/dist/registry/package/components/LogStream/styles.css +152 -0
  64. package/dist/registry/package/components/MarkdownEditor/index.tsx +14 -3
  65. package/dist/registry/package/components/Modal/index.tsx +7 -1
  66. package/dist/registry/package/components/MultiSelect/index.tsx +14 -3
  67. package/dist/registry/package/components/OtpInput/index.tsx +2 -1
  68. package/dist/registry/package/components/Pagination/index.tsx +13 -2
  69. package/dist/registry/package/components/PasswordInput/index.tsx +23 -9
  70. package/dist/registry/package/components/PhoneInput/index.tsx +66 -30
  71. package/dist/registry/package/components/Popover/index.tsx +19 -4
  72. package/dist/registry/package/components/QrCode/index.tsx +3 -2
  73. package/dist/registry/package/components/RadioGroup/styles.css +3 -0
  74. package/dist/registry/package/components/Rating/index.tsx +6 -2
  75. package/dist/registry/package/components/ReferenceSelect/index.tsx +8 -1
  76. package/dist/registry/package/components/RelativeTime/index.tsx +5 -1
  77. package/dist/registry/package/components/RequireSession/index.tsx +10 -4
  78. package/dist/registry/package/components/Resource/index.tsx +70 -17
  79. package/dist/registry/package/components/RichTextEditor/index.tsx +2 -1
  80. package/dist/registry/package/components/SearchInput/index.tsx +4 -1
  81. package/dist/registry/package/components/SecretFields/index.tsx +70 -17
  82. package/dist/registry/package/components/Select/index.tsx +44 -12
  83. package/dist/registry/package/components/Select/styles.css +6 -3
  84. package/dist/registry/package/components/Sidebar/index.tsx +22 -6
  85. package/dist/registry/package/components/Slider/index.tsx +26 -5
  86. package/dist/registry/package/components/Spinner/index.tsx +3 -2
  87. package/dist/registry/package/components/StatCard/index.tsx +11 -3
  88. package/dist/registry/package/components/Statistic/index.tsx +5 -1
  89. package/dist/registry/package/components/Steps/styles.css +2 -2
  90. package/dist/registry/package/components/TagsInput/index.tsx +2 -1
  91. package/dist/registry/package/components/TimePicker/index.tsx +10 -4
  92. package/dist/registry/package/components/Toast/index.tsx +20 -5
  93. package/dist/registry/package/components/Tour/index.tsx +2 -1
  94. package/dist/registry/package/components/Transfer/index.tsx +2 -1
  95. package/dist/registry/package/components/Tree/index.tsx +3 -2
  96. package/dist/registry/package/components/TreeSelect/index.tsx +17 -4
  97. package/dist/registry/package/components/UnsavedChangesGuard/index.tsx +34 -3
  98. package/dist/registry/registry.json +257 -75
  99. package/dist/registry/styles/sia-ui.css +23 -4
  100. package/package.json +5 -5
@@ -8,6 +8,7 @@ import {
8
8
  type ReactNode,
9
9
  } from "react";
10
10
  import { cn } from "@sia-ui/utils";
11
+ import { useSiaLocale } from "@sia-ui/headless";
11
12
  import "./styles.css";
12
13
 
13
14
  export type AvatarSize = "xs" | "sm" | "md" | "lg" | "xl";
@@ -22,13 +23,6 @@ export type AvatarVariant = "soft" | "solid" | "outline";
22
23
  */
23
24
  export type AvatarStatus = "online" | "offline" | "busy" | "away";
24
25
 
25
- const LIBELLES_STATUT: Record<AvatarStatus, string> = {
26
- online: "en ligne",
27
- offline: "hors ligne",
28
- busy: "occupé",
29
- away: "absent",
30
- };
31
-
32
26
  /**
33
27
  * Les teintes dérivées du nom.
34
28
  *
@@ -121,7 +115,7 @@ export interface AvatarProps
121
115
  initials?: 1 | 2;
122
116
 
123
117
  status?: AvatarStatus;
124
- /** Le libellé du statut, pour les lecteurs d'écran. */
118
+ /** Le libellé du statut, pour les lecteurs d'écran. Par défaut, celui de la locale. */
125
119
  statusLabel?: string;
126
120
 
127
121
  /** Un liseré qui détache l'avatar de son fond. */
@@ -159,6 +153,7 @@ export function Avatar({
159
153
  style,
160
154
  ...props
161
155
  }: AvatarProps) {
156
+ const libellesStatut = useSiaLocale().avatar;
162
157
  const groupe = useContext(AvatarGroupContext);
163
158
  const [etatImage, setEtatImage] = useState<"attente" | "prete" | "echouee">(
164
159
  src ? "attente" : "echouee",
@@ -227,7 +222,7 @@ export function Avatar({
227
222
  annoncent la personne une seule fois, statut compris. */}
228
223
  <span className="sia-visually-hidden">
229
224
  {name}
230
- {status ? ` — ${statusLabel ?? LIBELLES_STATUT[status]}` : ""}
225
+ {status ? ` — ${statusLabel ?? libellesStatut[status]}` : ""}
231
226
  </span>
232
227
  </>
233
228
  );
@@ -2,7 +2,7 @@ import { type HTMLAttributes, type ReactNode } from "react";
2
2
  import { cn } from "@sia-ui/utils";
3
3
  import { Spinner, type SpinnerProps } from "../Spinner";
4
4
  import type { ComponentTone } from "@sia-ui/tokens";
5
- import { useComponentDefaults } from "@sia-ui/headless";
5
+ import { useComponentDefaults, useSiaLocale } from "@sia-ui/headless";
6
6
  import "./styles.css";
7
7
 
8
8
  export interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
@@ -37,7 +37,8 @@ export function Badge({
37
37
  const defaults = useComponentDefaults<BadgeProps>("badge");
38
38
  const tone = toneProp ?? defaults.tone ?? "neutral";
39
39
  const variant = variantProp ?? defaults.variant ?? "soft";
40
- const loadingLabel = loadingLabelProp ?? defaults.loadingLabel ?? "Chargement";
40
+ const locale = useSiaLocale();
41
+ const loadingLabel = loadingLabelProp ?? defaults.loadingLabel ?? locale.loading;
41
42
  return (
42
43
  <span
43
44
  className={cn(
@@ -6,9 +6,9 @@
6
6
  .sia-badge--soft.sia-badge--danger { color:var(--sia-danger); background:color-mix(in srgb,var(--sia-danger) 12%,transparent); }
7
7
  .sia-badge--solid.sia-badge--neutral { color:var(--sia-background); background:var(--sia-foreground); }
8
8
  .sia-badge--solid.sia-badge--primary { color:var(--sia-primary-foreground); background:var(--sia-primary); }
9
- .sia-badge--solid.sia-badge--success { color:#fff; background:var(--sia-success); }
10
- .sia-badge--solid.sia-badge--warning { color:#fff; background:var(--sia-warning); }
11
- .sia-badge--solid.sia-badge--danger { color:#fff; background:var(--sia-danger); }
9
+ .sia-badge--solid.sia-badge--success { color:var(--sia-success-foreground); background:var(--sia-success); }
10
+ .sia-badge--solid.sia-badge--warning { color:var(--sia-warning-foreground); background:var(--sia-warning); }
11
+ .sia-badge--solid.sia-badge--danger { color:var(--sia-danger-foreground); background:var(--sia-danger); }
12
12
  .sia-badge--outline { border-color:currentColor; background:transparent; }
13
13
  .sia-badge--outline.sia-badge--primary { color:var(--sia-primary); }
14
14
  .sia-badge--outline.sia-badge--success { color:var(--sia-success); }
@@ -21,6 +21,6 @@
21
21
  .sia-badge--loading { cursor:progress; }
22
22
  /* ComponentTone completeness for components created before the shared contract. */
23
23
  .sia-badge--soft.sia-badge--info { color:var(--sia-info); background:color-mix(in srgb,var(--sia-info) 12%,transparent); }
24
- .sia-badge--solid.sia-badge--info { color:#fff; background:var(--sia-info); }
24
+ .sia-badge--solid.sia-badge--info { color:var(--sia-info-foreground); background:var(--sia-info); }
25
25
  .sia-badge--outline.sia-badge--neutral { color:var(--sia-muted); }
26
26
  .sia-badge--outline.sia-badge--info { color:var(--sia-info); }
@@ -0,0 +1,100 @@
1
+ import { type CSSProperties, type SVGAttributes } from "react";
2
+ import { cn } from "@sia-ui/utils";
3
+ import { luminance } from "@sia-ui/tokens";
4
+ import "./styles.css";
5
+
6
+ /**
7
+ * Une icône de marque, dans la forme d'un objet `simple-icons` : `path` est
8
+ * le tracé d'un SVG 24×24, `hex` la couleur de la marque, sans dièse.
9
+ */
10
+ export interface BrandIconData {
11
+ title: string;
12
+ path: string;
13
+ hex: string;
14
+ }
15
+
16
+ export interface BrandIconProps
17
+ extends Omit<SVGAttributes<SVGSVGElement>, "children" | "title"> {
18
+ icon: BrandIconData;
19
+ /** La taille du carré. `1em` par défaut : l'icône suit le texte. */
20
+ size?: number | string;
21
+ /** La couleur de la marque (défaut), ou `currentColor` quand il vaut `false`. */
22
+ colored?: boolean;
23
+ /** Le nom accessible. Par défaut, `icon.title`. */
24
+ title?: string;
25
+ /** Masquée des lecteurs d'écran : l'icône accompagne un texte qui la nomme déjà. */
26
+ decorative?: boolean;
27
+ }
28
+
29
+ /** En dessous, la marque est quasi noire ; au-dessus, quasi blanche. */
30
+ const SOMBRE = 0.08;
31
+ const CLAIRE = 0.85;
32
+
33
+ /**
34
+ * Le logo d'un service tiers — Vercel, Supabase, GitHub — sans dépendance.
35
+ *
36
+ * Le système ne livre aucun logo : il prend l'objet d'icône que le projet
37
+ * importe lui-même, et ne dépend donc ni d'un paquet d'icônes ni de sa
38
+ * version.
39
+ *
40
+ * ```tsx
41
+ * import { siVercel } from "simple-icons";
42
+ * <BrandIcon icon={siVercel} />
43
+ * ```
44
+ *
45
+ * Une marque quasi noire (Vercel, GitHub) disparaîtrait sur un fond sombre,
46
+ * une marque quasi blanche sur un fond clair : sur le thème où elle serait
47
+ * illisible, elle prend la couleur du texte plutôt que la sienne.
48
+ */
49
+ export function BrandIcon({
50
+ icon,
51
+ size = "1em",
52
+ colored = true,
53
+ title,
54
+ decorative = false,
55
+ className,
56
+ style,
57
+ ...props
58
+ }: BrandIconProps) {
59
+ const name = title ?? icon.title;
60
+ const lum = colored ? luminance(icon.hex) : undefined;
61
+ const contrast =
62
+ lum === undefined || Number.isNaN(lum)
63
+ ? undefined
64
+ : lum < SOMBRE
65
+ ? "dark"
66
+ : lum > CLAIRE
67
+ ? "light"
68
+ : undefined;
69
+
70
+ return (
71
+ <svg
72
+ xmlns="http://www.w3.org/2000/svg"
73
+ viewBox="0 0 24 24"
74
+ width={size}
75
+ height={size}
76
+ focusable="false"
77
+ {...(decorative
78
+ ? { "aria-hidden": true }
79
+ : { role: "img", "aria-label": name })}
80
+ {...props}
81
+ className={cn(
82
+ "sia-brand-icon",
83
+ colored && "sia-brand-icon--colored",
84
+ contrast && `sia-brand-icon--${contrast}`,
85
+ className,
86
+ )}
87
+ style={
88
+ colored
89
+ ? ({
90
+ "--sia-brand-icon-color": `#${icon.hex.replace(/^#/, "")}`,
91
+ ...style,
92
+ } as CSSProperties)
93
+ : style
94
+ }
95
+ >
96
+ {!decorative && <title>{name}</title>}
97
+ <path d={icon.path} />
98
+ </svg>
99
+ );
100
+ }
@@ -0,0 +1,31 @@
1
+ /* Un glyphe dans le texte : aligné sur la ligne de base comme une lettre,
2
+ jamais étiré ni rétréci par un parent flex. */
3
+ .sia-brand-icon {
4
+ display: inline-block;
5
+ flex: none;
6
+ vertical-align: -.125em;
7
+ fill: currentColor;
8
+ }
9
+
10
+ /* La couleur de la marque arrive par une variable posée sur l'élément : la
11
+ feuille garde la main pour la retirer là où elle serait illisible. */
12
+ .sia-brand-icon--colored { fill: var(--sia-brand-icon-color, currentColor); }
13
+
14
+ /* Quasi blanche : illisible sur le thème clair, qui est celui par défaut. */
15
+ .sia-brand-icon--colored.sia-brand-icon--light { fill: currentColor; }
16
+
17
+ /* Thème sombre, choisi par l'utilisateur ou suivi du système : la marque
18
+ claire retrouve sa couleur, la marque quasi noire prend celle du texte. */
19
+ [data-sia-color-mode="dark"] .sia-brand-icon--colored.sia-brand-icon--light {
20
+ fill: var(--sia-brand-icon-color, currentColor);
21
+ }
22
+ [data-sia-color-mode="dark"] .sia-brand-icon--colored.sia-brand-icon--dark { fill: currentColor; }
23
+
24
+ @media (prefers-color-scheme: dark) {
25
+ :root:not([data-sia-color-mode="light"]) .sia-brand-icon--colored.sia-brand-icon--light {
26
+ fill: var(--sia-brand-icon-color, currentColor);
27
+ }
28
+ :root:not([data-sia-color-mode="light"]) .sia-brand-icon--colored.sia-brand-icon--dark {
29
+ fill: currentColor;
30
+ }
31
+ }
@@ -2,7 +2,11 @@ import { Fragment, type ReactNode } from "react";
2
2
  import { cn } from "@sia-ui/utils";
3
3
  import { useSiaLocale } from "@sia-ui/headless";
4
4
  import { ChevronRightIcon, MoreHorizontalIcon } from "../Icons";
5
- import { DropdownMenu, type MenuEntry } from "../DropdownMenu";
5
+ import {
6
+ DropdownMenu,
7
+ type DropdownMenuProps,
8
+ type MenuEntry,
9
+ } from "../DropdownMenu";
6
10
  import "./styles.css";
7
11
 
8
12
  export interface BreadcrumbItem {
@@ -24,6 +28,13 @@ export interface BreadcrumbProps {
24
28
  separator?: ReactNode;
25
29
  ariaLabel?: string;
26
30
  className?: string;
31
+ /**
32
+ * Le menu des niveaux repliés : placement, décalage, classe. Ses entrées
33
+ * et son déclencheur viennent de `items`.
34
+ */
35
+ dropdownMenuProps?: Partial<
36
+ Omit<DropdownMenuProps, "items" | "children" | "onSelect">
37
+ >;
27
38
  }
28
39
 
29
40
  /**
@@ -38,6 +49,7 @@ export function Breadcrumb({
38
49
  separator,
39
50
  ariaLabel,
40
51
  className,
52
+ dropdownMenuProps,
41
53
  }: BreadcrumbProps) {
42
54
  const locale = useSiaLocale();
43
55
  const mark = separator ?? <ChevronRightIcon />;
@@ -71,7 +83,7 @@ export function Breadcrumb({
71
83
  <Fragment key={item ? (item.key ?? index) : "overflow"}>
72
84
  <li className="sia-breadcrumb__item">
73
85
  {item === null ? (
74
- <DropdownMenu items={hidden}>
86
+ <DropdownMenu {...dropdownMenuProps} items={hidden}>
75
87
  <button
76
88
  type="button"
77
89
  className="sia-breadcrumb__more"
@@ -2,7 +2,7 @@ import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from "react";
2
2
  import { cn } from "@sia-ui/utils";
3
3
  import { Spinner, type SpinnerProps } from "../Spinner";
4
4
  import { Tooltip, type TooltipProps } from "../Tooltip";
5
- import { useComponentDefaults } from "@sia-ui/headless";
5
+ import { useComponentDefaults, useSiaLocale } from "@sia-ui/headless";
6
6
  import type { ComponentTone } from "@sia-ui/tokens";
7
7
  import "./styles.css";
8
8
 
@@ -58,8 +58,9 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
58
58
  const tone = toneProp ?? defaults.tone ?? "primary";
59
59
  const size = sizeProp ?? defaults.size ?? "md";
60
60
  const radius = radiusProp ?? defaults.radius ?? "default";
61
+ const locale = useSiaLocale();
61
62
  const loadingLabel =
62
- loadingLabelProp ?? defaults.loadingLabel ?? "Chargement";
63
+ loadingLabelProp ?? defaults.loadingLabel ?? locale.loading;
63
64
  const button = (
64
65
  <button
65
66
  ref={ref}
@@ -35,10 +35,10 @@
35
35
 
36
36
  .sia-button--primary { --sia-button-accent: var(--sia-primary); }
37
37
  .sia-button--neutral { --sia-button-accent: var(--sia-foreground); --sia-button-on-accent: var(--sia-background); }
38
- .sia-button--info { --sia-button-accent: var(--sia-info); }
39
- .sia-button--success { --sia-button-accent: var(--sia-success); }
40
- .sia-button--warning { --sia-button-accent: var(--sia-warning); }
41
- .sia-button--danger { --sia-button-accent: var(--sia-danger); }
38
+ .sia-button--info { --sia-button-accent: var(--sia-info); --sia-button-on-accent: var(--sia-info-foreground); }
39
+ .sia-button--success { --sia-button-accent: var(--sia-success); --sia-button-on-accent: var(--sia-success-foreground); }
40
+ .sia-button--warning { --sia-button-accent: var(--sia-warning); --sia-button-on-accent: var(--sia-warning-foreground); }
41
+ .sia-button--danger { --sia-button-accent: var(--sia-danger); --sia-button-on-accent: var(--sia-danger-foreground); }
42
42
 
43
43
  /* Variantes ----------------------------------------------------------- */
44
44
 
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useMemo, useState, type ReactNode } from "react";
2
2
  import { cn, toneClass } from "@sia-ui/utils";
3
+ import { useSiaLocale } from "@sia-ui/headless";
3
4
  import type { ComponentTone } from "@sia-ui/tokens";
4
5
  import { ChevronLeftIcon, ChevronRightIcon } from "../Icons";
5
6
 
@@ -36,6 +37,7 @@ export interface CalendarProps {
36
37
  cellRender?: (day: CalendarDay, originNode: ReactNode) => ReactNode;
37
38
  headerRender?: (props: { month: string; mode: CalendarMode; onMonthChange: (month: string) => void; onModeChange: (mode: CalendarMode) => void }) => ReactNode;
38
39
  renderExtraFooter?: () => ReactNode;
40
+ /** La langue des jours et des mois. Par défaut, celle de la locale SIA. */
39
41
  locale?: string;
40
42
  firstDayOfWeek?: 0 | 1;
41
43
  showWeek?: boolean;
@@ -91,13 +93,15 @@ export function Calendar({
91
93
  cellRender,
92
94
  headerRender,
93
95
  renderExtraFooter,
94
- locale = "fr-FR",
96
+ locale: localeProp,
95
97
  firstDayOfWeek = 1,
96
98
  showWeek = false,
97
99
  fullscreen = false,
98
100
  tone = "primary",
99
101
  className,
100
102
  }: CalendarProps) {
103
+ const messages = useSiaLocale();
104
+ const locale = localeProp ?? messages.language;
101
105
  const [selected, setSelected] = useState(defaultValue ?? "");
102
106
  const [selectedRange, setSelectedRange] = useState(defaultRange);
103
107
  const [view, setView] = useState(() => parseMonth(month ?? defaultMonth ?? value ?? defaultValue));
@@ -169,12 +173,12 @@ export function Calendar({
169
173
  <section className={cn("sia-calendar", `sia-calendar--${currentMode}`, fullscreen && "sia-calendar--fullscreen", toneClass("sia-tone", tone), className)}>
170
174
  {headerRender?.({ month: monthIso(controlledMonth), mode: currentMode, onMonthChange: updateFromString, onModeChange: changeMode }) ?? (
171
175
  <header className="sia-calendar__header">
172
- <button type="button" className="sia-calendar__nav" aria-label={currentMode === "month" ? "Mois précédent" : "Année précédente"} onClick={() => currentMode === "month" ? changeMonth(-1) : setMonth(new Date(year - 1, monthIndex, 1, 12))}><ChevronLeftIcon /></button>
176
+ <button type="button" className="sia-calendar__nav" aria-label={currentMode === "month" ? messages.calendar.previousMonth : messages.calendar.previousYear} onClick={() => currentMode === "month" ? changeMonth(-1) : setMonth(new Date(year - 1, monthIndex, 1, 12))}><ChevronLeftIcon /></button>
173
177
  <div className="sia-calendar__heading">
174
178
  <button type="button" onClick={() => changeMode(currentMode === "month" ? "year" : "month")}>{currentMode === "month" ? monthName : year}</button>
175
179
  {currentMode === "month" && <button type="button" onClick={() => changeMode("year")}>{year}</button>}
176
180
  </div>
177
- <button type="button" className="sia-calendar__nav" aria-label={currentMode === "month" ? "Mois suivant" : "Année suivante"} onClick={() => currentMode === "month" ? changeMonth(1) : setMonth(new Date(year + 1, monthIndex, 1, 12))}><ChevronRightIcon /></button>
181
+ <button type="button" className="sia-calendar__nav" aria-label={currentMode === "month" ? messages.calendar.nextMonth : messages.calendar.nextYear} onClick={() => currentMode === "month" ? changeMonth(1) : setMonth(new Date(year + 1, monthIndex, 1, 12))}><ChevronRightIcon /></button>
178
182
  </header>
179
183
  )}
180
184
  {currentMode === "year" ? (
@@ -2,6 +2,16 @@ import { type HTMLAttributes } from "react";
2
2
  import { cn } from "@sia-ui/utils";
3
3
  import "./styles.css";
4
4
 
5
+ /*
6
+ Les quatre parties ne sont que des `div` : ces alias ne changent rien au
7
+ typage, ils donnent un nom aux props qu'un composant composé laisse passer
8
+ (`cardBodyProps?: Partial<CardBodyProps>`).
9
+ */
10
+ export type CardProps = HTMLAttributes<HTMLDivElement>;
11
+ export type CardHeaderProps = HTMLAttributes<HTMLDivElement>;
12
+ export type CardBodyProps = HTMLAttributes<HTMLDivElement>;
13
+ export type CardFooterProps = HTMLAttributes<HTMLDivElement>;
14
+
5
15
  /**
6
16
  * Une surface qui regroupe ce qui va ensemble.
7
17
  *
@@ -10,7 +20,7 @@ import "./styles.css";
10
20
  * deux boutons et une pastille, une prop ne suffit plus, et l'on se retrouve
11
21
  * à passer du JSX à travers une chaîne.
12
22
  */
13
- export function Card({ className, ...props }: HTMLAttributes<HTMLDivElement>) { return <div className={cn("sia-card", className)} {...props} />; }
14
- export function CardHeader({ className, ...props }: HTMLAttributes<HTMLDivElement>) { return <div className={cn("sia-card__header", className)} {...props} />; }
15
- export function CardBody({ className, ...props }: HTMLAttributes<HTMLDivElement>) { return <div className={cn("sia-card__body", className)} {...props} />; }
16
- export function CardFooter({ className, ...props }: HTMLAttributes<HTMLDivElement>) { return <div className={cn("sia-card__footer", className)} {...props} />; }
23
+ export function Card({ className, ...props }: CardProps) { return <div className={cn("sia-card", className)} {...props} />; }
24
+ export function CardHeader({ className, ...props }: CardHeaderProps) { return <div className={cn("sia-card__header", className)} {...props} />; }
25
+ export function CardBody({ className, ...props }: CardBodyProps) { return <div className={cn("sia-card__body", className)} {...props} />; }
26
+ export function CardFooter({ className, ...props }: CardFooterProps) { return <div className={cn("sia-card__footer", className)} {...props} />; }
@@ -0,0 +1,44 @@
1
+ import { useSiaLocale } from "@sia-ui/headless";
2
+ import { useColorMode } from "@sia-ui/react";
3
+ import { IconButton, type IconButtonProps } from "../IconButton";
4
+ import { MoonIcon, SunIcon } from "../Icons";
5
+
6
+ export interface ColorModeToggleProps
7
+ extends Partial<Omit<IconButtonProps, "icon" | "onClick" | "aria-pressed">> {
8
+ /** Le nom du bouton en mode clair. Par défaut, celui de la locale. */
9
+ darkLabel?: string;
10
+ /** Le nom du bouton en mode sombre. Par défaut, celui de la locale. */
11
+ lightLabel?: string;
12
+ }
13
+
14
+ /**
15
+ * Le bouton de bascule clair / sombre.
16
+ *
17
+ * Il lit et écrit le mode de `SiaProvider` — persistance et script
18
+ * anti-clignotement compris — plutôt que de tenir le sien. L'icône montre le
19
+ * mode vers lequel on part, comme le libellé : le soleil en mode sombre.
20
+ * Toutes les props d'`IconButton` passent, sauf ce qu'il pilote.
21
+ */
22
+ export function ColorModeToggle({
23
+ darkLabel: darkLabelProp,
24
+ lightLabel: lightLabelProp,
25
+ variant = "ghost",
26
+ ...props
27
+ }: ColorModeToggleProps) {
28
+ const m = useSiaLocale().colorModeToggle;
29
+ const darkLabel = darkLabelProp ?? m.toDark;
30
+ const lightLabel = lightLabelProp ?? m.toLight;
31
+ const { resolvedColorMode, toggleColorMode } = useColorMode();
32
+ const sombre = resolvedColorMode === "dark";
33
+
34
+ return (
35
+ <IconButton
36
+ variant={variant}
37
+ {...props}
38
+ label={props.label ?? (sombre ? lightLabel : darkLabel)}
39
+ icon={sombre ? <SunIcon /> : <MoonIcon />}
40
+ aria-pressed={sombre}
41
+ onClick={toggleColorMode}
42
+ />
43
+ );
44
+ }
@@ -1,9 +1,10 @@
1
1
  import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
2
2
  import { cn } from "@sia-ui/utils";
3
+ import { useSiaLocale } from "@sia-ui/headless";
3
4
  import "./styles.css";
4
5
 
5
6
  export interface CommandItem { id: string; label: string; description?: string; keywords?: string[]; icon?: ReactNode; disabled?: boolean; onSelect: () => void; }
6
- export interface CommandPaletteProps { open: boolean; onOpenChange: (open: boolean) => void; items: CommandItem[]; placeholder?: string; emptyLabel?: string; className?: string; }
7
+ export interface CommandPaletteProps { open: boolean; onOpenChange: (open: boolean) => void; items: CommandItem[]; /** Par défaut, celui de la locale. */ placeholder?: string; /** Par défaut, celui de la locale. */ emptyLabel?: string; className?: string; }
7
8
  /**
8
9
  * Tout ce que l'application sait faire, au clavier.
9
10
  *
@@ -11,4 +12,4 @@ export interface CommandPaletteProps { open: boolean; onOpenChange: (open: boole
11
12
  * « facture » doit trouver « Nouvelle vente » sans avoir appris comment
12
13
  * l'action s'appelle ici.
13
14
  */
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>; }
15
+ export function CommandPalette({ open, onOpenChange, items, placeholder: placeholderProp, emptyLabel: emptyLabelProp, className }: CommandPaletteProps) { const m = useSiaLocale().commandPalette; const placeholder = placeholderProp ?? m.placeholder; const emptyLabel = emptyLabelProp ?? m.empty; 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={m.ariaLabel}><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>; }
@@ -1,7 +1,8 @@
1
1
  import { type ReactNode } from "react";
2
- import { Button } from "../Button";
2
+ import { cn } from "@sia-ui/utils";
3
+ import { Button, type ButtonProps } from "../Button";
3
4
  import type { ComponentTone } from "@sia-ui/tokens";
4
- import { Modal } from "../Modal";
5
+ import { Modal, type ModalProps } from "../Modal";
5
6
  import { useSiaLocale } from "@sia-ui/headless";
6
7
 
7
8
  export interface ConfirmDialogProps {
@@ -14,6 +15,24 @@ export interface ConfirmDialogProps {
14
15
  tone?: Extract<ComponentTone, "primary" | "danger">;
15
16
  loading?: boolean;
16
17
  onConfirm: () => void | Promise<void>;
18
+ /**
19
+ * Les props de la `Modal`. Sans `open`, `onOpenChange`, `children` ni
20
+ * `footer`, que la boîte de confirmation tient elle-même. Pendant
21
+ * `loading`, un clic sur le voile ne ferme jamais, quel que soit
22
+ * `closeOnBackdrop` : ce serait abandonner un appel encore en vol.
23
+ */
24
+ modalProps?: Partial<Omit<ModalProps, "open" | "onOpenChange" | "children" | "footer">>;
25
+ /**
26
+ * Les props du bouton de confirmation. Sans `onClick` ni `loading`,
27
+ * branchés sur `onConfirm` et `loading` ; le libellé passe par
28
+ * `confirmLabel`.
29
+ */
30
+ confirmButtonProps?: Partial<Omit<ButtonProps, "onClick" | "loading" | "children">>;
31
+ /**
32
+ * Les props du bouton d'annulation. Sans `onClick`, qui referme la boîte ;
33
+ * `disabled` s'ajoute à `loading` au lieu de le remplacer.
34
+ */
35
+ cancelButtonProps?: Partial<Omit<ButtonProps, "onClick" | "children">>;
17
36
  }
18
37
 
19
38
  /**
@@ -23,9 +42,10 @@ export interface ConfirmDialogProps {
23
42
  * n'a pas abouti. Sans cela, la boîte se referme sur un appel encore en
24
43
  * vol, et l'on ignore si la suppression a eu lieu.
25
44
  */
26
- export function ConfirmDialog({ open, onOpenChange, title, description, confirmLabel: confirmLabelProp, cancelLabel: cancelLabelProp, tone = "primary", loading = false, onConfirm }: ConfirmDialogProps) {
45
+ export function ConfirmDialog({ open, onOpenChange, title, description, confirmLabel: confirmLabelProp, cancelLabel: cancelLabelProp, tone = "primary", loading = false, onConfirm, modalProps, confirmButtonProps, cancelButtonProps }: ConfirmDialogProps) {
27
46
  const locale = useSiaLocale();
28
47
  const cancelLabel = cancelLabelProp ?? locale.cancel;
29
48
  const confirmLabel = confirmLabelProp ?? locale.confirm;
30
- return <Modal open={open} onOpenChange={onOpenChange} title={title} description={description} closeOnBackdrop={!loading} footer={<><Button variant="ghost" disabled={loading} onClick={() => onOpenChange(false)}>{cancelLabel}</Button><Button className={tone === "danger" ? "sia-button--danger" : undefined} loading={loading} onClick={() => void onConfirm()}>{confirmLabel}</Button></>}>{description ? null : <p>Cette action nécessite votre confirmation.</p>}</Modal>;
49
+ const confirmClassName = cn(tone === "danger" && "sia-button--danger", confirmButtonProps?.className);
50
+ return <Modal title={title} description={description} {...modalProps} open={open} onOpenChange={onOpenChange} closeOnBackdrop={!loading && (modalProps?.closeOnBackdrop ?? true)} footer={<><Button variant="ghost" {...cancelButtonProps} disabled={loading || Boolean(cancelButtonProps?.disabled)} onClick={() => onOpenChange(false)}>{cancelLabel}</Button><Button {...confirmButtonProps} {...(confirmClassName ? { className: confirmClassName } : {})} loading={loading} onClick={() => void onConfirm()}>{confirmLabel}</Button></>}>{description ? null : <p>{locale.confirmDialog.defaultDescription}</p>}</Modal>;
31
51
  }
@@ -1,17 +1,20 @@
1
1
  import { type Key, type ReactNode } from "react";
2
2
  import {
3
3
  hasFormErrors,
4
+ useSiaLocale,
4
5
  type FormSubmitResult,
5
6
  type FormValidator,
6
7
  } from "@sia-ui/headless";
7
- import { Modal } from "../Modal";
8
+ import { cn } from "@sia-ui/utils";
9
+ import { Modal, type ModalProps } from "../Modal";
8
10
  import {
9
11
  Form,
10
12
  formFieldNames,
11
13
  type FormEntry,
14
+ type FormProps,
12
15
  type FormShape,
13
16
  } from "../Form";
14
- import { Descriptions } from "../Descriptions";
17
+ import { Descriptions, type DescriptionsProps } from "../Descriptions";
15
18
  import type { DataTableColumn } from "../DataTable/types";
16
19
  import { renduCellule } from "../DataTable/values";
17
20
 
@@ -70,6 +73,33 @@ export function valeursDeLigne<T>(row: T, fields: FormEntry[]): FormShape {
70
73
  return valeurs;
71
74
  }
72
75
 
76
+ /**
77
+ * Les props des deux `Modal` — formulaire et détail — qu'on peut régler.
78
+ *
79
+ * Sans `open` ni `onOpenChange`, qui suivent l'état des boîtes, ni
80
+ * `children`. Sans `title` ni `description` non plus : ils diffèrent d'une
81
+ * boîte à l'autre et se règlent par `dialog`. `className` s'ajoute à celui
82
+ * des boîtes.
83
+ */
84
+ export type CrudModalProps = Partial<
85
+ Omit<ModalProps, "open" | "onOpenChange" | "children" | "title" | "description">
86
+ >;
87
+
88
+ /**
89
+ * Les props du `Form` de création et de modification qu'on peut régler —
90
+ * `showClear`, `submitProps`, `fieldProps`.
91
+ *
92
+ * Sans ce que la boîte branche elle-même : les champs, les valeurs de
93
+ * départ, la validation et l'envoi, qui ont leurs props sur la page ; ni
94
+ * l'adaptateur `form`, qui couperait la boîte de ses propres valeurs.
95
+ */
96
+ export type CrudFormProps = Partial<
97
+ Omit<FormProps, "fields" | "defaultValues" | "validate" | "onSubmit" | "form">
98
+ >;
99
+
100
+ /** Les props de la vue de détail par défaut. Sans `items`, tirés des colonnes. */
101
+ export type CrudDescriptionsProps = Partial<Omit<DescriptionsProps, "items">>;
102
+
73
103
  export interface CrudDialogsProps<T> {
74
104
  state: CrudDialogState<T> | null;
75
105
  onClose: () => void;
@@ -97,6 +127,13 @@ export interface CrudDialogsProps<T> {
97
127
 
98
128
  getRowKey?: ((row: T, index: number) => Key) | undefined;
99
129
  dialog?: CrudDialogOptions | undefined;
130
+
131
+ /** Voir `CrudModalProps`. */
132
+ modalProps?: CrudModalProps | undefined;
133
+ /** Voir `CrudFormProps`. */
134
+ formProps?: CrudFormProps | undefined;
135
+ /** Voir `CrudDescriptionsProps`. */
136
+ descriptionsProps?: CrudDescriptionsProps | undefined;
100
137
  }
101
138
 
102
139
  /**
@@ -121,7 +158,11 @@ export function CrudDialogs<T>({
121
158
  renderDetail,
122
159
  getRowKey,
123
160
  dialog,
161
+ modalProps,
162
+ formProps,
163
+ descriptionsProps,
124
164
  }: CrudDialogsProps<T>) {
165
+ const locale = useSiaLocale();
125
166
  const creation = state?.mode === "create";
126
167
  const modification = state?.mode === "edit";
127
168
  const consultation = state?.mode === "view";
@@ -155,14 +196,15 @@ export function CrudDialogs<T>({
155
196
  <>
156
197
  {fields && (creation || modification) && (
157
198
  <Modal
199
+ {...modalProps}
158
200
  open
159
201
  onOpenChange={(ouvert) => {
160
202
  if (!ouvert) onClose();
161
203
  }}
162
204
  title={
163
205
  creation
164
- ? (dialog?.createTitle ?? "Créer")
165
- : (dialog?.editTitle ?? "Modifier")
206
+ ? (dialog?.createTitle ?? locale.create)
207
+ : (dialog?.editTitle ?? locale.edit)
166
208
  }
167
209
  {...(creation
168
210
  ? dialog?.createDescription
@@ -171,7 +213,7 @@ export function CrudDialogs<T>({
171
213
  : dialog?.editDescription
172
214
  ? { description: dialog.editDescription }
173
215
  : {})}
174
- className="sia-crud-dialog"
216
+ className={cn("sia-crud-dialog", modalProps?.className)}
175
217
  >
176
218
  <Form
177
219
  key={cle}
@@ -180,12 +222,13 @@ export function CrudDialogs<T>({
180
222
  {...(validate ? { validate } : {})}
181
223
  columns={dialog?.columns ?? 1}
182
224
  submitText={
183
- dialog?.submitText ?? (creation ? "Créer" : "Enregistrer")
225
+ dialog?.submitText ?? (creation ? locale.create : locale.save)
184
226
  }
185
227
  // Une création part de champs vides : exiger une modification
186
228
  // bloquerait l'envoi d'un formulaire dont tous les défauts
187
229
  // conviennent.
188
230
  requireDirty={modification}
231
+ {...formProps}
189
232
  onSubmit={async (values) => {
190
233
  const resultat = await onSubmit?.(values, {
191
234
  mode: creation ? "create" : "edit",
@@ -203,15 +246,16 @@ export function CrudDialogs<T>({
203
246
 
204
247
  {consultation && state?.row !== undefined && (
205
248
  <Modal
249
+ {...modalProps}
206
250
  open
207
251
  onOpenChange={(ouvert) => {
208
252
  if (!ouvert) onClose();
209
253
  }}
210
- title={dialog?.viewTitle ?? "Détail"}
254
+ title={dialog?.viewTitle ?? locale.crudPage.detailTitle}
211
255
  {...(dialog?.viewDescription
212
256
  ? { description: dialog.viewDescription }
213
257
  : {})}
214
- className="sia-crud-dialog"
258
+ className={cn("sia-crud-dialog", modalProps?.className)}
215
259
  >
216
260
  {renderDetail?.(state.row, state.index ?? 0) ?? (
217
261
  /*
@@ -223,6 +267,7 @@ export function CrudDialogs<T>({
223
267
  */
224
268
  <Descriptions
225
269
  columns={dialog?.detailColumns ?? 2}
270
+ {...descriptionsProps}
226
271
  items={columns
227
272
  .filter((colonne) => colonne.card !== "hidden")
228
273
  .map((colonne) => ({