@sia-ui/cli 0.1.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 -0
- package/README.md +51 -0
- package/dist/diff.mjs +91 -0
- package/dist/index.js +325 -0
- package/dist/lifecycle.mjs +242 -0
- package/dist/lock.mjs +92 -0
- package/dist/registry/package/components/Accordion/index.tsx +172 -0
- package/dist/registry/package/components/Accordion/styles.css +97 -0
- package/dist/registry/package/components/Alert/index.tsx +31 -0
- package/dist/registry/package/components/Alert/styles.css +12 -0
- package/dist/registry/package/components/AmountDisplay/index.tsx +11 -0
- package/dist/registry/package/components/AmountDisplay/styles.css +9 -0
- package/dist/registry/package/components/AppShell/index.tsx +6 -0
- package/dist/registry/package/components/AppShell/styles.css +7 -0
- package/dist/registry/package/components/AspectRatio/index.tsx +29 -0
- package/dist/registry/package/components/AspectRatio/styles.css +15 -0
- package/dist/registry/package/components/AsyncState/index.tsx +22 -0
- package/dist/registry/package/components/AsyncState/styles.css +1 -0
- package/dist/registry/package/components/AuditMeta/index.tsx +44 -0
- package/dist/registry/package/components/AuditMeta/styles.css +12 -0
- package/dist/registry/package/components/AuthLayout/index.tsx +39 -0
- package/dist/registry/package/components/AuthLayout/styles.css +6 -0
- package/dist/registry/package/components/Autocomplete/index.tsx +8 -0
- package/dist/registry/package/components/Avatar/index.tsx +23 -0
- package/dist/registry/package/components/Avatar/styles.css +5 -0
- package/dist/registry/package/components/Badge/index.tsx +62 -0
- package/dist/registry/package/components/Badge/styles.css +26 -0
- package/dist/registry/package/components/Breadcrumb/index.tsx +131 -0
- package/dist/registry/package/components/Breadcrumb/styles.css +76 -0
- package/dist/registry/package/components/Button/index.tsx +91 -0
- package/dist/registry/package/components/Button/styles.css +111 -0
- package/dist/registry/package/components/Calendar/index.tsx +192 -0
- package/dist/registry/package/components/Card/index.tsx +8 -0
- package/dist/registry/package/components/Card/styles.css +4 -0
- package/dist/registry/package/components/Chart/index.tsx +437 -0
- package/dist/registry/package/components/Chart/styles.css +182 -0
- package/dist/registry/package/components/Checkbox/index.tsx +193 -0
- package/dist/registry/package/components/Checkbox/styles.css +39 -0
- package/dist/registry/package/components/ColorPicker/index.tsx +6 -0
- package/dist/registry/package/components/ColorPicker/styles.css +2 -0
- package/dist/registry/package/components/CommandPalette/index.tsx +7 -0
- package/dist/registry/package/components/CommandPalette/styles.css +8 -0
- package/dist/registry/package/components/ConfirmDialog/index.tsx +24 -0
- package/dist/registry/package/components/Container/index.tsx +11 -0
- package/dist/registry/package/components/Container/styles.css +1 -0
- package/dist/registry/package/components/Countdown/index.tsx +3 -0
- package/dist/registry/package/components/CrudPage/index.tsx +9 -0
- package/dist/registry/package/components/CrudPage/styles.css +1 -0
- package/dist/registry/package/components/CurrencyInput/index.tsx +15 -0
- package/dist/registry/package/components/CurrencyInput/styles.css +1 -0
- package/dist/registry/package/components/DataTable/index.tsx +240 -0
- package/dist/registry/package/components/DataTable/styles.css +98 -0
- package/dist/registry/package/components/DatePicker/index.tsx +102 -0
- package/dist/registry/package/components/DateRangeFilter/index.tsx +8 -0
- package/dist/registry/package/components/DateRangeFilter/styles.css +4 -0
- package/dist/registry/package/components/DateRangePicker/index.tsx +36 -0
- package/dist/registry/package/components/DateTimePicker/index.tsx +32 -0
- package/dist/registry/package/components/Descriptions/index.tsx +7 -0
- package/dist/registry/package/components/Descriptions/styles.css +8 -0
- package/dist/registry/package/components/Divider/index.tsx +12 -0
- package/dist/registry/package/components/Divider/styles.css +3 -0
- package/dist/registry/package/components/Drawer/context.ts +14 -0
- package/dist/registry/package/components/Drawer/index.tsx +209 -0
- package/dist/registry/package/components/Drawer/styles.css +151 -0
- package/dist/registry/package/components/DropdownMenu/index.tsx +350 -0
- package/dist/registry/package/components/DropdownMenu/styles.css +100 -0
- package/dist/registry/package/components/DurationDisplay/index.tsx +3 -0
- package/dist/registry/package/components/EmptyState/index.tsx +23 -0
- package/dist/registry/package/components/EmptyState/styles.css +6 -0
- package/dist/registry/package/components/EntityMeta/index.tsx +65 -0
- package/dist/registry/package/components/EntityMeta/styles.css +9 -0
- package/dist/registry/package/components/ErrorState/index.tsx +30 -0
- package/dist/registry/package/components/ErrorState/styles.css +6 -0
- package/dist/registry/package/components/EventCalendar/index.tsx +8 -0
- package/dist/registry/package/components/EventCalendar/styles.css +8 -0
- package/dist/registry/package/components/Field/index.tsx +664 -0
- package/dist/registry/package/components/Field/styles.css +14 -0
- package/dist/registry/package/components/FileUpload/index.tsx +68 -0
- package/dist/registry/package/components/FileUpload/styles.css +4 -0
- package/dist/registry/package/components/FiltersBar/index.tsx +20 -0
- package/dist/registry/package/components/FiltersBar/styles.css +6 -0
- package/dist/registry/package/components/Form/index.tsx +211 -0
- package/dist/registry/package/components/Form/styles.css +80 -0
- package/dist/registry/package/components/FormField/index.tsx +2 -0
- package/dist/registry/package/components/HoverCard/index.tsx +121 -0
- package/dist/registry/package/components/HoverCard/styles.css +26 -0
- package/dist/registry/package/components/IconButton/index.tsx +27 -0
- package/dist/registry/package/components/IconButton/styles.css +7 -0
- package/dist/registry/package/components/Icons/index.tsx +185 -0
- package/dist/registry/package/components/ImageUpload/index.tsx +6 -0
- package/dist/registry/package/components/ImageUpload/styles.css +4 -0
- package/dist/registry/package/components/Input/index.tsx +103 -0
- package/dist/registry/package/components/Input/styles.css +8 -0
- package/dist/registry/package/components/InputNumber/index.tsx +196 -0
- package/dist/registry/package/components/InputNumber/styles.css +54 -0
- package/dist/registry/package/components/JsonEditor/index.tsx +6 -0
- package/dist/registry/package/components/JsonEditor/styles.css +2 -0
- package/dist/registry/package/components/KpiGrid/index.tsx +11 -0
- package/dist/registry/package/components/KpiGrid/styles.css +5 -0
- package/dist/registry/package/components/Label/index.tsx +43 -0
- package/dist/registry/package/components/Label/styles.css +25 -0
- package/dist/registry/package/components/MarkdownEditor/index.tsx +5 -0
- package/dist/registry/package/components/MiniCalendar/index.tsx +3 -0
- package/dist/registry/package/components/Modal/index.tsx +104 -0
- package/dist/registry/package/components/Modal/styles.css +20 -0
- package/dist/registry/package/components/MonthPicker/index.tsx +5 -0
- package/dist/registry/package/components/MultiSelect/index.tsx +133 -0
- package/dist/registry/package/components/MultiSelect/styles.css +3 -0
- package/dist/registry/package/components/OtpInput/index.tsx +5 -0
- package/dist/registry/package/components/OtpInput/styles.css +2 -0
- package/dist/registry/package/components/Overlay/index.tsx +140 -0
- package/dist/registry/package/components/Overlay/styles.css +1 -0
- package/dist/registry/package/components/PageHeader/index.tsx +24 -0
- package/dist/registry/package/components/PageHeader/styles.css +5 -0
- package/dist/registry/package/components/Pagination/index.tsx +155 -0
- package/dist/registry/package/components/Pagination/styles.css +103 -0
- package/dist/registry/package/components/PasswordInput/index.tsx +125 -0
- package/dist/registry/package/components/PasswordInput/styles.css +58 -0
- package/dist/registry/package/components/PermissionGate/index.tsx +3 -0
- package/dist/registry/package/components/PhoneInput/index.tsx +189 -0
- package/dist/registry/package/components/PhoneInput/styles.css +35 -0
- package/dist/registry/package/components/Popover/index.tsx +12 -0
- package/dist/registry/package/components/Popover/styles.css +12 -0
- package/dist/registry/package/components/Progress/index.tsx +149 -0
- package/dist/registry/package/components/Progress/styles.css +101 -0
- package/dist/registry/package/components/QrCode/index.tsx +6 -0
- package/dist/registry/package/components/QrCode/styles.css +5 -0
- package/dist/registry/package/components/RadioGroup/index.tsx +29 -0
- package/dist/registry/package/components/RadioGroup/styles.css +20 -0
- package/dist/registry/package/components/Rating/index.tsx +82 -0
- package/dist/registry/package/components/Rating/styles.css +41 -0
- package/dist/registry/package/components/ReferenceSelect/index.tsx +7 -0
- package/dist/registry/package/components/ReferenceSelect/styles.css +2 -0
- package/dist/registry/package/components/RelativeTime/index.tsx +22 -0
- package/dist/registry/package/components/RelativeTime/styles.css +1 -0
- package/dist/registry/package/components/RequireSession/index.tsx +127 -0
- package/dist/registry/package/components/RequireSession/styles.css +13 -0
- package/dist/registry/package/components/ResizablePanel/index.tsx +6 -0
- package/dist/registry/package/components/ResizablePanel/styles.css +5 -0
- package/dist/registry/package/components/RichTextEditor/index.tsx +5 -0
- package/dist/registry/package/components/RichTextEditor/styles.css +2 -0
- package/dist/registry/package/components/ScrollArea/index.tsx +114 -0
- package/dist/registry/package/components/ScrollArea/styles.css +74 -0
- package/dist/registry/package/components/SearchInput/index.tsx +121 -0
- package/dist/registry/package/components/SearchInput/styles.css +6 -0
- package/dist/registry/package/components/Select/index.tsx +324 -0
- package/dist/registry/package/components/Select/styles.css +58 -0
- package/dist/registry/package/components/Sidebar/index.tsx +232 -0
- package/dist/registry/package/components/Sidebar/styles.css +140 -0
- package/dist/registry/package/components/Skeleton/index.tsx +21 -0
- package/dist/registry/package/components/Skeleton/styles.css +2 -0
- package/dist/registry/package/components/Slider/index.tsx +193 -0
- package/dist/registry/package/components/Slider/styles.css +53 -0
- package/dist/registry/package/components/Spinner/index.tsx +28 -0
- package/dist/registry/package/components/Spinner/styles.css +8 -0
- package/dist/registry/package/components/StatCard/index.tsx +15 -0
- package/dist/registry/package/components/StatCard/styles.css +3 -0
- package/dist/registry/package/components/Statistic/index.tsx +23 -0
- package/dist/registry/package/components/Statistic/styles.css +6 -0
- package/dist/registry/package/components/Steps/index.tsx +108 -0
- package/dist/registry/package/components/Steps/styles.css +138 -0
- package/dist/registry/package/components/Switch/index.tsx +46 -0
- package/dist/registry/package/components/Switch/styles.css +26 -0
- package/dist/registry/package/components/Tabs/index.tsx +44 -0
- package/dist/registry/package/components/Tabs/styles.css +41 -0
- package/dist/registry/package/components/TagsInput/index.tsx +6 -0
- package/dist/registry/package/components/TagsInput/styles.css +2 -0
- package/dist/registry/package/components/Textarea/index.tsx +21 -0
- package/dist/registry/package/components/Textarea/styles.css +7 -0
- package/dist/registry/package/components/TimePicker/index.tsx +76 -0
- package/dist/registry/package/components/TimePicker/styles.css +13 -0
- package/dist/registry/package/components/TimeRangePicker/index.tsx +32 -0
- package/dist/registry/package/components/TimeRangePicker/styles.css +1 -0
- package/dist/registry/package/components/Timeline/index.tsx +8 -0
- package/dist/registry/package/components/Timeline/styles.css +12 -0
- package/dist/registry/package/components/Timer/index.tsx +9 -0
- package/dist/registry/package/components/Toast/index.tsx +426 -0
- package/dist/registry/package/components/Toast/styles.css +175 -0
- package/dist/registry/package/components/Toggle/index.tsx +181 -0
- package/dist/registry/package/components/Toggle/styles.css +124 -0
- package/dist/registry/package/components/Tooltip/index.tsx +163 -0
- package/dist/registry/package/components/Tooltip/styles.css +57 -0
- package/dist/registry/package/components/Tour/index.tsx +8 -0
- package/dist/registry/package/components/Tour/styles.css +6 -0
- package/dist/registry/package/components/Transfer/index.tsx +7 -0
- package/dist/registry/package/components/Transfer/styles.css +6 -0
- package/dist/registry/package/components/Tree/index.tsx +8 -0
- package/dist/registry/package/components/Tree/styles.css +6 -0
- package/dist/registry/package/components/TreeSelect/index.tsx +10 -0
- package/dist/registry/package/components/TreeSelect/styles.css +3 -0
- package/dist/registry/package/components/Typography/index.tsx +27 -0
- package/dist/registry/package/components/Typography/styles.css +16 -0
- package/dist/registry/package/components/UnsavedChangesGuard/index.tsx +4 -0
- package/dist/registry/package/components/WeekPicker/index.tsx +5 -0
- package/dist/registry/package/components/YearPicker/index.tsx +3 -0
- package/dist/registry/package/lib/form-adapter-rhf.tsx +141 -0
- package/dist/registry/registry.json +1780 -0
- package/dist/registry/styles/sia-ui.css +361 -0
- package/dist/source.mjs +93 -0
- package/package.json +55 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { TimePicker, type TimePickerProps } from "../TimePicker";
|
|
3
|
+
import { cn, toneClass } from "@sia-ui/utils";
|
|
4
|
+
import type { ComponentTone } from "@sia-ui/tokens";
|
|
5
|
+
import { ArrowRightIcon } from "../Icons";
|
|
6
|
+
import { useSiaLocale } from "@sia-ui/headless";
|
|
7
|
+
import "./styles.css";
|
|
8
|
+
|
|
9
|
+
export interface TimeRangeValue { start: string; end: string; }
|
|
10
|
+
export interface TimeRangePickerProps extends Pick<TimePickerProps, "minuteStep" | "secondStep" | "showSeconds" | "hourFormat" | "needConfirm" | "allowClear"> {
|
|
11
|
+
value?: TimeRangeValue;
|
|
12
|
+
defaultValue?: TimeRangeValue;
|
|
13
|
+
onValueChange?: (value: TimeRangeValue) => void;
|
|
14
|
+
order?: boolean;
|
|
15
|
+
startLabel?: string;
|
|
16
|
+
endLabel?: string;
|
|
17
|
+
tone?: ComponentTone;
|
|
18
|
+
invalid?: boolean;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
export function TimeRangePicker({ value, defaultValue = { start: "", end: "" }, onValueChange, order = true, startLabel: startLabelProp, endLabel: endLabelProp, minuteStep = 1, secondStep = 1, showSeconds = false, hourFormat = 24, needConfirm = false, allowClear = true, tone = "primary", invalid, disabled, className }: TimeRangePickerProps) {
|
|
23
|
+
const locale = useSiaLocale();
|
|
24
|
+
const endLabel = endLabelProp ?? locale.endTime;
|
|
25
|
+
const startLabel = startLabelProp ?? locale.startTime;
|
|
26
|
+
const [internal, setInternal] = useState(defaultValue);
|
|
27
|
+
const current = value ?? internal;
|
|
28
|
+
const update = (next: TimeRangeValue) => { const normalized = order && next.start && next.end && next.start > next.end ? { start: next.end, end: next.start } : next; if (value === undefined) setInternal(normalized); onValueChange?.(normalized); };
|
|
29
|
+
const rangeInvalid = Boolean(invalid || (!order && current.start && current.end && current.start > current.end));
|
|
30
|
+
const shared = { minuteStep, secondStep, showSeconds, hourFormat, needConfirm, allowClear, tone, ...(disabled !== undefined ? { disabled } : {}) };
|
|
31
|
+
return <div className={cn("sia-time-range-picker", toneClass("sia-tone", tone), rangeInvalid && "sia-time-range-picker--invalid", className)}><TimePicker {...shared} aria-label={startLabel} value={current.start} {...(!order && current.end ? { max: current.end } : {})} invalid={rangeInvalid} onValueChange={(start) => update({ ...current, start })} /><ArrowRightIcon className="sia-range-picker__separator" /><TimePicker {...shared} aria-label={endLabel} value={current.end} {...(!order && current.start ? { min: current.start } : {})} invalid={rangeInvalid} onValueChange={(end) => update({ ...current, end })} /></div>;
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.sia-time-range-picker--invalid { color:var(--sia-danger); }
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { cn } from "@sia-ui/utils";
|
|
3
|
+
import type { ComponentTone } from "@sia-ui/tokens";
|
|
4
|
+
import "./styles.css";
|
|
5
|
+
|
|
6
|
+
export interface TimelineItem { key: string; title: ReactNode; description?: ReactNode; time?: ReactNode; tone?: ComponentTone; }
|
|
7
|
+
export interface TimelineProps { items: TimelineItem[]; pending?: ReactNode; className?: string; }
|
|
8
|
+
export function Timeline({ items, pending, className }: TimelineProps) { return <ol className={cn("sia-timeline", className)}>{items.map((item) => <li key={item.key} className={`sia-timeline__item sia-timeline__item--${item.tone ?? "primary"}`}><span className="sia-timeline__dot" /><div><strong>{item.title}</strong>{item.description && <p>{item.description}</p>}{item.time && <small>{item.time}</small>}</div></li>)}{pending && <li className="sia-timeline__item sia-timeline__item--pending"><span className="sia-timeline__dot" /><div>{pending}</div></li>}</ol>; }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
.sia-timeline { display:grid; gap:0; padding:0; margin:0; list-style:none; }
|
|
2
|
+
.sia-timeline__item { position:relative; display:grid; grid-template-columns:1rem 1fr; gap:.75rem; padding-bottom:1.25rem; }
|
|
3
|
+
.sia-timeline__item:not(:last-child)::before { content:""; position:absolute; top:.8rem; bottom:0; left:.32rem; width:1px; background:var(--sia-border); }
|
|
4
|
+
.sia-timeline__dot { position:relative; z-index:1; width:.7rem; height:.7rem; margin-top:.3rem; border-radius:50%; background:var(--sia-primary); }
|
|
5
|
+
.sia-timeline__item--neutral .sia-timeline__dot { background:var(--sia-muted); }
|
|
6
|
+
.sia-timeline__item--info .sia-timeline__dot { background:var(--sia-info); }
|
|
7
|
+
.sia-timeline__item--success .sia-timeline__dot { background:var(--sia-success); }
|
|
8
|
+
.sia-timeline__item--warning .sia-timeline__dot { background:var(--sia-warning); }
|
|
9
|
+
.sia-timeline__item--danger .sia-timeline__dot { background:var(--sia-danger); }
|
|
10
|
+
.sia-timeline p { margin:.25rem 0; color:var(--sia-muted); }
|
|
11
|
+
.sia-timeline small { color:var(--sia-muted); }
|
|
12
|
+
.sia-timeline__item--primary .sia-timeline__dot { background:var(--sia-primary); }
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { cn } from "@sia-ui/utils";
|
|
3
|
+
|
|
4
|
+
export interface TimerProps { duration?: number; initialValue?: number; direction?: "down" | "up"; running?: boolean; interval?: number; onChange?: (milliseconds: number) => void; onFinish?: () => void; format?: (milliseconds: number) => string; className?: string; }
|
|
5
|
+
export interface TimeParts { hours: number; minutes: number; seconds: number; }
|
|
6
|
+
export function parseTimeValue(value = ""): TimeParts { const [hours = 0, minutes = 0, seconds = 0] = value.split(":").map(Number); return { hours: Math.min(Math.max(hours || 0, 0), 23), minutes: Math.min(Math.max(minutes || 0, 0), 59), seconds: Math.min(Math.max(seconds || 0, 0), 59) }; }
|
|
7
|
+
export function formatTimeValue(parts: TimeParts, showSeconds = false) { const base = `${String(parts.hours).padStart(2, "0")}:${String(parts.minutes).padStart(2, "0")}`; return showSeconds ? `${base}:${String(parts.seconds).padStart(2, "0")}` : base; }
|
|
8
|
+
export function formatDuration(milliseconds: number, showMilliseconds = false) { const total = Math.max(0, milliseconds); const hours = Math.floor(total / 3600000); const minutes = Math.floor((total % 3600000) / 60000); const seconds = Math.floor((total % 60000) / 1000); const ms = Math.floor((total % 1000) / 10); return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}${showMilliseconds ? `.${String(ms).padStart(2, "0")}` : ""}`; }
|
|
9
|
+
export function Timer({ duration = 60000, initialValue = 0, direction = "down", running = true, interval = 250, onChange, onFinish, format = formatDuration, className }: TimerProps) { const initial = direction === "down" ? duration : initialValue; const [value, setValue] = useState(initial); const finishCalled = useRef(false); useEffect(() => { setValue(direction === "down" ? duration : initialValue); finishCalled.current = false; }, [direction, duration, initialValue]); useEffect(() => { if (!running) return; const startedAt = Date.now(); const base = value; const update = () => { const elapsed = Date.now() - startedAt; const next = direction === "down" ? Math.max(0, base - elapsed) : base + elapsed; setValue(next); onChange?.(next); if (direction === "down" && next === 0 && !finishCalled.current) { finishCalled.current = true; onFinish?.(); } }; update(); const id = window.setInterval(update, interval); return () => window.clearInterval(id); }, [running, direction, interval]); return <time className={cn("sia-timer", className)} aria-live="polite">{format(value)}</time>; }
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useMemo,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
type CSSProperties,
|
|
8
|
+
type PointerEvent as ReactPointerEvent,
|
|
9
|
+
type PropsWithChildren,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
} from "react";
|
|
12
|
+
import { createPortal } from "react-dom";
|
|
13
|
+
import { cn } from "@sia-ui/utils";
|
|
14
|
+
import {
|
|
15
|
+
toast,
|
|
16
|
+
toastStore,
|
|
17
|
+
useSiaLocale,
|
|
18
|
+
useToasts,
|
|
19
|
+
type ToastId,
|
|
20
|
+
type ToastOptions,
|
|
21
|
+
type ToastPlacement,
|
|
22
|
+
type ToastRecord,
|
|
23
|
+
type ToastTone,
|
|
24
|
+
} from "@sia-ui/headless";
|
|
25
|
+
import { usePortal } from "@sia-ui/react";
|
|
26
|
+
import { AlertCircleIcon, CheckIcon, InfoIcon, XIcon } from "../Icons";
|
|
27
|
+
import { Spinner } from "../Spinner";
|
|
28
|
+
import "./styles.css";
|
|
29
|
+
|
|
30
|
+
export type { ToastId, ToastOptions, ToastPlacement, ToastTone };
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* L'API des annonces, appelable de n'importe où.
|
|
34
|
+
*
|
|
35
|
+
* `toast("Enregistré")`, `toast.success(…)`, `toast.promise(…)`. Ce n'est pas
|
|
36
|
+
* un hook : le magasin vit hors de React, donc un intercepteur HTTP ou une
|
|
37
|
+
* fonction utilitaire peut poser une annonce sans être un composant.
|
|
38
|
+
*/
|
|
39
|
+
export { toast, toastStore };
|
|
40
|
+
|
|
41
|
+
const TONE_ICON: Record<ToastTone, ReactNode> = {
|
|
42
|
+
neutral: null,
|
|
43
|
+
primary: <InfoIcon />,
|
|
44
|
+
info: <InfoIcon />,
|
|
45
|
+
success: <CheckIcon />,
|
|
46
|
+
warning: <AlertCircleIcon />,
|
|
47
|
+
danger: <AlertCircleIcon />,
|
|
48
|
+
loading: <Spinner size="sm" tone="current" />,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** Le temps laissé à l'animation de sortie avant le retrait définitif. */
|
|
52
|
+
const EXIT_DELAY = 260;
|
|
53
|
+
|
|
54
|
+
const PLACEMENTS: ToastPlacement[] = [
|
|
55
|
+
"top-left",
|
|
56
|
+
"top-center",
|
|
57
|
+
"top-right",
|
|
58
|
+
"bottom-left",
|
|
59
|
+
"bottom-center",
|
|
60
|
+
"bottom-right",
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
export interface ToasterProps {
|
|
64
|
+
/** Coin par défaut des annonces qui n'en précisent pas. */
|
|
65
|
+
placement?: ToastPlacement;
|
|
66
|
+
/** Nombre visible simultanément. Au-delà, la plus ancienne s'efface. */
|
|
67
|
+
limit?: number;
|
|
68
|
+
/** Durée par défaut, pour les tons qui n'en imposent pas une. */
|
|
69
|
+
duration?: number;
|
|
70
|
+
/** Largeur d'une annonce. */
|
|
71
|
+
width?: number | string;
|
|
72
|
+
/** Décalage depuis le bord de l'écran. */
|
|
73
|
+
offset?: number | string;
|
|
74
|
+
/**
|
|
75
|
+
* Où rendre la pile. Par défaut, un nœud créé sous `document.body`.
|
|
76
|
+
*
|
|
77
|
+
* À fournir quand le thème est porté par un conteneur plutôt que par la
|
|
78
|
+
* racine du document : sorties de ce conteneur, les annonces perdraient
|
|
79
|
+
* ses variables et s'afficheraient dans le thème clair.
|
|
80
|
+
*/
|
|
81
|
+
container?: HTMLElement | null | undefined;
|
|
82
|
+
className?: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* La pile visible.
|
|
87
|
+
*
|
|
88
|
+
* Un seul `<Toaster />` à la racine de l'application. Il rend les six coins :
|
|
89
|
+
* une annonce peut choisir le sien, ce qui permet à une confirmation
|
|
90
|
+
* d'apparaître près du geste qui l'a déclenchée et à une panne générale de
|
|
91
|
+
* s'imposer en haut de l'écran.
|
|
92
|
+
*/
|
|
93
|
+
export function Toaster({
|
|
94
|
+
placement = "bottom-right",
|
|
95
|
+
limit,
|
|
96
|
+
duration,
|
|
97
|
+
width = "24rem",
|
|
98
|
+
offset = "1rem",
|
|
99
|
+
container,
|
|
100
|
+
className,
|
|
101
|
+
}: ToasterProps) {
|
|
102
|
+
const records = useToasts<ReactNode>();
|
|
103
|
+
const node = usePortal(container ? { container } : { id: "sia-toaster" });
|
|
104
|
+
const locale = useSiaLocale();
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
toastStore.configure({
|
|
108
|
+
...(limit !== undefined ? { limit } : {}),
|
|
109
|
+
...(duration !== undefined ? { defaultDuration: duration } : {}),
|
|
110
|
+
});
|
|
111
|
+
}, [duration, limit]);
|
|
112
|
+
|
|
113
|
+
const byPlacement = useMemo(() => {
|
|
114
|
+
const groups = new Map<ToastPlacement, Array<ToastRecord<ReactNode>>>();
|
|
115
|
+
for (const record of records) {
|
|
116
|
+
const corner = record.placement ?? placement;
|
|
117
|
+
const list = groups.get(corner);
|
|
118
|
+
if (list) list.push(record);
|
|
119
|
+
else groups.set(corner, [record]);
|
|
120
|
+
}
|
|
121
|
+
return groups;
|
|
122
|
+
}, [placement, records]);
|
|
123
|
+
|
|
124
|
+
if (!node) return null;
|
|
125
|
+
|
|
126
|
+
return createPortal(
|
|
127
|
+
<>
|
|
128
|
+
{PLACEMENTS.filter((corner) => byPlacement.has(corner)).map((corner) => {
|
|
129
|
+
const list = byPlacement.get(corner) ?? [];
|
|
130
|
+
// En haut, la plus récente arrive au-dessus de la pile; en bas, elle
|
|
131
|
+
// arrive au-dessous. Dans les deux cas elle est la plus proche du
|
|
132
|
+
// bord, donc la première lue.
|
|
133
|
+
const ordered = corner.startsWith("top") ? [...list].reverse() : list;
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div
|
|
137
|
+
key={corner}
|
|
138
|
+
className={cn("sia-toaster", `sia-toaster--${corner}`, className)}
|
|
139
|
+
style={
|
|
140
|
+
{
|
|
141
|
+
"--sia-toaster-width": typeof width === "number" ? `${width}px` : width,
|
|
142
|
+
"--sia-toaster-offset":
|
|
143
|
+
typeof offset === "number" ? `${offset}px` : offset,
|
|
144
|
+
} as CSSProperties
|
|
145
|
+
}
|
|
146
|
+
role="region"
|
|
147
|
+
aria-label={locale.notifications}
|
|
148
|
+
>
|
|
149
|
+
{ordered.map((record) => (
|
|
150
|
+
<ToastItem
|
|
151
|
+
key={record.id}
|
|
152
|
+
record={record}
|
|
153
|
+
placement={corner}
|
|
154
|
+
closeLabel={locale.close}
|
|
155
|
+
/>
|
|
156
|
+
))}
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
})}
|
|
160
|
+
</>,
|
|
161
|
+
node,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
interface ToastItemProps {
|
|
166
|
+
record: ToastRecord<ReactNode>;
|
|
167
|
+
placement: ToastPlacement;
|
|
168
|
+
closeLabel: string;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function ToastItem({ record, placement, closeLabel }: ToastItemProps) {
|
|
172
|
+
const tone = record.tone ?? "neutral";
|
|
173
|
+
const icon = record.icon === undefined ? TONE_ICON[tone] : record.icon;
|
|
174
|
+
const closable = record.closable ?? tone !== "loading";
|
|
175
|
+
const duration = toastStore.durationOf(record);
|
|
176
|
+
const timed = Number.isFinite(duration) && duration > 0;
|
|
177
|
+
|
|
178
|
+
const dismiss = useCallback(() => toastStore.dismiss(record.id), [record.id]);
|
|
179
|
+
|
|
180
|
+
// Le retrait définitif est déclenché par une minuterie, pas par la fin de
|
|
181
|
+
// l'animation : `animationend` ne se produit pas si l'animation n'a jamais
|
|
182
|
+
// tourné — préférence de mouvement réduit, onglet en arrière-plan — et
|
|
183
|
+
// l'annonce resterait alors dans le magasin pour toujours.
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
if (record.open) return undefined;
|
|
186
|
+
const timer = setTimeout(() => toastStore.remove(record.id), EXIT_DELAY);
|
|
187
|
+
return () => clearTimeout(timer);
|
|
188
|
+
}, [record.id, record.open]);
|
|
189
|
+
|
|
190
|
+
// La fermeture automatique, suspendue au survol et au focus clavier.
|
|
191
|
+
const [paused, setPaused] = useState(false);
|
|
192
|
+
const remaining = useRef(duration);
|
|
193
|
+
const startedAt = useRef(Date.now());
|
|
194
|
+
|
|
195
|
+
// Une mise à jour — ce que fait `toast.promise` — rend son temps entier à
|
|
196
|
+
// l'annonce : la nouvelle phrase doit pouvoir être lue.
|
|
197
|
+
//
|
|
198
|
+
// La remise à zéro se fait pendant le rendu, pas dans un effet. Un effet
|
|
199
|
+
// s'exécuterait *après* celui qui arme la minuterie, qui lirait alors la
|
|
200
|
+
// durée précédente. Quand elle valait `Infinity` — le cas d'une annonce
|
|
201
|
+
// en chargement qui devient un succès — `setTimeout` reçoit une valeur
|
|
202
|
+
// qui déborde son entier 32 bits et se déclenche immédiatement : la carte
|
|
203
|
+
// de succès disparaissait avant d'avoir été vue.
|
|
204
|
+
const signature = `${duration}|${record.title}|${record.description}|${record.tone}`;
|
|
205
|
+
const lastSignature = useRef(signature);
|
|
206
|
+
if (lastSignature.current !== signature) {
|
|
207
|
+
lastSignature.current = signature;
|
|
208
|
+
remaining.current = duration;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
if (!timed || !record.open || paused) return undefined;
|
|
213
|
+
if (!Number.isFinite(remaining.current)) return undefined;
|
|
214
|
+
|
|
215
|
+
startedAt.current = Date.now();
|
|
216
|
+
const timer = setTimeout(dismiss, remaining.current);
|
|
217
|
+
return () => {
|
|
218
|
+
clearTimeout(timer);
|
|
219
|
+
remaining.current = Math.max(
|
|
220
|
+
0,
|
|
221
|
+
remaining.current - (Date.now() - startedAt.current),
|
|
222
|
+
);
|
|
223
|
+
};
|
|
224
|
+
}, [dismiss, paused, record.open, signature, timed]);
|
|
225
|
+
|
|
226
|
+
const swipe = useSwipeToDismiss(placement, dismiss);
|
|
227
|
+
|
|
228
|
+
const run = (action: NonNullable<ToastRecord["action"]>) => {
|
|
229
|
+
action.onSelect();
|
|
230
|
+
if (action.dismiss ?? true) dismiss();
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
return (
|
|
234
|
+
<div
|
|
235
|
+
className={cn(
|
|
236
|
+
"sia-toast",
|
|
237
|
+
`sia-toast--${tone}`,
|
|
238
|
+
`sia-toast--from-${edgeOf(placement)}`,
|
|
239
|
+
record.className,
|
|
240
|
+
)}
|
|
241
|
+
data-state={record.open ? "open" : "closed"}
|
|
242
|
+
data-swiping={swipe.swiping || undefined}
|
|
243
|
+
style={swipe.style}
|
|
244
|
+
// `alert` interrompt la lecture d'écran, `status` attend une pause. Une
|
|
245
|
+
// erreur mérite l'interruption; une confirmation ne la mérite pas.
|
|
246
|
+
role={tone === "danger" ? "alert" : "status"}
|
|
247
|
+
aria-live={tone === "danger" ? "assertive" : "polite"}
|
|
248
|
+
onMouseEnter={() => setPaused(true)}
|
|
249
|
+
onMouseLeave={() => setPaused(false)}
|
|
250
|
+
onFocusCapture={() => setPaused(true)}
|
|
251
|
+
onBlurCapture={() => setPaused(false)}
|
|
252
|
+
onPointerDown={swipe.onPointerDown}
|
|
253
|
+
onPointerMove={swipe.onPointerMove}
|
|
254
|
+
onPointerUp={swipe.onPointerUp}
|
|
255
|
+
onPointerCancel={swipe.onPointerUp}
|
|
256
|
+
>
|
|
257
|
+
{record.render ? (
|
|
258
|
+
record.render({ id: record.id, dismiss })
|
|
259
|
+
) : (
|
|
260
|
+
<>
|
|
261
|
+
{icon && (
|
|
262
|
+
<span className="sia-toast__icon" aria-hidden="true">
|
|
263
|
+
{icon}
|
|
264
|
+
</span>
|
|
265
|
+
)}
|
|
266
|
+
|
|
267
|
+
<div className="sia-toast__body">
|
|
268
|
+
{record.title && (
|
|
269
|
+
<strong className="sia-toast__title">{record.title}</strong>
|
|
270
|
+
)}
|
|
271
|
+
{record.description && (
|
|
272
|
+
<p className="sia-toast__description">{record.description}</p>
|
|
273
|
+
)}
|
|
274
|
+
|
|
275
|
+
{(record.action || record.cancel) && (
|
|
276
|
+
<div className="sia-toast__actions">
|
|
277
|
+
{record.action && (
|
|
278
|
+
<button
|
|
279
|
+
type="button"
|
|
280
|
+
className="sia-toast__action"
|
|
281
|
+
onClick={() => run(record.action!)}
|
|
282
|
+
>
|
|
283
|
+
{record.action.label}
|
|
284
|
+
</button>
|
|
285
|
+
)}
|
|
286
|
+
{record.cancel && (
|
|
287
|
+
<button
|
|
288
|
+
type="button"
|
|
289
|
+
className="sia-toast__cancel"
|
|
290
|
+
onClick={() => run(record.cancel!)}
|
|
291
|
+
>
|
|
292
|
+
{record.cancel.label}
|
|
293
|
+
</button>
|
|
294
|
+
)}
|
|
295
|
+
</div>
|
|
296
|
+
)}
|
|
297
|
+
</div>
|
|
298
|
+
|
|
299
|
+
{closable && (
|
|
300
|
+
<button
|
|
301
|
+
type="button"
|
|
302
|
+
className="sia-toast__close"
|
|
303
|
+
aria-label={closeLabel}
|
|
304
|
+
onClick={dismiss}
|
|
305
|
+
>
|
|
306
|
+
<XIcon />
|
|
307
|
+
</button>
|
|
308
|
+
)}
|
|
309
|
+
|
|
310
|
+
{timed && record.open && (
|
|
311
|
+
<span
|
|
312
|
+
className="sia-toast__timer"
|
|
313
|
+
style={{ animationDuration: `${duration}ms` }}
|
|
314
|
+
data-paused={paused || undefined}
|
|
315
|
+
aria-hidden="true"
|
|
316
|
+
/>
|
|
317
|
+
)}
|
|
318
|
+
</>
|
|
319
|
+
)}
|
|
320
|
+
</div>
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function edgeOf(placement: ToastPlacement): "left" | "right" | "center" {
|
|
325
|
+
if (placement.endsWith("left")) return "left";
|
|
326
|
+
if (placement.endsWith("center")) return "center";
|
|
327
|
+
return "right";
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Seuil, en pixels, au-delà duquel le geste ferme l'annonce. */
|
|
331
|
+
const SWIPE_THRESHOLD = 60;
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Balayer pour écarter.
|
|
335
|
+
*
|
|
336
|
+
* Le sens suit le bord le plus proche : une annonce à droite se chasse vers
|
|
337
|
+
* la droite. Une pile au centre se chasse verticalement, vers le bord dont
|
|
338
|
+
* elle vient.
|
|
339
|
+
*/
|
|
340
|
+
function useSwipeToDismiss(placement: ToastPlacement, dismiss: () => void) {
|
|
341
|
+
const axis = placement.endsWith("center") ? "y" : "x";
|
|
342
|
+
const sign = useMemo(() => {
|
|
343
|
+
if (axis === "y") return placement.startsWith("top") ? -1 : 1;
|
|
344
|
+
return placement.endsWith("left") ? -1 : 1;
|
|
345
|
+
}, [axis, placement]);
|
|
346
|
+
|
|
347
|
+
const origin = useRef<{ x: number; y: number } | null>(null);
|
|
348
|
+
const [offset, setOffset] = useState(0);
|
|
349
|
+
const [swiping, setSwiping] = useState(false);
|
|
350
|
+
|
|
351
|
+
const onPointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {
|
|
352
|
+
// Un clic sur un bouton n'est pas un balayage : capter le pointeur ici
|
|
353
|
+
// avalerait l'action.
|
|
354
|
+
if ((event.target as HTMLElement).closest("button")) return;
|
|
355
|
+
if (event.pointerType === "mouse" && event.button !== 0) return;
|
|
356
|
+
|
|
357
|
+
origin.current = { x: event.clientX, y: event.clientY };
|
|
358
|
+
setSwiping(true);
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const onPointerMove = (event: ReactPointerEvent<HTMLDivElement>) => {
|
|
362
|
+
if (!origin.current) return;
|
|
363
|
+
|
|
364
|
+
const delta =
|
|
365
|
+
axis === "x"
|
|
366
|
+
? event.clientX - origin.current.x
|
|
367
|
+
: event.clientY - origin.current.y;
|
|
368
|
+
|
|
369
|
+
// Seul le sens de la sortie répond; tirer dans l'autre sens ne fait rien,
|
|
370
|
+
// ce qui évite de décoller la carte de son bord.
|
|
371
|
+
setOffset(delta * sign > 0 ? delta : 0);
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const onPointerUp = () => {
|
|
375
|
+
if (!origin.current) return;
|
|
376
|
+
origin.current = null;
|
|
377
|
+
setSwiping(false);
|
|
378
|
+
|
|
379
|
+
if (Math.abs(offset) >= SWIPE_THRESHOLD) dismiss();
|
|
380
|
+
else setOffset(0);
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
swiping,
|
|
385
|
+
onPointerDown,
|
|
386
|
+
onPointerMove,
|
|
387
|
+
onPointerUp,
|
|
388
|
+
style:
|
|
389
|
+
offset === 0
|
|
390
|
+
? undefined
|
|
391
|
+
: ({
|
|
392
|
+
transform:
|
|
393
|
+
axis === "x"
|
|
394
|
+
? `translate3d(${offset}px, 0, 0)`
|
|
395
|
+
: `translate3d(0, ${offset}px, 0)`,
|
|
396
|
+
opacity: Math.max(0.3, 1 - Math.abs(offset) / 160),
|
|
397
|
+
} as CSSProperties),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export interface ToastProviderProps extends PropsWithChildren, ToasterProps {}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Enveloppe les enfants et monte le `Toaster`.
|
|
405
|
+
*
|
|
406
|
+
* Aucun contexte : le magasin vit hors de React. Ce composant n'existe que
|
|
407
|
+
* pour monter la pile au bon endroit sans y penser.
|
|
408
|
+
*/
|
|
409
|
+
export function ToastProvider({ children, ...props }: ToastProviderProps) {
|
|
410
|
+
return (
|
|
411
|
+
<>
|
|
412
|
+
{children}
|
|
413
|
+
<Toaster {...props} />
|
|
414
|
+
</>
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* L'API des annonces, pour ceux qui préfèrent un hook.
|
|
420
|
+
*
|
|
421
|
+
* Elle rend exactement l'objet `toast`, dont l'identité ne change jamais :
|
|
422
|
+
* l'appeler dans un `useCallback` ou un `useEffect` ne relance rien.
|
|
423
|
+
*/
|
|
424
|
+
export function useToast() {
|
|
425
|
+
return toast;
|
|
426
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
.sia-toaster {
|
|
2
|
+
position: fixed;
|
|
3
|
+
z-index: 70;
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
gap: .625rem;
|
|
7
|
+
width: min(var(--sia-toaster-width, 24rem), calc(100vw - 2rem));
|
|
8
|
+
padding: var(--sia-toaster-offset, 1rem);
|
|
9
|
+
/* La pile ne doit pas voler les clics à la page; chaque annonce les reprend. */
|
|
10
|
+
pointer-events: none;
|
|
11
|
+
}
|
|
12
|
+
.sia-toaster > * { pointer-events: auto; }
|
|
13
|
+
|
|
14
|
+
.sia-toaster--top-left { top: 0; left: 0; }
|
|
15
|
+
.sia-toaster--top-center { top: 0; left: 50%; transform: translateX(-50%); }
|
|
16
|
+
.sia-toaster--top-right { top: 0; right: 0; }
|
|
17
|
+
.sia-toaster--bottom-left { bottom: 0; left: 0; }
|
|
18
|
+
.sia-toaster--bottom-center { bottom: 0; left: 50%; transform: translateX(-50%); }
|
|
19
|
+
.sia-toaster--bottom-right { bottom: 0; right: 0; }
|
|
20
|
+
|
|
21
|
+
.sia-toast {
|
|
22
|
+
--sia-toast-color: var(--sia-muted);
|
|
23
|
+
position: relative;
|
|
24
|
+
display: flex;
|
|
25
|
+
align-items: flex-start;
|
|
26
|
+
gap: .75rem;
|
|
27
|
+
overflow: hidden;
|
|
28
|
+
padding: .875rem 1rem;
|
|
29
|
+
border: 1px solid var(--sia-border);
|
|
30
|
+
border-radius: calc(var(--sia-radius) + 2px);
|
|
31
|
+
background: var(--sia-surface-raised);
|
|
32
|
+
box-shadow: var(--sia-elevation-overlay);
|
|
33
|
+
/* Le balayage ne doit pas déclencher le défilement de la page. */
|
|
34
|
+
touch-action: none;
|
|
35
|
+
user-select: none;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.sia-toast[data-state="open"] {
|
|
39
|
+
animation: sia-toast-in var(--sia-duration-slow) var(--sia-easing-spring) both;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* La sortie est plus courte que l'entrée : ce qui arrive se remarque, ce qui
|
|
43
|
+
part ne doit pas se faire attendre. */
|
|
44
|
+
.sia-toast[data-state="closed"] {
|
|
45
|
+
animation: sia-toast-out var(--sia-duration-base) var(--sia-easing-exit) forwards;
|
|
46
|
+
pointer-events: none;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* Pendant le geste, la carte suit le doigt sans transition : une interpolation
|
|
50
|
+
donnerait un retard visible entre le pointeur et la carte. */
|
|
51
|
+
.sia-toast[data-swiping] { transition: none; cursor: grabbing; }
|
|
52
|
+
.sia-toast:not([data-swiping]) {
|
|
53
|
+
transition:
|
|
54
|
+
transform var(--sia-duration-base) var(--sia-easing-standard),
|
|
55
|
+
opacity var(--sia-duration-base) var(--sia-easing-standard);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@keyframes sia-toast-in {
|
|
59
|
+
from { opacity: 0; transform: translate3d(var(--sia-toast-from, 110%), 0, 0) scale(.96); }
|
|
60
|
+
to { opacity: 1; transform: none; }
|
|
61
|
+
}
|
|
62
|
+
@keyframes sia-toast-out {
|
|
63
|
+
from { opacity: 1; transform: none; max-height: 10rem; }
|
|
64
|
+
to { opacity: 0; transform: translate3d(0, -.5rem, 0) scale(.94); max-height: 0; margin-top: -.625rem; padding-block: 0; }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.sia-toast--from-right { --sia-toast-from: 110%; }
|
|
68
|
+
.sia-toast--from-left { --sia-toast-from: -110%; }
|
|
69
|
+
.sia-toast--from-center { --sia-toast-from: 0; }
|
|
70
|
+
|
|
71
|
+
.sia-toast__icon {
|
|
72
|
+
display: grid;
|
|
73
|
+
flex: none;
|
|
74
|
+
place-items: center;
|
|
75
|
+
width: 1.5rem;
|
|
76
|
+
height: 1.5rem;
|
|
77
|
+
border-radius: 999px;
|
|
78
|
+
font-size: .875rem;
|
|
79
|
+
background: color-mix(in srgb, var(--sia-toast-color) 14%, transparent);
|
|
80
|
+
color: var(--sia-toast-color);
|
|
81
|
+
/* L'icône entre après le panneau : l'œil suit le bloc, puis le signe. */
|
|
82
|
+
animation: sia-toast-icon var(--sia-duration-slow) var(--sia-easing-spring) both;
|
|
83
|
+
animation-delay: 60ms;
|
|
84
|
+
}
|
|
85
|
+
@keyframes sia-toast-icon {
|
|
86
|
+
from { opacity: 0; transform: scale(.4); }
|
|
87
|
+
to { opacity: 1; transform: none; }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.sia-toast__body { flex: 1; display: grid; gap: .15rem; min-width: 0; }
|
|
91
|
+
.sia-toast__title { color: var(--sia-foreground); font-size: .9rem; line-height: 1.35; }
|
|
92
|
+
.sia-toast__description { margin: 0; color: var(--sia-muted); font-size: .825rem; line-height: 1.45; }
|
|
93
|
+
|
|
94
|
+
.sia-toast__actions { display: flex; align-items: center; gap: .5rem; margin-top: .4rem; }
|
|
95
|
+
|
|
96
|
+
.sia-toast__action,
|
|
97
|
+
.sia-toast__cancel {
|
|
98
|
+
padding: .3rem .65rem;
|
|
99
|
+
border-radius: var(--sia-radius);
|
|
100
|
+
font-size: .8rem;
|
|
101
|
+
font-weight: 600;
|
|
102
|
+
cursor: pointer;
|
|
103
|
+
transition:
|
|
104
|
+
background-color var(--sia-duration-fast) var(--sia-easing-standard),
|
|
105
|
+
color var(--sia-duration-fast) var(--sia-easing-standard);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* L'action principale porte la couleur du ton : c'est le geste qu'on attend
|
|
109
|
+
de la personne, et il doit se voir sans lire. */
|
|
110
|
+
.sia-toast__action {
|
|
111
|
+
border: 1px solid color-mix(in srgb, var(--sia-toast-color) 32%, transparent);
|
|
112
|
+
background: color-mix(in srgb, var(--sia-toast-color) 12%, transparent);
|
|
113
|
+
color: var(--sia-toast-color);
|
|
114
|
+
}
|
|
115
|
+
.sia-toast__action:hover {
|
|
116
|
+
background: color-mix(in srgb, var(--sia-toast-color) 20%, transparent);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.sia-toast__cancel {
|
|
120
|
+
border: 0;
|
|
121
|
+
background: transparent;
|
|
122
|
+
color: var(--sia-muted);
|
|
123
|
+
}
|
|
124
|
+
.sia-toast__cancel:hover { color: var(--sia-foreground); }
|
|
125
|
+
|
|
126
|
+
.sia-toast__close {
|
|
127
|
+
flex: none;
|
|
128
|
+
padding: .1rem;
|
|
129
|
+
border: 0;
|
|
130
|
+
background: transparent;
|
|
131
|
+
color: var(--sia-muted);
|
|
132
|
+
font-size: 1rem;
|
|
133
|
+
line-height: 1;
|
|
134
|
+
cursor: pointer;
|
|
135
|
+
opacity: .6;
|
|
136
|
+
transition: opacity var(--sia-duration-fast) var(--sia-easing-standard);
|
|
137
|
+
}
|
|
138
|
+
.sia-toast__close:hover { opacity: 1; }
|
|
139
|
+
|
|
140
|
+
/* La barre de temps : elle dit combien il reste, et se fige au survol — la
|
|
141
|
+
suspension de la minuterie et l'animation doivent raconter la même chose. */
|
|
142
|
+
.sia-toast__timer {
|
|
143
|
+
position: absolute;
|
|
144
|
+
inset: auto 0 0 0;
|
|
145
|
+
height: 2px;
|
|
146
|
+
transform-origin: left;
|
|
147
|
+
background: var(--sia-toast-color);
|
|
148
|
+
opacity: .4;
|
|
149
|
+
animation: sia-toast-timer linear forwards;
|
|
150
|
+
}
|
|
151
|
+
.sia-toast__timer[data-paused] { animation-play-state: paused; }
|
|
152
|
+
|
|
153
|
+
@keyframes sia-toast-timer {
|
|
154
|
+
from { transform: scaleX(1); }
|
|
155
|
+
to { transform: scaleX(0); }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* Les tons reprennent ceux d'`Alert` : un même événement doit avoir la même
|
|
159
|
+
couleur en bandeau dans la page et en annonce flottante. */
|
|
160
|
+
.sia-toast--neutral { --sia-toast-color: var(--sia-muted); }
|
|
161
|
+
.sia-toast--loading { --sia-toast-color: var(--sia-muted); }
|
|
162
|
+
.sia-toast--primary { --sia-toast-color: var(--sia-primary); }
|
|
163
|
+
.sia-toast--info { --sia-toast-color: var(--sia-info); }
|
|
164
|
+
.sia-toast--success { --sia-toast-color: var(--sia-success); }
|
|
165
|
+
.sia-toast--warning { --sia-toast-color: var(--sia-warning); }
|
|
166
|
+
.sia-toast--danger { --sia-toast-color: var(--sia-danger); }
|
|
167
|
+
|
|
168
|
+
.sia-toast--danger { border-color: color-mix(in srgb, var(--sia-danger) 28%, var(--sia-border)); }
|
|
169
|
+
.sia-toast--loading .sia-toast__icon { background: transparent; }
|
|
170
|
+
|
|
171
|
+
@media (max-width: 640px) {
|
|
172
|
+
.sia-toaster { width: 100vw; padding: .75rem; }
|
|
173
|
+
.sia-toast--from-right,
|
|
174
|
+
.sia-toast--from-left { --sia-toast-from: 0; }
|
|
175
|
+
}
|