@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
@@ -2,4 +2,11 @@ import { useState, type ReactNode, type TextareaHTMLAttributes } from "react";
2
2
  import { Textarea } from "../Textarea";
3
3
  import { cn } from "@sia-ui/utils";
4
4
  export interface MarkdownEditorProps extends Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "value" | "defaultValue" | "onChange"> { value?: string; defaultValue?: string; onValueChange?: (value: string) => void; preview?: (value: string) => ReactNode; }
5
+ /**
6
+ * Une saisie Markdown, avec son aperçu.
7
+ *
8
+ * Le rendu n'est pas fourni : `preview` reçoit le texte et rend ce qu'il
9
+ * veut. Embarquer un moteur Markdown imposerait le nôtre à un projet qui a
10
+ * déjà le sien — et ferait grossir le paquet pour tous les autres.
11
+ */
5
12
  export function MarkdownEditor({ value, defaultValue = "", onValueChange, preview, className, ...props }: MarkdownEditorProps) { const [internal, setInternal] = useState(defaultValue); const [mode, setMode] = useState<"write" | "preview">("write"); const current = value ?? internal; const update = (next: string) => { if (value === undefined) setInternal(next); onValueChange?.(next); }; return <div className={cn("sia-markdown-editor", className)}><div className="sia-editor-toolbar"><button type="button" aria-pressed={mode === "write"} onClick={() => setMode("write")}>Écrire</button><button type="button" aria-pressed={mode === "preview"} onClick={() => setMode("preview")}>Aperçu</button></div>{mode === "write" ? <Textarea {...props} value={current} onChange={(event) => update(event.target.value)} /> : <div className="sia-editor-preview">{preview ? preview(current) : <pre>{current}</pre>}</div>}</div>; }
@@ -1,3 +1,10 @@
1
1
  import { Calendar, type CalendarProps } from "../Calendar";
2
2
  import { cn } from "@sia-ui/utils";
3
+ /**
4
+ * Le même calendrier, en plus petit.
5
+ *
6
+ * Une variante de taille et non un composant à part : deux calendriers
7
+ * séparés auraient divergé sur la sélection de plage, les jours désactivés
8
+ * ou le premier jour de la semaine.
9
+ */
3
10
  export function MiniCalendar({ className, ...props }: CalendarProps) { return <Calendar {...props} className={cn("sia-calendar--mini", className)} />; }
@@ -17,6 +17,14 @@ export interface ModalProps {
17
17
  className?: string;
18
18
  }
19
19
 
