@sia-ui/cli 0.7.1 → 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.
- package/CHANGELOG.md +6 -0
- package/README.md +6 -0
- package/dist/registry/package/components/AccountMenu/index.tsx +107 -0
- package/dist/registry/package/components/AccountMenu/styles.css +30 -0
- package/dist/registry/package/components/ActivityLog/index.tsx +96 -25
- package/dist/registry/package/components/AmountDisplay/index.tsx +5 -2
- package/dist/registry/package/components/AppShell/index.tsx +44 -8
- package/dist/registry/package/components/AppShell/tab-bar.tsx +6 -3
- package/dist/registry/package/components/AsyncState/index.tsx +29 -7
- package/dist/registry/package/components/AuditMeta/index.tsx +12 -2
- package/dist/registry/package/components/Avatar/index.tsx +4 -9
- package/dist/registry/package/components/Badge/index.tsx +3 -2
- package/dist/registry/package/components/Badge/styles.css +4 -4
- package/dist/registry/package/components/BrandIcon/index.tsx +100 -0
- package/dist/registry/package/components/BrandIcon/styles.css +31 -0
- package/dist/registry/package/components/Breadcrumb/index.tsx +14 -2
- package/dist/registry/package/components/Button/index.tsx +3 -2
- package/dist/registry/package/components/Button/styles.css +4 -4
- package/dist/registry/package/components/Calendar/index.tsx +7 -3
- package/dist/registry/package/components/Card/index.tsx +14 -4
- package/dist/registry/package/components/ColorModeToggle/index.tsx +44 -0
- package/dist/registry/package/components/CommandPalette/index.tsx +3 -2
- package/dist/registry/package/components/ConfirmDialog/index.tsx +24 -4
- package/dist/registry/package/components/CrudPage/dialogs.tsx +53 -8
- package/dist/registry/package/components/CrudPage/index.tsx +99 -25
- package/dist/registry/package/components/CurrencyInput/index.tsx +5 -2
- package/dist/registry/package/components/DataTable/index.tsx +53 -18
- package/dist/registry/package/components/DataTable/row-actions.tsx +20 -8
- package/dist/registry/package/components/DataTable/types.ts +100 -0
- package/dist/registry/package/components/DatePicker/index.tsx +14 -6
- package/dist/registry/package/components/DateRangeFilter/index.tsx +12 -3
- package/dist/registry/package/components/DateRangePicker/index.tsx +13 -4
- package/dist/registry/package/components/DateTimePicker/index.tsx +3 -1
- package/dist/registry/package/components/Drawer/index.tsx +16 -6
- package/dist/registry/package/components/DropdownMenu/index.tsx +37 -4
- package/dist/registry/package/components/DropdownMenu/styles.css +10 -0
- package/dist/registry/package/components/DurationDisplay/index.tsx +20 -2
- package/dist/registry/package/components/EntityMeta/index.tsx +14 -9
- package/dist/registry/package/components/ErrorState/index.tsx +9 -3
- package/dist/registry/package/components/ErrorState/styles.css +1 -1
- package/dist/registry/package/components/EventCalendar/index.tsx +657 -9
- package/dist/registry/package/components/EventCalendar/styles.css +360 -8
- package/dist/registry/package/components/EventManager/index.tsx +456 -0
- package/dist/registry/package/components/EventManager/styles.css +12 -0
- package/dist/registry/package/components/Field/index.tsx +74 -27
- package/dist/registry/package/components/FileUpload/index.tsx +13 -3
- package/dist/registry/package/components/FiltersBar/index.tsx +8 -3
- package/dist/registry/package/components/FiltersBar/styles.css +7 -0
- package/dist/registry/package/components/Form/index.tsx +118 -14
- package/dist/registry/package/components/Form/styles.css +3 -0
- package/dist/registry/package/components/HoverCard/index.tsx +8 -4
- package/dist/registry/package/components/Icons/index.tsx +18 -0
- package/dist/registry/package/components/ImageUpload/index.tsx +17 -2
- package/dist/registry/package/components/Input/index.tsx +3 -2
- package/dist/registry/package/components/JobProgress/index.tsx +289 -0
- package/dist/registry/package/components/JobProgress/styles.css +58 -0
- package/dist/registry/package/components/JsonEditor/index.tsx +18 -3
- package/dist/registry/package/components/JsonView/index.tsx +282 -0
- package/dist/registry/package/components/JsonView/styles.css +99 -0
- package/dist/registry/package/components/LiveIndicator/index.tsx +43 -0
- package/dist/registry/package/components/LiveIndicator/styles.css +41 -0
- package/dist/registry/package/components/LogStream/index.tsx +545 -0
- package/dist/registry/package/components/LogStream/styles.css +152 -0
- package/dist/registry/package/components/MarkdownEditor/index.tsx +14 -3
- package/dist/registry/package/components/Modal/index.tsx +7 -1
- package/dist/registry/package/components/MultiSelect/index.tsx +14 -3
- package/dist/registry/package/components/OtpInput/index.tsx +2 -1
- package/dist/registry/package/components/Pagination/index.tsx +13 -2
- package/dist/registry/package/components/PasswordInput/index.tsx +23 -9
- package/dist/registry/package/components/PhoneInput/index.tsx +66 -30
- package/dist/registry/package/components/Popover/index.tsx +19 -4
- package/dist/registry/package/components/QrCode/index.tsx +3 -2
- package/dist/registry/package/components/RadioGroup/styles.css +3 -0
- package/dist/registry/package/components/Rating/index.tsx +6 -2
- package/dist/registry/package/components/ReferenceSelect/index.tsx +8 -1
- package/dist/registry/package/components/RelativeTime/index.tsx +5 -1
- package/dist/registry/package/components/RequireSession/index.tsx +10 -4
- package/dist/registry/package/components/Resource/index.tsx +70 -17
- package/dist/registry/package/components/RichTextEditor/index.tsx +2 -1
- package/dist/registry/package/components/SearchInput/index.tsx +4 -1
- package/dist/registry/package/components/SecretFields/index.tsx +70 -17
- package/dist/registry/package/components/Select/index.tsx +34 -12
- package/dist/registry/package/components/Sidebar/index.tsx +22 -6
- package/dist/registry/package/components/Slider/index.tsx +26 -5
- package/dist/registry/package/components/Spinner/index.tsx +3 -2
- package/dist/registry/package/components/StatCard/index.tsx +11 -3
- package/dist/registry/package/components/Statistic/index.tsx +5 -1
- package/dist/registry/package/components/Steps/styles.css +2 -2
- package/dist/registry/package/components/TagsInput/index.tsx +2 -1
- package/dist/registry/package/components/TimePicker/index.tsx +10 -4
- package/dist/registry/package/components/Toast/index.tsx +20 -5
- package/dist/registry/package/components/Tour/index.tsx +2 -1
- package/dist/registry/package/components/Transfer/index.tsx +2 -1
- package/dist/registry/package/components/Tree/index.tsx +3 -2
- package/dist/registry/package/components/TreeSelect/index.tsx +17 -4
- package/dist/registry/package/components/UnsavedChangesGuard/index.tsx +34 -3
- package/dist/registry/registry.json +257 -75
- package/dist/registry/styles/sia-ui.css +23 -4
- package/package.json +5 -5
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type HTMLAttributes } from "react";
|
|
2
2
|
import { cn } from "@sia-ui/utils";
|
|
3
3
|
import type { ComponentTone } from "@sia-ui/tokens";
|
|
4
|
-
import { useComponentDefaults } from "@sia-ui/headless";
|
|
4
|
+
import { useComponentDefaults, useSiaLocale } from "@sia-ui/headless";
|
|
5
5
|
import "./styles.css";
|
|
6
6
|
|
|
7
7
|
export interface SpinnerProps extends HTMLAttributes<HTMLSpanElement> {
|
|
@@ -20,7 +20,8 @@ export interface SpinnerProps extends HTMLAttributes<HTMLSpanElement> {
|
|
|
20
20
|
*/
|
|
21
21
|
export function Spinner({ label: labelProp, size: sizeProp, variant: variantProp, tone: toneProp, className, ...props }: SpinnerProps) {
|
|
22
22
|
const defaults = useComponentDefaults<SpinnerProps>("spinner");
|
|
23
|
-
const
|
|
23
|
+
const locale = useSiaLocale();
|
|
24
|
+
const label = labelProp ?? defaults.label ?? locale.loading;
|
|
24
25
|
const size = sizeProp ?? defaults.size ?? "md";
|
|
25
26
|
const variant = variantProp ?? defaults.variant ?? "ring";
|
|
26
27
|
const tone = toneProp ?? defaults.tone ?? "current";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
import { cn } from "@sia-ui/utils";
|
|
3
|
-
import { Card, CardBody } from "../Card";
|
|
3
|
+
import { Card, CardBody, type CardBodyProps, type CardProps } from "../Card";
|
|
4
4
|
import { Statistic, type StatisticProps } from "../Statistic";
|
|
5
5
|
import "./styles.css";
|
|
6
6
|
|
|
@@ -8,6 +8,14 @@ export interface StatCardProps extends StatisticProps {
|
|
|
8
8
|
icon?: ReactNode;
|
|
9
9
|
description?: ReactNode;
|
|
10
10
|
action?: ReactNode;
|
|
11
|
+
/**
|
|
12
|
+
* Les props de la `Card` qui entoure l'indicateur — un `id`, un
|
|
13
|
+
* `aria-label`, un gestionnaire de clic. Sans `children` : le contenu est
|
|
14
|
+
* celui de l'indicateur. `className` s'ajoute à celui de la carte.
|
|
15
|
+
*/
|
|
16
|
+
cardProps?: Partial<Omit<CardProps, "children">>;
|
|
17
|
+
/** Les props du `CardBody`, sans `children`, rempli par l'indicateur. */
|
|
18
|
+
cardBodyProps?: Partial<Omit<CardBodyProps, "children">>;
|
|
11
19
|
}
|
|
12
20
|
|
|
13
21
|
/**
|
|
@@ -17,6 +25,6 @@ export interface StatCardProps extends StatisticProps {
|
|
|
17
25
|
* presque toujours ensemble, et qu'il serait fastidieux de recomposer à
|
|
18
26
|
* chaque tableau de bord.
|
|
19
27
|
*/
|
|
20
|
-
export function StatCard({ icon, description, action, className, ...statisticProps }: StatCardProps) {
|
|
21
|
-
return <Card className={cn("sia-stat-card", className)}><CardBody><div className="sia-stat-card__top">{icon && <span className="sia-stat-card__icon" aria-hidden="true">{icon}</span>}{action}</div><Statistic {...statisticProps} />{description && <div className="sia-stat-card__description">{description}</div>}</CardBody></Card>;
|
|
28
|
+
export function StatCard({ icon, description, action, className, cardProps, cardBodyProps, ...statisticProps }: StatCardProps) {
|
|
29
|
+
return <Card {...cardProps} className={cn("sia-stat-card", className, cardProps?.className)}><CardBody {...cardBodyProps}><div className="sia-stat-card__top">{icon && <span className="sia-stat-card__icon" aria-hidden="true">{icon}</span>}{action}</div><Statistic {...statisticProps} />{description && <div className="sia-stat-card__description">{description}</div>}</CardBody></Card>;
|
|
22
30
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
import { cn, formatCurrency } from "@sia-ui/utils";
|
|
3
|
+
import { useSiaLocale } from "@sia-ui/headless";
|
|
3
4
|
import "./styles.css";
|
|
4
5
|
|
|
5
6
|
export interface StatisticProps {
|
|
6
7
|
value: number;
|
|
7
8
|
label?: ReactNode;
|
|
8
9
|
format?: "number" | "currency" | "percent";
|
|
10
|
+
/** La langue du format. Par défaut, celle de la locale SIA. */
|
|
9
11
|
locale?: string;
|
|
10
12
|
currency?: string;
|
|
11
13
|
precision?: number;
|
|
@@ -22,7 +24,9 @@ export interface StatisticProps {
|
|
|
22
24
|
* décide du sens et de la couleur. Une flèche passée en prop finit par
|
|
23
25
|
* pointer vers le haut sur une baisse.
|
|
24
26
|
*/
|
|
25
|
-
export function Statistic({ value, label, format = "number", locale
|
|
27
|
+
export function Statistic({ value, label, format = "number", locale: localeProp, currency = "XAF", precision, prefix, suffix, trend, className }: StatisticProps) {
|
|
28
|
+
const messages = useSiaLocale();
|
|
29
|
+
const locale = localeProp ?? messages.language;
|
|
26
30
|
const formatted = format === "currency"
|
|
27
31
|
? formatCurrency(value, { locale, currency, ...(precision !== undefined ? { minimumFractionDigits: precision, maximumFractionDigits: precision } : {}) })
|
|
28
32
|
: new Intl.NumberFormat(locale, format === "percent" ? { style: "percent", ...(precision !== undefined ? { minimumFractionDigits: precision, maximumFractionDigits: precision } : {}) } : { ...(precision !== undefined ? { minimumFractionDigits: precision, maximumFractionDigits: precision } : {}) }).format(value);
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
.sia-steps__item[data-status="done"] .sia-steps__bullet {
|
|
35
35
|
border-color: var(--sia-success);
|
|
36
36
|
background: var(--sia-success);
|
|
37
|
-
color:
|
|
37
|
+
color: var(--sia-success-foreground);
|
|
38
38
|
/* La coche tombe avec un léger rebond quand l'étape bascule. */
|
|
39
39
|
animation: sia-step-done var(--sia-duration-slow) var(--sia-easing-spring);
|
|
40
40
|
}
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
.sia-steps__item[data-status="error"] .sia-steps__bullet {
|
|
69
69
|
border-color: var(--sia-danger);
|
|
70
70
|
background: var(--sia-danger);
|
|
71
|
-
color:
|
|
71
|
+
color: var(--sia-danger-foreground);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
.sia-steps__connector { position: relative; overflow: hidden; background: var(--sia-border); }
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState, type KeyboardEvent } from "react";
|
|
2
2
|
import { cn } from "@sia-ui/utils";
|
|
3
|
+
import { formatMessage, useSiaLocale } from "@sia-ui/headless";
|
|
3
4
|
import { XIcon } from "../Icons";
|
|
4
5
|
import "./styles.css";
|
|
5
6
|
export interface TagsInputProps { id?: string; value?: string[]; defaultValue?: string[]; onValueChange?: (value: string[]) => void; placeholder?: string; separators?: string[]; maxTags?: number; disabled?: boolean; className?: string; "aria-describedby"?: string; "aria-labelledby"?: string; "aria-invalid"?: boolean; "aria-required"?: boolean; }
|
|
@@ -10,4 +11,4 @@ export interface TagsInputProps { id?: string; value?: string[]; defaultValue?:
|
|
|
10
11
|
* que ce sont les deux touches que l'on presse sans y penser après avoir
|
|
11
12
|
* tapé un mot.
|
|
12
13
|
*/
|
|
13
|
-
export function TagsInput({ id, value, defaultValue = [], onValueChange, placeholder
|
|
14
|
+
export function TagsInput({ id, value, defaultValue = [], onValueChange, placeholder: placeholderProp, separators = ["Enter", ","], maxTags, disabled, className, ...ariaProps }: TagsInputProps) { const messages = useSiaLocale().tagsInput; const placeholder = placeholderProp ?? messages.placeholder; const [internal, setInternal] = useState(defaultValue); const [draft, setDraft] = useState(""); const current = value ?? internal; const update = (next: string[]) => { if (value === undefined) setInternal(next); onValueChange?.(next); }; const add = () => { const tag = draft.trim().replace(/,$/, ""); if (!tag || current.includes(tag) || (maxTags !== undefined && current.length >= maxTags)) return; update([...current, tag]); setDraft(""); }; return <div id={id} className={cn("sia-tags-input", className)} {...ariaProps}>{current.map((tag) => <span key={tag}>{tag}<button type="button" disabled={disabled} aria-label={formatMessage(messages.removeTag, { name: tag })} onClick={() => update(current.filter((item) => item !== tag))}><XIcon /></button></span>)}<input value={draft} disabled={disabled} placeholder={placeholder} onChange={(event) => setDraft(event.target.value)} onBlur={add} onKeyDown={(event: KeyboardEvent<HTMLInputElement>) => { if (separators.includes(event.key)) { event.preventDefault(); add(); } if (event.key === "Backspace" && !draft && current.length) update(current.slice(0, -1)); }} /></div>; }
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { forwardRef, useId, useMemo, useRef, 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 { CheckIcon, ChevronDownIcon, ClockIcon, XIcon } from "../Icons";
|
|
5
|
-
import { Overlay, type OverlayPlacement } from "../Overlay";
|
|
6
|
+
import { Overlay, type OverlayPlacement, type OverlayProps } from "../Overlay";
|
|
6
7
|
import { formatTimeValue, parseTimeValue, type TimeParts } from "../Timer";
|
|
7
8
|
import "./styles.css";
|
|
8
9
|
|
|
@@ -19,6 +20,7 @@ export interface TimePickerProps {
|
|
|
19
20
|
hourFormat?: 12 | 24;
|
|
20
21
|
needConfirm?: boolean;
|
|
21
22
|
allowClear?: boolean;
|
|
23
|
+
/** Par défaut, « Sélectionner une heure » de la locale SIA. */
|
|
22
24
|
placeholder?: string;
|
|
23
25
|
disabledTime?: (value: TimeParts) => boolean;
|
|
24
26
|
renderExtraFooter?: () => ReactNode;
|
|
@@ -26,6 +28,8 @@ export interface TimePickerProps {
|
|
|
26
28
|
onOpenChange?: (open: boolean) => void;
|
|
27
29
|
open?: boolean;
|
|
28
30
|
placement?: OverlayPlacement;
|
|
31
|
+
/** Réglages du panneau flottant — décalage, marge au bord, classe. */
|
|
32
|
+
overlayProps?: Partial<Omit<OverlayProps, "open" | "anchorRef" | "onOpenChange" | "children">>;
|
|
29
33
|
tone?: ComponentTone;
|
|
30
34
|
invalid?: boolean;
|
|
31
35
|
disabled?: boolean;
|
|
@@ -50,8 +54,10 @@ function steppedValues(limit: number, step: number) {
|
|
|
50
54
|
* demande — la plupart des heures métier n'en ont pas.
|
|
51
55
|
*/
|
|
52
56
|
export const TimePicker = forwardRef<HTMLButtonElement, TimePickerProps>(function TimePicker({
|
|
53
|
-
id, name, value, defaultValue = "", min, max, showSeconds = false, minuteStep = 1, secondStep = 1, hourFormat = 24, needConfirm = false, allowClear = true, placeholder
|
|
57
|
+
id, name, value, defaultValue = "", min, max, showSeconds = false, minuteStep = 1, secondStep = 1, hourFormat = 24, needConfirm = false, allowClear = true, placeholder: placeholderProp, disabledTime, renderExtraFooter, onValueChange, onOpenChange, open: openProp, placement = "bottom-start", overlayProps, tone = "primary", invalid = false, disabled = false, required = false, className, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, ...ariaProps
|
|
54
58
|
}, forwardedRef) {
|
|
59
|
+
const locale = useSiaLocale();
|
|
60
|
+
const placeholder = placeholderProp ?? locale.selectTime;
|
|
55
61
|
const generatedId = useId();
|
|
56
62
|
const controlId = id ?? `sia-time-picker-${generatedId.replace(/:/g, "")}`;
|
|
57
63
|
const anchorRef = useRef<HTMLButtonElement>(null);
|
|
@@ -76,8 +82,8 @@ export const TimePicker = forwardRef<HTMLButtonElement, TimePickerProps>(functio
|
|
|
76
82
|
const renderColumn = (label: string, values: number[], selected: number, key: keyof TimeParts) => <div className="sia-time-panel__column" role="listbox" aria-label={label}>{values.map((option) => { const candidate = { ...draft, [key]: option }; const serialized = formatTimeValue(candidate, showSeconds); const optionDisabled = Boolean((min && serialized < min) || (max && serialized > max) || disabledTime?.(candidate)); const display = key === "hours" && hourFormat === 12 ? ((option + 11) % 12) + 1 : option; return <button type="button" role="option" aria-selected={selected === option} disabled={optionDisabled} key={option} onClick={() => select({ [key]: option })}><span>{String(display).padStart(2, "0")}</span>{selected === option && <CheckIcon />}</button>; })}</div>;
|
|
77
83
|
return <div className={cn("sia-picker", "sia-time-picker", toneClass("sia-tone", tone), invalid && "sia-picker--invalid", disabled && "sia-picker--disabled", className)}>
|
|
78
84
|
{name && <input type="hidden" name={name} value={current} />}
|
|
79
|
-
<button ref={setRef} id={controlId} type="button" className="sia-picker__trigger" disabled={disabled} aria-haspopup="dialog" aria-expanded={open} aria-invalid={ariaInvalid ?? (invalid || undefined)} aria-required={ariaRequired ?? (required || undefined)} onClick={() => setOpen(!open)} {...ariaProps}><ClockIcon className="sia-picker__icon" /><span className={cn("sia-picker__value", !current && "sia-picker__placeholder")}>{current || placeholder}</span><span className="sia-picker__actions">{allowClear && current && <span className="sia-picker__clear" title=
|
|
80
|
-
<Overlay open={open} anchorRef={anchorRef} onOpenChange={setOpen}
|
|
85
|
+
<button ref={setRef} id={controlId} type="button" className="sia-picker__trigger" disabled={disabled} aria-haspopup="dialog" aria-expanded={open} aria-invalid={ariaInvalid ?? (invalid || undefined)} aria-required={ariaRequired ?? (required || undefined)} onClick={() => setOpen(!open)} {...ariaProps}><ClockIcon className="sia-picker__icon" /><span className={cn("sia-picker__value", !current && "sia-picker__placeholder")}>{current || placeholder}</span><span className="sia-picker__actions">{allowClear && current && <span className="sia-picker__clear" title={locale.clear} aria-hidden="true" onClick={(event) => { event.stopPropagation(); update(""); }}><XIcon /></span>}<ChevronDownIcon /></span></button>
|
|
86
|
+
<Overlay placement={placement} {...overlayProps} className={cn("sia-picker__overlay", overlayProps?.className)} open={open} anchorRef={anchorRef} onOpenChange={setOpen}><div className={cn("sia-time-panel", toneClass("sia-tone", tone))}><div className="sia-time-panel__columns">{renderColumn(locale.timePicker.hours, hours, draft.hours, "hours")}{renderColumn(locale.timePicker.minutes, minutes, draft.minutes, "minutes")}{showSeconds && renderColumn(locale.timePicker.seconds, seconds, draft.seconds, "seconds")}</div><footer className="sia-time-panel__footer">{renderExtraFooter?.()}<button type="button" className="sia-time-panel__now" onClick={() => setDraft(parseTimeValue(new Date().toTimeString().slice(0, 8)))}>{locale.timePicker.now}</button>{needConfirm && <button type="button" className="sia-time-panel__confirm" onClick={confirm}>{locale.timePicker.ok}</button>}</footer></div></Overlay>
|
|
81
87
|
</div>;
|
|
82
88
|
});
|
|
83
89
|
TimePicker.displayName = "TimePicker";
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
} from "@sia-ui/headless";
|
|
25
25
|
import { usePortal } from "@sia-ui/react";
|
|
26
26
|
import { AlertCircleIcon, CheckIcon, InfoIcon, XIcon } from "../Icons";
|
|
27
|
-
import { Spinner } from "../Spinner";
|
|
27
|
+
import { Spinner, type SpinnerProps } from "../Spinner";
|
|
28
28
|
import "./styles.css";
|
|
29
29
|
|
|
30
30
|
export type { ToastId, ToastOptions, ToastPlacement, ToastTone };
|
|
@@ -38,14 +38,15 @@ export type { ToastId, ToastOptions, ToastPlacement, ToastTone };
|
|
|
38
38
|
*/
|
|
39
39
|
export { toast, toastStore };
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
// Le ton `loading` n'y figure pas : son `Spinner` se règle par
|
|
42
|
+
// `spinnerProps`, il est donc rendu par l'annonce elle-même.
|
|
43
|
+
const TONE_ICON: Record<Exclude<ToastTone, "loading">, ReactNode> = {
|
|
42
44
|
neutral: null,
|
|
43
45
|
primary: <InfoIcon />,
|
|
44
46
|
info: <InfoIcon />,
|
|
45
47
|
success: <CheckIcon />,
|
|
46
48
|
warning: <AlertCircleIcon />,
|
|
47
49
|
danger: <AlertCircleIcon />,
|
|
48
|
-
loading: <Spinner size="sm" tone="current" />,
|
|
49
50
|
};
|
|
50
51
|
|
|
51
52
|
/** Le temps laissé à l'animation de sortie avant le retrait définitif. */
|
|
@@ -80,6 +81,12 @@ export interface ToasterProps {
|
|
|
80
81
|
*/
|
|
81
82
|
container?: HTMLElement | null | undefined;
|
|
82
83
|
className?: string;
|
|
84
|
+
/**
|
|
85
|
+
* Les props du `Spinner` des annonces en attente (`toast.loading`,
|
|
86
|
+
* `toast.promise`) — variante, taille. Sans effet sur une annonce qui
|
|
87
|
+
* fournit sa propre `icon`.
|
|
88
|
+
*/
|
|
89
|
+
spinnerProps?: Partial<SpinnerProps>;
|
|
83
90
|
}
|
|
84
91
|
|
|
85
92
|
/**
|
|
@@ -98,6 +105,7 @@ export function Toaster({
|
|
|
98
105
|
offset = "1rem",
|
|
99
106
|
container,
|
|
100
107
|
className,
|
|
108
|
+
spinnerProps,
|
|
101
109
|
}: ToasterProps) {
|
|
102
110
|
const records = useToasts<ReactNode>();
|
|
103
111
|
const node = usePortal(container ? { container } : { id: "sia-toaster" });
|
|
@@ -152,6 +160,7 @@ export function Toaster({
|
|
|
152
160
|
record={record}
|
|
153
161
|
placement={corner}
|
|
154
162
|
closeLabel={locale.close}
|
|
163
|
+
{...(spinnerProps ? { spinnerProps } : {})}
|
|
155
164
|
/>
|
|
156
165
|
))}
|
|
157
166
|
</div>
|
|
@@ -166,11 +175,17 @@ interface ToastItemProps {
|
|
|
166
175
|
record: ToastRecord<ReactNode>;
|
|
167
176
|
placement: ToastPlacement;
|
|
168
177
|
closeLabel: string;
|
|
178
|
+
spinnerProps?: Partial<SpinnerProps>;
|
|
169
179
|
}
|
|
170
180
|
|
|
171
|
-
function ToastItem({ record, placement, closeLabel }: ToastItemProps) {
|
|
181
|
+
function ToastItem({ record, placement, closeLabel, spinnerProps }: ToastItemProps) {
|
|
172
182
|
const tone = record.tone ?? "neutral";
|
|
173
|
-
const icon =
|
|
183
|
+
const icon =
|
|
184
|
+
record.icon !== undefined
|
|
185
|
+
? record.icon
|
|
186
|
+
: tone === "loading"
|
|
187
|
+
? <Spinner size="sm" tone="current" {...spinnerProps} />
|
|
188
|
+
: TONE_ICON[tone];
|
|
174
189
|
const closable = record.closable ?? tone !== "loading";
|
|
175
190
|
const duration = toastStore.durationOf(record);
|
|
176
191
|
const timed = Number.isFinite(duration) && duration > 0;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useState, type ReactNode } from "react";
|
|
2
2
|
import { createPortal } from "react-dom";
|
|
3
3
|
import { cn } from "@sia-ui/utils";
|
|
4
|
+
import { formatMessage, useSiaLocale } from "@sia-ui/headless";
|
|
4
5
|
import "./styles.css";
|
|
5
6
|
|
|
6
7
|
export interface TourStep { target: string; title: ReactNode; description?: ReactNode; }
|
|
@@ -12,4 +13,4 @@ export interface TourProps { open: boolean; onOpenChange: (open: boolean) => voi
|
|
|
12
13
|
* référence : une visite se déclare souvent loin des composants qu'elle
|
|
13
14
|
* montre, et parfois avant qu'ils n'existent.
|
|
14
15
|
*/
|
|
15
|
-
export function Tour({ open, onOpenChange, steps, initialStep = 0, onStepChange, className }: TourProps) { const [index, setIndex] = useState(initialStep); useEffect(() => { if (open) setIndex(initialStep); }, [open, initialStep]); useEffect(() => { if (!open) return; const target = document.querySelector<HTMLElement>(steps[index]?.target ?? ""); target?.scrollIntoView({ block: "center", behavior: "smooth" }); target?.setAttribute("data-sia-tour-active", "true"); return () => target?.removeAttribute("data-sia-tour-active"); }, [open, index, steps]); if (!open || typeof document === "undefined" || steps.length === 0) return null; const step = steps[index]; const move = (next: number) => { if (next >= steps.length) { onOpenChange(false); return; } setIndex(next); onStepChange?.(next); }; return createPortal(<div className="sia-tour-backdrop"><section className={cn("sia-tour", className)} role="dialog" aria-modal="true"><small>{index + 1
|
|
16
|
+
export function Tour({ open, onOpenChange, steps, initialStep = 0, onStepChange, className }: TourProps) { const locale = useSiaLocale(); const [index, setIndex] = useState(initialStep); useEffect(() => { if (open) setIndex(initialStep); }, [open, initialStep]); useEffect(() => { if (!open) return; const target = document.querySelector<HTMLElement>(steps[index]?.target ?? ""); target?.scrollIntoView({ block: "center", behavior: "smooth" }); target?.setAttribute("data-sia-tour-active", "true"); return () => target?.removeAttribute("data-sia-tour-active"); }, [open, index, steps]); if (!open || typeof document === "undefined" || steps.length === 0) return null; const step = steps[index]; const move = (next: number) => { if (next >= steps.length) { onOpenChange(false); return; } setIndex(next); onStepChange?.(next); }; return createPortal(<div className="sia-tour-backdrop"><section className={cn("sia-tour", className)} role="dialog" aria-modal="true"><small>{formatMessage(locale.tour.progress, { current: index + 1, total: steps.length })}</small><h2>{step?.title}</h2>{step?.description && <p>{step.description}</p>}<footer><button type="button" onClick={() => onOpenChange(false)}>{locale.close}</button><button type="button" disabled={index === 0} onClick={() => move(index - 1)}>{locale.previous}</button><button type="button" onClick={() => move(index + 1)}>{index === steps.length - 1 ? locale.tour.finish : locale.next}</button></footer></section></div>, document.body); }
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useMemo, useState, type Key, 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 TransferItem { key: Key; label: ReactNode; disabled?: boolean; }
|
|
@@ -11,4 +12,4 @@ export interface TransferProps { items: TransferItem[]; targetKeys: Key[]; onCha
|
|
|
11
12
|
* cocher de deux cents lignes ne montre plus ce qui est retenu, alors que
|
|
12
13
|
* la colonne de droite ne contient que cela.
|
|
13
14
|
*/
|
|
14
|
-
export function Transfer({ items, targetKeys, onChange, sourceTitle
|
|
15
|
+
export function Transfer({ items, targetKeys, onChange, sourceTitle: sourceTitleProp, targetTitle: targetTitleProp, className }: TransferProps) { const messages = useSiaLocale().transfer; const sourceTitle = sourceTitleProp ?? messages.sourceTitle; const targetTitle = targetTitleProp ?? messages.targetTitle; const [checked, setChecked] = useState<Key[]>([]); const target = useMemo(() => new Set(targetKeys), [targetKeys]); const renderList = (list: TransferItem[], title: string) => <section><header>{title} <span>{list.length}</span></header>{list.map((item) => <label key={item.key}><input type="checkbox" disabled={item.disabled} checked={checked.includes(item.key)} onChange={(event) => setChecked((current) => event.target.checked ? [...current, item.key] : current.filter((key) => key !== item.key))} />{item.label}</label>)}</section>; const sourceItems = items.filter((item) => !target.has(item.key)); const targetItems = items.filter((item) => target.has(item.key)); return <div className={cn("sia-transfer", className)}>{renderList(sourceItems, sourceTitle)}<div className="sia-transfer__actions"><button type="button" aria-label={messages.moveToTarget} onClick={() => { onChange([...new Set([...targetKeys, ...checked])]); setChecked([]); }}>→</button><button type="button" aria-label={messages.moveToSource} onClick={() => { onChange(targetKeys.filter((key) => !checked.includes(key))); setChecked([]); }}>←</button></div>{renderList(targetItems, targetTitle)}</div>; }
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { useState, type Key, type ReactNode } from "react";
|
|
2
2
|
import { cn } from "@sia-ui/utils";
|
|
3
|
+
import { useSiaLocale, type TreeMessages } from "@sia-ui/headless";
|
|
3
4
|
import "./styles.css";
|
|
4
5
|
|
|
5
6
|
export interface TreeNode { key: Key; label: ReactNode; children?: TreeNode[]; disabled?: boolean; }
|
|
6
7
|
export interface TreeProps { nodes: TreeNode[]; defaultExpandedKeys?: Key[]; selectedKey?: Key | undefined; onSelect?: ((node: TreeNode) => void) | undefined; className?: string; }
|
|
7
|
-
function TreeBranch({ nodes, level, expanded, toggle, selectedKey, onSelect }: { nodes: TreeNode[]; level: number; expanded: Set<Key>; toggle: (key: Key) => void; selectedKey?: Key | undefined; onSelect?: ((node: TreeNode) => void) | undefined }) { return <ul role={level === 1 ? "tree" : "group"}>{nodes.map((node) => { const hasChildren = Boolean(node.children?.length); const isExpanded = expanded.has(node.key); return <li key={node.key} role="treeitem" aria-expanded={hasChildren ? isExpanded : undefined} aria-selected={selectedKey === node.key} aria-disabled={node.disabled || undefined}><div className="sia-tree__row" style={{ paddingInlineStart: `${(level - 1) * 1.25}rem` }}>{hasChildren ? <button type="button" aria-label={isExpanded ?
|
|
8
|
+
function TreeBranch({ nodes, level, expanded, toggle, selectedKey, onSelect, messages }: { nodes: TreeNode[]; level: number; expanded: Set<Key>; toggle: (key: Key) => void; selectedKey?: Key | undefined; onSelect?: ((node: TreeNode) => void) | undefined; messages: TreeMessages }) { return <ul role={level === 1 ? "tree" : "group"}>{nodes.map((node) => { const hasChildren = Boolean(node.children?.length); const isExpanded = expanded.has(node.key); return <li key={node.key} role="treeitem" aria-expanded={hasChildren ? isExpanded : undefined} aria-selected={selectedKey === node.key} aria-disabled={node.disabled || undefined}><div className="sia-tree__row" style={{ paddingInlineStart: `${(level - 1) * 1.25}rem` }}>{hasChildren ? <button type="button" aria-label={isExpanded ? messages.collapse : messages.expand} onClick={() => toggle(node.key)}>{isExpanded ? "−" : "+"}</button> : <span className="sia-tree__spacer" />}<button type="button" disabled={node.disabled} className="sia-tree__label" onClick={() => onSelect?.(node)}>{node.label}</button></div>{hasChildren && isExpanded && <TreeBranch nodes={node.children ?? []} level={level + 1} expanded={expanded} toggle={toggle} selectedKey={selectedKey} onSelect={onSelect} messages={messages} />}</li>; })}</ul>; }
|
|
8
9
|
/**
|
|
9
10
|
* Une arborescence qu'on déplie.
|
|
10
11
|
*
|
|
@@ -12,4 +13,4 @@ function TreeBranch({ nodes, level, expanded, toggle, selectedKey, onSelect }: {
|
|
|
12
13
|
* nœud ne désélectionne pas ce qu'il contient, et rouvrir l'écran ne doit
|
|
13
14
|
* pas tout refermer.
|
|
14
15
|
*/
|
|
15
|
-
export function Tree({ nodes, defaultExpandedKeys = [], selectedKey, onSelect, className }: TreeProps) { const [expanded, setExpanded] = useState(new Set(defaultExpandedKeys)); const toggle = (key: Key) => setExpanded((current) => { const next = new Set(current); if (next.has(key)) next.delete(key); else next.add(key); return next; }); return <div className={cn("sia-tree", className)}><TreeBranch nodes={nodes} level={1} expanded={expanded} toggle={toggle} selectedKey={selectedKey} onSelect={onSelect} /></div>; }
|
|
16
|
+
export function Tree({ nodes, defaultExpandedKeys = [], selectedKey, onSelect, className }: TreeProps) { const messages = useSiaLocale().tree; const [expanded, setExpanded] = useState(new Set(defaultExpandedKeys)); const toggle = (key: Key) => setExpanded((current) => { const next = new Set(current); if (next.has(key)) next.delete(key); else next.add(key); return next; }); return <div className={cn("sia-tree", className)}><TreeBranch nodes={nodes} level={1} expanded={expanded} toggle={toggle} selectedKey={selectedKey} onSelect={onSelect} messages={messages} /></div>; }
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import { useState, type Key } from "react";
|
|
2
|
-
import { Tree, type TreeNode } from "../Tree";
|
|
2
|
+
import { Tree, type TreeNode, type TreeProps } from "../Tree";
|
|
3
3
|
import { cn } from "@sia-ui/utils";
|
|
4
4
|
import { useSiaLocale } from "@sia-ui/headless";
|
|
5
5
|
import "./styles.css";
|
|
6
6
|
|
|
7
|
-
export interface TreeSelectProps {
|
|
7
|
+
export interface TreeSelectProps {
|
|
8
|
+
nodes: TreeNode[];
|
|
9
|
+
value?: Key | undefined;
|
|
10
|
+
onValueChange?: (value: Key, node: TreeNode) => void;
|
|
11
|
+
placeholder?: string;
|
|
12
|
+
className?: string;
|
|
13
|
+
/**
|
|
14
|
+
* L'arbre du panneau : nœuds dépliés à l'ouverture, classe.
|
|
15
|
+
*
|
|
16
|
+
* Les nœuds, la sélection et son rappel restent au sélecteur, qui se
|
|
17
|
+
* referme sur le choix.
|
|
18
|
+
*/
|
|
19
|
+
treeProps?: Partial<Omit<TreeProps, "nodes" | "selectedKey" | "onSelect">>;
|
|
20
|
+
}
|
|
8
21
|
/**
|
|
9
22
|
* Un choix dans une arborescence.
|
|
10
23
|
*
|
|
@@ -12,6 +25,6 @@ export interface TreeSelectProps { nodes: TreeNode[]; value?: Key | undefined; o
|
|
|
12
25
|
* attente » ne se réduit pas à « En attente » sans devenir ambigu dès qu'un
|
|
13
26
|
* autre nœud porte le même nom.
|
|
14
27
|
*/
|
|
15
|
-
export function TreeSelect({ nodes, value, onValueChange, placeholder: placeholderProp, className }: TreeSelectProps) {
|
|
28
|
+
export function TreeSelect({ nodes, value, onValueChange, placeholder: placeholderProp, className, treeProps }: TreeSelectProps) {
|
|
16
29
|
const locale = useSiaLocale();
|
|
17
|
-
const placeholder = placeholderProp ?? locale.select; const [open, setOpen] = useState(false); const find = (items: TreeNode[]): TreeNode | undefined => { for (const item of items) { if (item.key === value) return item; const child = item.children ? find(item.children) : undefined; if (child) return child; } }; const selected = find(nodes); return <div className={cn("sia-tree-select", className)}><button type="button" className="sia-tree-select__trigger" aria-expanded={open} onClick={() => setOpen(!open)}>{selected?.label ?? placeholder}</button>{open && <div className="sia-tree-select__panel"><Tree
|
|
30
|
+
const placeholder = placeholderProp ?? locale.select; const [open, setOpen] = useState(false); const find = (items: TreeNode[]): TreeNode | undefined => { for (const item of items) { if (item.key === value) return item; const child = item.children ? find(item.children) : undefined; if (child) return child; } }; const selected = find(nodes); return <div className={cn("sia-tree-select", className)}><button type="button" className="sia-tree-select__trigger" aria-expanded={open} onClick={() => setOpen(!open)}>{selected?.label ?? placeholder}</button>{open && <div className="sia-tree-select__panel"><Tree defaultExpandedKeys={nodes.map((node) => node.key)} {...treeProps} nodes={nodes} selectedKey={value} onSelect={(node) => { onValueChange?.(node.key, node); setOpen(false); }} /></div>}</div>; }
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { useEffect } from "react";
|
|
2
|
-
|
|
2
|
+
import { FRENCH, useSiaLocale } from "@sia-ui/headless";
|
|
3
|
+
export interface UnsavedChangesGuardProps {
|
|
4
|
+
when: boolean;
|
|
5
|
+
/** Le message de la confirmation. Par défaut, `unsavedChangesGuard.message` de la locale. */
|
|
6
|
+
message?: string;
|
|
7
|
+
}
|
|
3
8
|
/**
|
|
4
9
|
* L'avertissement avant de quitter une saisie en cours.
|
|
5
10
|
*
|
|
@@ -8,5 +13,31 @@ export interface UnsavedChangesGuardProps { when: boolean; message?: string; }
|
|
|
8
13
|
* confirmation interne, que `confirmUnsavedNavigation` déclenche avant une
|
|
9
14
|
* navigation du routeur.
|
|
10
15
|
*/
|
|
11
|
-
export function UnsavedChangesGuard({ when, message
|
|
12
|
-
|
|
16
|
+
export function UnsavedChangesGuard({ when, message: messageProp }: UnsavedChangesGuardProps) {
|
|
17
|
+
const locale = useSiaLocale();
|
|
18
|
+
const message = messageProp ?? locale.unsavedChangesGuard.message;
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!when) return;
|
|
21
|
+
const handler = (event: BeforeUnloadEvent) => {
|
|
22
|
+
event.preventDefault();
|
|
23
|
+
event.returnValue = message;
|
|
24
|
+
return message;
|
|
25
|
+
};
|
|
26
|
+
window.addEventListener("beforeunload", handler);
|
|
27
|
+
return () => window.removeEventListener("beforeunload", handler);
|
|
28
|
+
}, [when, message]);
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* La confirmation avant une navigation du routeur.
|
|
33
|
+
*
|
|
34
|
+
* Hors d'un composant, la locale n'est pas lisible : passer
|
|
35
|
+
* `locale.unsavedChangesGuard.message` (lu avec `useSiaLocale()`) pour un
|
|
36
|
+
* message dans la langue de l'application. À défaut, le message français.
|
|
37
|
+
*/
|
|
38
|
+
export function confirmUnsavedNavigation(
|
|
39
|
+
when: boolean,
|
|
40
|
+
message: string = FRENCH.unsavedChangesGuard.message,
|
|
41
|
+
) {
|
|
42
|
+
return !when || window.confirm(message);
|
|
43
|
+
}
|