@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.
- package/LICENSE +21 -21
- package/dist/index.js +2 -0
- package/dist/registry/package/components/Alert/index.tsx +7 -0
- package/dist/registry/package/components/AmountDisplay/index.tsx +8 -0
- package/dist/registry/package/components/AppShell/index.tsx +413 -3
- package/dist/registry/package/components/AppShell/styles.css +259 -7
- package/dist/registry/package/components/AppShell/tab-bar.tsx +134 -0
- package/dist/registry/package/components/AsyncState/index.tsx +7 -0
- package/dist/registry/package/components/AuditMeta/index.tsx +8 -0
- package/dist/registry/package/components/AuthLayout/index.tsx +7 -0
- package/dist/registry/package/components/Autocomplete/index.tsx +7 -0
- package/dist/registry/package/components/Avatar/index.tsx +333 -12
- package/dist/registry/package/components/Avatar/styles.css +236 -5
- package/dist/registry/package/components/Badge/index.tsx +7 -0
- package/dist/registry/package/components/Button/index.tsx +7 -0
- package/dist/registry/package/components/Calendar/index.tsx +11 -0
- package/dist/registry/package/components/Card/index.tsx +8 -0
- package/dist/registry/package/components/Checkbox/index.tsx +8 -0
- package/dist/registry/package/components/ColorPicker/index.tsx +7 -0
- package/dist/registry/package/components/CommandPalette/index.tsx +7 -0
- package/dist/registry/package/components/ConfirmDialog/index.tsx +7 -0
- package/dist/registry/package/components/Container/index.tsx +7 -0
- package/dist/registry/package/components/Countdown/index.tsx +7 -0
- package/dist/registry/package/components/CrudPage/dialogs.tsx +230 -0
- package/dist/registry/package/components/CrudPage/index.tsx +546 -5
- package/dist/registry/package/components/CrudPage/styles.css +25 -1
- package/dist/registry/package/components/CurrencyInput/index.tsx +7 -0
- package/dist/registry/package/components/DataTable/index.tsx +548 -200
- package/dist/registry/package/components/DataTable/query.ts +68 -0
- package/dist/registry/package/components/DataTable/row-actions.tsx +158 -0
- package/dist/registry/package/components/DataTable/styles.css +285 -73
- package/dist/registry/package/components/DataTable/types.ts +194 -0
- package/dist/registry/package/components/DataTable/values.ts +28 -0
- package/dist/registry/package/components/DatePicker/index.tsx +7 -0
- package/dist/registry/package/components/DateRangeFilter/index.tsx +7 -0
- package/dist/registry/package/components/DateRangePicker/index.tsx +7 -0
- package/dist/registry/package/components/DateTimePicker/index.tsx +7 -0
- package/dist/registry/package/components/Descriptions/index.tsx +7 -0
- package/dist/registry/package/components/Divider/index.tsx +7 -0
- package/dist/registry/package/components/Drawer/index.tsx +8 -0
- package/dist/registry/package/components/DurationDisplay/index.tsx +6 -0
- package/dist/registry/package/components/EmptyState/index.tsx +7 -0
- package/dist/registry/package/components/EntityMeta/index.tsx +7 -0
- package/dist/registry/package/components/ErrorState/index.tsx +8 -0
- package/dist/registry/package/components/EventCalendar/index.tsx +7 -0
- package/dist/registry/package/components/Field/index.tsx +12 -0
- package/dist/registry/package/components/FileUpload/index.tsx +7 -0
- package/dist/registry/package/components/FiltersBar/index.tsx +7 -0
- package/dist/registry/package/components/HoverCard/index.tsx +14 -37
- package/dist/registry/package/components/IconButton/index.tsx +8 -0
- package/dist/registry/package/components/Icons/index.tsx +15 -0
- package/dist/registry/package/components/ImageUpload/index.tsx +7 -0
- package/dist/registry/package/components/Input/index.tsx +7 -0
- package/dist/registry/package/components/JsonEditor/index.tsx +7 -0
- package/dist/registry/package/components/KpiGrid/index.tsx +7 -0
- package/dist/registry/package/components/Label/index.tsx +7 -0
- package/dist/registry/package/components/MarkdownEditor/index.tsx +7 -0
- package/dist/registry/package/components/MiniCalendar/index.tsx +7 -0
- package/dist/registry/package/components/Modal/index.tsx +8 -0
- package/dist/registry/package/components/MonthPicker/index.tsx +7 -0
- package/dist/registry/package/components/MultiSelect/index.tsx +7 -0
- package/dist/registry/package/components/OtpInput/index.tsx +7 -0
- package/dist/registry/package/components/Overlay/index.tsx +34 -24
- package/dist/registry/package/components/PageHeader/index.tsx +7 -0
- package/dist/registry/package/components/PermissionGate/index.tsx +6 -0
- package/dist/registry/package/components/Popover/index.tsx +7 -0
- package/dist/registry/package/components/QrCode/index.tsx +7 -0
- package/dist/registry/package/components/RadioGroup/index.tsx +7 -0
- package/dist/registry/package/components/Rating/index.tsx +6 -0
- package/dist/registry/package/components/ReferenceSelect/index.tsx +7 -0
- package/dist/registry/package/components/RelativeTime/index.tsx +6 -0
- package/dist/registry/package/components/ResizablePanel/index.tsx +7 -0
- package/dist/registry/package/components/Resource/index.tsx +514 -0
- package/dist/registry/package/components/RichTextEditor/index.tsx +7 -0
- package/dist/registry/package/components/SearchInput/index.tsx +8 -0
- package/dist/registry/package/components/Select/index.tsx +11 -0
- package/dist/registry/package/components/Sidebar/index.tsx +489 -113
- package/dist/registry/package/components/Sidebar/styles.css +96 -4
- package/dist/registry/package/components/Skeleton/index.tsx +7 -0
- package/dist/registry/package/components/Spinner/index.tsx +7 -0
- package/dist/registry/package/components/StatCard/index.tsx +7 -0
- package/dist/registry/package/components/Statistic/index.tsx +7 -0
- package/dist/registry/package/components/Switch/index.tsx +7 -0
- package/dist/registry/package/components/Tabs/index.tsx +7 -0
- package/dist/registry/package/components/TagsInput/index.tsx +7 -0
- package/dist/registry/package/components/Textarea/index.tsx +7 -0
- package/dist/registry/package/components/TimePicker/index.tsx +7 -0
- package/dist/registry/package/components/TimeRangePicker/index.tsx +6 -0
- package/dist/registry/package/components/Timeline/index.tsx +7 -0
- package/dist/registry/package/components/Timer/index.tsx +7 -0
- package/dist/registry/package/components/Tour/index.tsx +7 -0
- package/dist/registry/package/components/Transfer/index.tsx +7 -0
- package/dist/registry/package/components/Tree/index.tsx +7 -0
- package/dist/registry/package/components/TreeSelect/index.tsx +7 -0
- package/dist/registry/package/components/Typography/index.tsx +8 -0
- package/dist/registry/package/components/UnsavedChangesGuard/index.tsx +8 -0
- package/dist/registry/package/components/WeekPicker/index.tsx +7 -0
- package/dist/registry/package/components/YearPicker/index.tsx +7 -0
- package/dist/registry/registry.json +69 -14
- 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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
89
|
-
left
|
|
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>; }
|