20
+ /**
21
+ * Une boîte qui interrompt.
22
+ *
23
+ * Elle enferme le focus et bloque le défilement de la page : sans cela, la
24
+ * tabulation continue derrière le voile, et l'on remplit un formulaire
25
+ * qu'on ne voit pas. L'échappement la referme toujours — c'est la sortie
26
+ * que tout le monde essaie en premier.
27
+ */
20
28
  export function Modal({
21
29
  open,
22
30
  onOpenChange,
@@ -1,5 +1,12 @@
1
1
  import { forwardRef, type InputHTMLAttributes } from "react";
2
2
  import { cn } from "@sia-ui/utils";
3
3
  export interface MonthPickerProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "onChange"> { onValueChange?: (value: string) => void; invalid?: boolean; }
4
+ /**
5
+ * Un mois, sans le jour.
6
+ *
7
+ * C'est le grain d'un exercice comptable ou d'un rapport mensuel : demander
8
+ * un jour obligerait à en choisir un arbitrairement, et le premier du mois
9
+ * finirait par être pris pour une vraie date.
10
+ */
4
11
  export const MonthPicker = forwardRef<HTMLInputElement, MonthPickerProps>(({ onValueChange, invalid, className, ...props }, ref) => <input {...props} ref={ref} type="month" className={cn("sia-month-picker", invalid && "sia-picker--invalid", className)} aria-invalid={invalid || undefined} onChange={(event) => onValueChange?.(event.target.value)} />);
5
12
  MonthPicker.displayName = "MonthPicker";
@@ -20,6 +20,13 @@ export interface MultiSelectProps {
20
20
  "aria-invalid"?: boolean;
21
21
  "aria-required"?: boolean;
22
22
  }
23
+ /**
24
+ * Plusieurs choix dans une liste.
25
+ *
26
+ * Les valeurs retenues s'affichent dans le champ, retirables une à une :
27
+ * un compte seul — « 3 sélectionnés » — oblige à rouvrir la liste pour
28
+ * savoir lesquels.
29
+ */
23
30
  export function MultiSelect({
24
31
  id,
25
32
  options,
@@ -2,4 +2,11 @@ import { useRef, useState, type ClipboardEvent, type KeyboardEvent } from "react
2
2
  import { cn } from "@sia-ui/utils";
3
3
  import "./styles.css";
4
4
  export interface OtpInputProps { id?: string; value?: string; defaultValue?: string; onValueChange?: (value: string) => void; length?: number; inputMode?: "numeric" | "text"; mask?: boolean; disabled?: boolean; className?: string; "aria-describedby"?: string; "aria-labelledby"?: string; "aria-invalid"?: boolean; "aria-required"?: boolean; }
5
+ /**
6
+ * Un code à usage unique, chiffre par chiffre.
7
+ *
8
+ * Le collage d'un code entier remplit toutes les cases d'un coup : c'est
9
+ * ainsi qu'on le récupère d'un SMS, et le forcer à être retapé serait une
10
+ * punition pour rien.
11
+ */
5
12
  export function OtpInput({ id, value, defaultValue = "", onValueChange, length = 6, inputMode = "numeric", mask, disabled, className, ...ariaProps }: OtpInputProps) { const [internal, setInternal] = useState(defaultValue); const refs = useRef<Array<HTMLInputElement | null>>([]); const current = (value ?? internal).padEnd(length, " ").slice(0, length).split(""); const update = (chars: string[]) => { const next = chars.join("").trimEnd(); if (value === undefined) setInternal(next); onValueChange?.(next); }; const accept = (text: string) => inputMode === "numeric" ? text.replace(/\D/g, "") : text; const paste = (event: ClipboardEvent<HTMLDivElement>) => { event.preventDefault(); const next = accept(event.clipboardData.getData("text")).slice(0, length).padEnd(length, " ").split(""); update(next); refs.current[Math.min(next.join("").trimEnd().length, length - 1)]?.focus(); }; return <div id={id} className={cn("sia-otp-input", className)} onPaste={paste} {...ariaProps}>{current.map((char, index) => <input key={index} ref={(node) => { refs.current[index] = node; }} value={char.trim()} type={mask ? "password" : "text"} inputMode={inputMode} maxLength={1} disabled={disabled} aria-label={`Caractère ${index + 1}`} onChange={(event) => { const next = [...current]; next[index] = accept(event.target.value).slice(-1) || " "; update(next); if (next[index]?.trim()) refs.current[index + 1]?.focus(); }} onKeyDown={(event: KeyboardEvent<HTMLInputElement>) => { if (event.key === "Backspace" && !current[index]?.trim()) refs.current[index - 1]?.focus(); if (event.key === "ArrowLeft") refs.current[index - 1]?.focus(); if (event.key === "ArrowRight") refs.current[index + 1]?.focus(); }} />)}</div>; }
@@ -10,13 +10,19 @@ import {
10
10
  } from "react";
11
11
  import { createPortal } from "react-dom";
12
12
  import { cn } from "@sia-ui/utils";
13
+ import { computeFloatingPosition } from "@sia-ui/headless";
14
+ import type { Placement } from "@sia-ui/headless";
13
15
  import "./styles.css";
14
16
 
15
- export type OverlayPlacement =
16
- | "bottom-start"
17
- | "bottom-end"
18
- | "top-start"
19
- | "top-end";
17
+ /**
18
+ * Les douze placements du moteur de géométrie.
19
+ *
20
+ * Ce composant n'en exposait que quatre — haut et bas — parce qu'il calculait
21
+ * lui-même sa position, en vertical seulement. Le calcul vit dans
22
+ * `@sia-ui/headless`, sait basculer et recadrer dans les deux axes, et sert
23
+ * déjà le natif : le refaire ici privait tous les flottants des côtés.
24
+ */
25
+ export type OverlayPlacement = Placement;
20
26
 
21
27
  export interface OverlayProps {
22
28
  open: boolean;
@@ -34,6 +40,16 @@ export interface OverlayProps {
34
40
 
35
41
  type Position = CSSProperties & { visibility: "hidden" | "visible" };
36
42
 
43
+ /**
44
+ * Ce qui flotte à côté d'un élément.
45
+ *
46
+ * Le socle des menus, des listes déroulantes et des infobulles. Il place le
47
+ * panneau parmi douze positions, le retourne quand le bord de la fenêtre
48
+ * approche, et le ramène dans la vue plutôt que de le laisser déborder.
49
+ *
50
+ * Le calcul vit dans `@sia-ui/headless` : il ne dépend d'aucun rendu, et
51
+ * sert donc aussi à ce qui n'est pas le DOM.
52
+ */
37
53
  export function Overlay({
38
54
  open,
39
55
  anchorRef,
@@ -61,32 +77,26 @@ export function Overlay({
61
77
 
62
78
  const anchorRect = anchor.getBoundingClientRect();
63
79
  const overlayRect = overlay.getBoundingClientRect();
80
+
81
+ // `Select` veut un panneau au moins aussi large que son champ : la largeur
82
+ // entre donc dans le calcul, sinon le placement serait fait sur une taille
83
+ // que le panneau n'aura pas.
64
84
  const width = matchAnchorWidth
65
85
  ? Math.max(anchorRect.width, overlayRect.width)
66
86
  : overlayRect.width;
67
- const spaceBelow = window.innerHeight - anchorRect.bottom - viewportPadding;
68
- const spaceAbove = anchorRect.top - viewportPadding;
69
- const requestedTop = placement.startsWith("top");
70
- const useTop = requestedTop
71
- ? spaceAbove >= overlayRect.height || spaceAbove > spaceBelow
72
- : spaceBelow < overlayRect.height && spaceAbove > spaceBelow;
73
- const rawTop = useTop
74
- ? anchorRect.top - overlayRect.height - offset
75
- : anchorRect.bottom + offset;
76
- const alignEnd = placement.endsWith("end");
77
- const rawLeft = alignEnd
78
- ? anchorRect.right - width
79
- : anchorRect.left;
80
- const maxLeft = Math.max(viewportPadding, window.innerWidth - width - viewportPadding);
81
- const maxTop = Math.max(
82
- viewportPadding,
83
- window.innerHeight - overlayRect.height - viewportPadding,
87
+
88
+ const { top, left } = computeFloatingPosition(
89
+ anchorRect,
90
+ { width, height: overlayRect.height },
91
+ { width: window.innerWidth, height: window.innerHeight },
92
+ placement,
93
+ { offset, padding: viewportPadding },
84
94
  );
85
95
 
86
96
  setStyle({
87
97
  position: "fixed",
88
- top: Math.min(Math.max(rawTop, viewportPadding), maxTop),
89
- left: Math.min(Math.max(rawLeft, viewportPadding), maxLeft),
98
+ top,
99
+ left,
90
100
  minWidth: matchAnchorWidth ? anchorRect.width : undefined,
91
101
  maxWidth: `calc(100vw - ${viewportPadding * 2}px)`,
92
102
  visibility: "visible",
@@ -10,6 +10,13 @@ export interface PageHeaderProps {
10
10
  className?: string;
11
11
  }
12
12
 
13
+ /**
14
+ * Le titre d'un écran, et ses actions.
15
+ *
16
+ * Un seul `<h1>` par page, rendu ici : la hiérarchie des titres est ce qui
17
+ * permet de parcourir un écran au lecteur d'écran, et deux `<h1>` la
18
+ * cassent sans que rien ne se voie.
19
+ */
13
20
  export function PageHeader({ title, description, eyebrow, actions, className }: PageHeaderProps) {
14
21
  return (
15
22
  <header className={cn("sia-page-header", className)}>
@@ -1,3 +1,9 @@
1
1
  import { type ReactNode } from "react";
2
2
  export interface PermissionGateProps { permissions: string[]; require: string | string[]; mode?: "all" | "any"; fallback?: ReactNode; children: ReactNode; }
3
+ /**
4
+ * Ce qui n'apparaît qu'avec le droit correspondant.
5
+ *
6
+ * > Ceci masque une portion d'écran; **ça ne protège pas une API**. Le
7
+ * > serveur reste seul juge, et doit refuser la même chose.
8
+ */
3
9
  export function PermissionGate({ permissions, require, mode = "all", fallback = null, children }: PermissionGateProps) { const expected = Array.isArray(require) ? require : [require]; const allowed = mode === "all" ? expected.every((item) => permissions.includes(item)) : expected.some((item) => permissions.includes(item)); return <>{allowed ? children : fallback}</>; }
@@ -5,6 +5,13 @@ import { Overlay, type OverlayPlacement } from "../Overlay";
5
5
  import "./styles.css";
6
6
 
7
7
  export interface PopoverProps { content: ReactNode; title?: ReactNode; children: ReactElement<{ onClick?: () => void; "aria-expanded"?: boolean }>; open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean) => void; placement?: OverlayPlacement; trigger?: "click" | "hover"; variant?: "solid" | "outline"; tone?: ComponentTone; disabled?: boolean; className?: string; }
8
+ /**
9
+ * Un panneau attaché à un élément, avec du contenu qu'on peut viser.
10
+ *
11
+ * À la différence d'une infobulle, il reste ouvert : on y met un lien, un
12
+ * bouton, un petit formulaire. Une infobulle qui contient un bouton est une
13
+ * infobulle cassée — elle disparaît dès qu'on va vers lui.
14
+ */
8
15
  export function Popover({ content, title, children, open: openProp, defaultOpen = false, onOpenChange, placement = "bottom-start", trigger = "click", variant = "outline", tone = "neutral", disabled = false, className }: PopoverProps) {
9
16
  const anchorRef = useRef<HTMLElement>(null); const [internalOpen, setInternalOpen] = useState(defaultOpen); const open = openProp ?? internalOpen; const setOpen = (next: boolean) => { if (disabled) return; if (openProp === undefined) setInternalOpen(next); onOpenChange?.(next); };
10
17
  const child = isValidElement(children) ? cloneElement(children, { ref: anchorRef, "aria-expanded": open, ...(trigger === "click" ? { onClick: () => { children.props.onClick?.(); setOpen(!open); } } : {}) } as never) : children;
@@ -3,4 +3,11 @@ import { cn } from "@sia-ui/utils";
3
3
  import "./styles.css";
4
4
 
5
5
  export interface QRCodeProps { value: string; size?: number; label?: string; render?: (value: string, size: number) => ReactNode; className?: string; }
6
+ /**
7
+ * Un code à scanner.
8
+ *
9
+ * Le rendu est délégué à `render` : embarquer un encodeur ajouterait une
10
+ * dépendance à tous ceux qui ne s'en servent pas, alors que les projets qui
11
+ * en ont besoin ont déjà le leur.
12
+ */
6
13
  export function QRCode({ value, size = 160, label = "Code QR", render, className }: QRCodeProps) { return <figure className={cn("sia-qr-code", className)} aria-label={label} style={{ width: size }}><div className="sia-qr-code__frame" style={{ width: size, height: size }}>{render ? render(value, size) : <div className="sia-qr-code__adapter"><strong>QR</strong><span>Encodeur requis</span></div>}</div><figcaption>{value}</figcaption></figure>; }
@@ -10,6 +10,13 @@ const RadioGroupContext = createContext<RadioContextValue | null>(null);
10
10
  export interface RadioGroupProps {
11
11
  id?: string; name?: string; label?: ReactNode; value?: RadioValue; defaultValue?: RadioValue; onValueChange?: (value: RadioValue) => void; orientation?: "horizontal" | "vertical"; disabled?: boolean; size?: "sm" | "md" | "lg"; tone?: ComponentTone; className?: string; "aria-describedby"?: string; "aria-labelledby"?: string; "aria-invalid"?: boolean; "aria-required"?: boolean; children: ReactNode;
12
12
  }
13
+ /**
14
+ * Un choix parmi plusieurs, tous visibles.
15
+ *
16
+ * Au-delà de cinq ou six options, une liste déroulante lit mieux : des
17
+ * boutons radio en montrent la totalité, ce qui est leur intérêt tant qu'on
18
+ * peut encore les embrasser du regard.
19
+ */
13
20
  export function RadioGroup({ id, name, label, value, defaultValue, onValueChange, orientation = "vertical", disabled, size = "md", tone = "primary", className, children, ...ariaProps }: RadioGroupProps) {
14
21
  const generatedName = useId(); const [internal, setInternal] = useState(defaultValue); const current = value ?? internal;
15
22
  const setValue = (next: RadioValue) => { if (value === undefined) setInternal(next); onValueChange?.(next); };
@@ -19,6 +19,12 @@ export interface RatingProps {
19
19
  "aria-invalid"?: boolean;
20
20
  "aria-required"?: boolean;
21
21
  }
22
+ /**
23
+ * Une note, en étoiles.
24
+ *
25
+ * `allowClear` rend la note réversible : sans lui, cliquer une étoile par
26
+ * erreur est définitif, puisqu'aucune étoile ne vaut « aucune note ».
27
+ */
22
28
  export function Rating({
23
29
  id,
24
30
  value,
@@ -4,4 +4,11 @@ import type { SelectOption } from "../Select";
4
4
  import "./styles.css";
5
5
  export interface ReferenceOption extends SelectOption { description?: ReactNode; }
6
6
  export interface ReferenceSelectProps extends Omit<AutocompleteProps, "options"> { options: ReferenceOption[]; }
7
+ /**
8
+ * Un choix parmi des objets métier, pas parmi des mots.
9
+ *
10
+ * Chaque option porte une description : deux clients peuvent s'appeler
11
+ * « Entreprise Alpha », et seule la ligne en dessous — la ville, le numéro
12
+ * de compte — dit lequel on vise.
13
+ */
7
14
  export function ReferenceSelect({ options, ...props }: ReferenceSelectProps) { return <Autocomplete {...props} options={options.map((option) => ({ ...option, label: <span className="sia-reference-select__option"><strong>{option.label}</strong>{option.description && <small>{option.description}</small>}</span> }))} />; }
@@ -9,6 +9,12 @@ export interface RelativeTimeProps extends Omit<HTMLAttributes<HTMLTimeElement>,
9
9
  refreshInterval?: number;
10
10
  }
11
11
 
12
+ /**
13
+ * Une date, dite par rapport à maintenant.
14
+ *
15
+ * Elle se rafraîchit d'elle-même : « il y a une minute » affiché une heure
16
+ * plus tard est faux, et rien ne le signale à qui lit l'écran resté ouvert.
17
+ */
12
18
  export function RelativeTime({ value, locale = "fr-FR", prefix, refreshInterval = 60_000, className, title, ...props }: RelativeTimeProps) {
13
19
  const [, refresh] = useState(0);
14
20
  useEffect(() => {
@@ -3,4 +3,11 @@ import { cn } from "@sia-ui/utils";
3
3
  import "./styles.css";
4
4
 
5
5
  export interface ResizablePanelProps { first: ReactNode; second: ReactNode; defaultSize?: number; minSize?: number; maxSize?: number; direction?: "horizontal" | "vertical"; className?: string; }
6
+ /**
7
+ * Deux zones, et une poignée entre elles.
8
+ *
9
+ * Les bornes `minSize` et `maxSize` empêchent de réduire un panneau à
10
+ * rien : une zone à zéro pour cent disparaît avec sa poignée, et on ne peut
11
+ * plus la rouvrir.
12
+ */
6
13
  export function ResizablePanel({ first, second, defaultSize = 50, minSize = 20, maxSize = 80, direction = "horizontal", className }: ResizablePanelProps) { const [size, setSize] = useState(defaultSize); const start = (event: PointerEvent<HTMLDivElement>) => { const container = event.currentTarget.parentElement; if (!container) return; event.currentTarget.setPointerCapture(event.pointerId); const move = (moveEvent: globalThis.PointerEvent) => { const rect = container.getBoundingClientRect(); const next = direction === "horizontal" ? ((moveEvent.clientX - rect.left) / rect.width) * 100 : ((moveEvent.clientY - rect.top) / rect.height) * 100; setSize(Math.min(maxSize, Math.max(minSize, next))); }; const stop = () => { window.removeEventListener("pointermove", move); window.removeEventListener("pointerup", stop); }; window.addEventListener("pointermove", move); window.addEventListener("pointerup", stop); }; const firstStyle = { flexBasis: `${size}%` }; return <div className={cn("sia-resizable-panel", `sia-resizable-panel--${direction}`, className)}><section style={firstStyle}>{first}</section><div className="sia-resizable-panel__handle" role="separator" aria-orientation={direction} aria-valuenow={Math.round(size)} tabIndex={0} onPointerDown={start} onKeyDown={(event) => { const delta = event.key === "ArrowLeft" || event.key === "ArrowUp" ? -5 : event.key === "ArrowRight" || event.key === "ArrowDown" ? 5 : 0; if (delta) { event.preventDefault(); setSize((current) => Math.min(maxSize, Math.max(minSize, current + delta))); } }} /><section>{second}</section></div>; }