@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.
Files changed (200) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +51 -0
  3. package/dist/diff.mjs +91 -0
  4. package/dist/index.js +325 -0
  5. package/dist/lifecycle.mjs +242 -0
  6. package/dist/lock.mjs +92 -0
  7. package/dist/registry/package/components/Accordion/index.tsx +172 -0
  8. package/dist/registry/package/components/Accordion/styles.css +97 -0
  9. package/dist/registry/package/components/Alert/index.tsx +31 -0
  10. package/dist/registry/package/components/Alert/styles.css +12 -0
  11. package/dist/registry/package/components/AmountDisplay/index.tsx +11 -0
  12. package/dist/registry/package/components/AmountDisplay/styles.css +9 -0
  13. package/dist/registry/package/components/AppShell/index.tsx +6 -0
  14. package/dist/registry/package/components/AppShell/styles.css +7 -0
  15. package/dist/registry/package/components/AspectRatio/index.tsx +29 -0
  16. package/dist/registry/package/components/AspectRatio/styles.css +15 -0
  17. package/dist/registry/package/components/AsyncState/index.tsx +22 -0
  18. package/dist/registry/package/components/AsyncState/styles.css +1 -0
  19. package/dist/registry/package/components/AuditMeta/index.tsx +44 -0
  20. package/dist/registry/package/components/AuditMeta/styles.css +12 -0
  21. package/dist/registry/package/components/AuthLayout/index.tsx +39 -0
  22. package/dist/registry/package/components/AuthLayout/styles.css +6 -0
  23. package/dist/registry/package/components/Autocomplete/index.tsx +8 -0
  24. package/dist/registry/package/components/Avatar/index.tsx +23 -0
  25. package/dist/registry/package/components/Avatar/styles.css +5 -0
  26. package/dist/registry/package/components/Badge/index.tsx +62 -0
  27. package/dist/registry/package/components/Badge/styles.css +26 -0
  28. package/dist/registry/package/components/Breadcrumb/index.tsx +131 -0
  29. package/dist/registry/package/components/Breadcrumb/styles.css +76 -0
  30. package/dist/registry/package/components/Button/index.tsx +91 -0
  31. package/dist/registry/package/components/Button/styles.css +111 -0
  32. package/dist/registry/package/components/Calendar/index.tsx +192 -0
  33. package/dist/registry/package/components/Card/index.tsx +8 -0
  34. package/dist/registry/package/components/Card/styles.css +4 -0
  35. package/dist/registry/package/components/Chart/index.tsx +437 -0
  36. package/dist/registry/package/components/Chart/styles.css +182 -0
  37. package/dist/registry/package/components/Checkbox/index.tsx +193 -0
  38. package/dist/registry/package/components/Checkbox/styles.css +39 -0
  39. package/dist/registry/package/components/ColorPicker/index.tsx +6 -0
  40. package/dist/registry/package/components/ColorPicker/styles.css +2 -0
  41. package/dist/registry/package/components/CommandPalette/index.tsx +7 -0
  42. package/dist/registry/package/components/CommandPalette/styles.css +8 -0
  43. package/dist/registry/package/components/ConfirmDialog/index.tsx +24 -0
  44. package/dist/registry/package/components/Container/index.tsx +11 -0
  45. package/dist/registry/package/components/Container/styles.css +1 -0
  46. package/dist/registry/package/components/Countdown/index.tsx +3 -0
  47. package/dist/registry/package/components/CrudPage/index.tsx +9 -0
  48. package/dist/registry/package/components/CrudPage/styles.css +1 -0
  49. package/dist/registry/package/components/CurrencyInput/index.tsx +15 -0
  50. package/dist/registry/package/components/CurrencyInput/styles.css +1 -0
  51. package/dist/registry/package/components/DataTable/index.tsx +240 -0
  52. package/dist/registry/package/components/DataTable/styles.css +98 -0
  53. package/dist/registry/package/components/DatePicker/index.tsx +102 -0
  54. package/dist/registry/package/components/DateRangeFilter/index.tsx +8 -0
  55. package/dist/registry/package/components/DateRangeFilter/styles.css +4 -0
  56. package/dist/registry/package/components/DateRangePicker/index.tsx +36 -0
  57. package/dist/registry/package/components/DateTimePicker/index.tsx +32 -0
  58. package/dist/registry/package/components/Descriptions/index.tsx +7 -0
  59. package/dist/registry/package/components/Descriptions/styles.css +8 -0
  60. package/dist/registry/package/components/Divider/index.tsx +12 -0
  61. package/dist/registry/package/components/Divider/styles.css +3 -0
  62. package/dist/registry/package/components/Drawer/context.ts +14 -0
  63. package/dist/registry/package/components/Drawer/index.tsx +209 -0
  64. package/dist/registry/package/components/Drawer/styles.css +151 -0
  65. package/dist/registry/package/components/DropdownMenu/index.tsx +350 -0
  66. package/dist/registry/package/components/DropdownMenu/styles.css +100 -0
  67. package/dist/registry/package/components/DurationDisplay/index.tsx +3 -0
  68. package/dist/registry/package/components/EmptyState/index.tsx +23 -0
  69. package/dist/registry/package/components/EmptyState/styles.css +6 -0
  70. package/dist/registry/package/components/EntityMeta/index.tsx +65 -0
  71. package/dist/registry/package/components/EntityMeta/styles.css +9 -0
  72. package/dist/registry/package/components/ErrorState/index.tsx +30 -0
  73. package/dist/registry/package/components/ErrorState/styles.css +6 -0
  74. package/dist/registry/package/components/EventCalendar/index.tsx +8 -0
  75. package/dist/registry/package/components/EventCalendar/styles.css +8 -0
  76. package/dist/registry/package/components/Field/index.tsx +664 -0
  77. package/dist/registry/package/components/Field/styles.css +14 -0
  78. package/dist/registry/package/components/FileUpload/index.tsx +68 -0
  79. package/dist/registry/package/components/FileUpload/styles.css +4 -0
  80. package/dist/registry/package/components/FiltersBar/index.tsx +20 -0
  81. package/dist/registry/package/components/FiltersBar/styles.css +6 -0
  82. package/dist/registry/package/components/Form/index.tsx +211 -0
  83. package/dist/registry/package/components/Form/styles.css +80 -0
  84. package/dist/registry/package/components/FormField/index.tsx +2 -0
  85. package/dist/registry/package/components/HoverCard/index.tsx +121 -0
  86. package/dist/registry/package/components/HoverCard/styles.css +26 -0
  87. package/dist/registry/package/components/IconButton/index.tsx +27 -0
  88. package/dist/registry/package/components/IconButton/styles.css +7 -0
  89. package/dist/registry/package/components/Icons/index.tsx +185 -0
  90. package/dist/registry/package/components/ImageUpload/index.tsx +6 -0
  91. package/dist/registry/package/components/ImageUpload/styles.css +4 -0
  92. package/dist/registry/package/components/Input/index.tsx +103 -0
  93. package/dist/registry/package/components/Input/styles.css +8 -0
  94. package/dist/registry/package/components/InputNumber/index.tsx +196 -0
  95. package/dist/registry/package/components/InputNumber/styles.css +54 -0
  96. package/dist/registry/package/components/JsonEditor/index.tsx +6 -0
  97. package/dist/registry/package/components/JsonEditor/styles.css +2 -0
  98. package/dist/registry/package/components/KpiGrid/index.tsx +11 -0
  99. package/dist/registry/package/components/KpiGrid/styles.css +5 -0
  100. package/dist/registry/package/components/Label/index.tsx +43 -0
  101. package/dist/registry/package/components/Label/styles.css +25 -0
  102. package/dist/registry/package/components/MarkdownEditor/index.tsx +5 -0
  103. package/dist/registry/package/components/MiniCalendar/index.tsx +3 -0
  104. package/dist/registry/package/components/Modal/index.tsx +104 -0
  105. package/dist/registry/package/components/Modal/styles.css +20 -0
  106. package/dist/registry/package/components/MonthPicker/index.tsx +5 -0
  107. package/dist/registry/package/components/MultiSelect/index.tsx +133 -0
  108. package/dist/registry/package/components/MultiSelect/styles.css +3 -0
  109. package/dist/registry/package/components/OtpInput/index.tsx +5 -0
  110. package/dist/registry/package/components/OtpInput/styles.css +2 -0
  111. package/dist/registry/package/components/Overlay/index.tsx +140 -0
  112. package/dist/registry/package/components/Overlay/styles.css +1 -0
  113. package/dist/registry/package/components/PageHeader/index.tsx +24 -0
  114. package/dist/registry/package/components/PageHeader/styles.css +5 -0
  115. package/dist/registry/package/components/Pagination/index.tsx +155 -0
  116. package/dist/registry/package/components/Pagination/styles.css +103 -0
  117. package/dist/registry/package/components/PasswordInput/index.tsx +125 -0
  118. package/dist/registry/package/components/PasswordInput/styles.css +58 -0
  119. package/dist/registry/package/components/PermissionGate/index.tsx +3 -0
  120. package/dist/registry/package/components/PhoneInput/index.tsx +189 -0
  121. package/dist/registry/package/components/PhoneInput/styles.css +35 -0
  122. package/dist/registry/package/components/Popover/index.tsx +12 -0
  123. package/dist/registry/package/components/Popover/styles.css +12 -0
  124. package/dist/registry/package/components/Progress/index.tsx +149 -0
  125. package/dist/registry/package/components/Progress/styles.css +101 -0
  126. package/dist/registry/package/components/QrCode/index.tsx +6 -0
  127. package/dist/registry/package/components/QrCode/styles.css +5 -0
  128. package/dist/registry/package/components/RadioGroup/index.tsx +29 -0
  129. package/dist/registry/package/components/RadioGroup/styles.css +20 -0
  130. package/dist/registry/package/components/Rating/index.tsx +82 -0
  131. package/dist/registry/package/components/Rating/styles.css +41 -0
  132. package/dist/registry/package/components/ReferenceSelect/index.tsx +7 -0
  133. package/dist/registry/package/components/ReferenceSelect/styles.css +2 -0
  134. package/dist/registry/package/components/RelativeTime/index.tsx +22 -0
  135. package/dist/registry/package/components/RelativeTime/styles.css +1 -0
  136. package/dist/registry/package/components/RequireSession/index.tsx +127 -0
  137. package/dist/registry/package/components/RequireSession/styles.css +13 -0
  138. package/dist/registry/package/components/ResizablePanel/index.tsx +6 -0
  139. package/dist/registry/package/components/ResizablePanel/styles.css +5 -0
  140. package/dist/registry/package/components/RichTextEditor/index.tsx +5 -0
  141. package/dist/registry/package/components/RichTextEditor/styles.css +2 -0
  142. package/dist/registry/package/components/ScrollArea/index.tsx +114 -0
  143. package/dist/registry/package/components/ScrollArea/styles.css +74 -0
  144. package/dist/registry/package/components/SearchInput/index.tsx +121 -0
  145. package/dist/registry/package/components/SearchInput/styles.css +6 -0
  146. package/dist/registry/package/components/Select/index.tsx +324 -0
  147. package/dist/registry/package/components/Select/styles.css +58 -0
  148. package/dist/registry/package/components/Sidebar/index.tsx +232 -0
  149. package/dist/registry/package/components/Sidebar/styles.css +140 -0
  150. package/dist/registry/package/components/Skeleton/index.tsx +21 -0
  151. package/dist/registry/package/components/Skeleton/styles.css +2 -0
  152. package/dist/registry/package/components/Slider/index.tsx +193 -0
  153. package/dist/registry/package/components/Slider/styles.css +53 -0
  154. package/dist/registry/package/components/Spinner/index.tsx +28 -0
  155. package/dist/registry/package/components/Spinner/styles.css +8 -0
  156. package/dist/registry/package/components/StatCard/index.tsx +15 -0
  157. package/dist/registry/package/components/StatCard/styles.css +3 -0
  158. package/dist/registry/package/components/Statistic/index.tsx +23 -0
  159. package/dist/registry/package/components/Statistic/styles.css +6 -0
  160. package/dist/registry/package/components/Steps/index.tsx +108 -0
  161. package/dist/registry/package/components/Steps/styles.css +138 -0
  162. package/dist/registry/package/components/Switch/index.tsx +46 -0
  163. package/dist/registry/package/components/Switch/styles.css +26 -0
  164. package/dist/registry/package/components/Tabs/index.tsx +44 -0
  165. package/dist/registry/package/components/Tabs/styles.css +41 -0
  166. package/dist/registry/package/components/TagsInput/index.tsx +6 -0
  167. package/dist/registry/package/components/TagsInput/styles.css +2 -0
  168. package/dist/registry/package/components/Textarea/index.tsx +21 -0
  169. package/dist/registry/package/components/Textarea/styles.css +7 -0
  170. package/dist/registry/package/components/TimePicker/index.tsx +76 -0
  171. package/dist/registry/package/components/TimePicker/styles.css +13 -0
  172. package/dist/registry/package/components/TimeRangePicker/index.tsx +32 -0
  173. package/dist/registry/package/components/TimeRangePicker/styles.css +1 -0
  174. package/dist/registry/package/components/Timeline/index.tsx +8 -0
  175. package/dist/registry/package/components/Timeline/styles.css +12 -0
  176. package/dist/registry/package/components/Timer/index.tsx +9 -0
  177. package/dist/registry/package/components/Toast/index.tsx +426 -0
  178. package/dist/registry/package/components/Toast/styles.css +175 -0
  179. package/dist/registry/package/components/Toggle/index.tsx +181 -0
  180. package/dist/registry/package/components/Toggle/styles.css +124 -0
  181. package/dist/registry/package/components/Tooltip/index.tsx +163 -0
  182. package/dist/registry/package/components/Tooltip/styles.css +57 -0
  183. package/dist/registry/package/components/Tour/index.tsx +8 -0
  184. package/dist/registry/package/components/Tour/styles.css +6 -0
  185. package/dist/registry/package/components/Transfer/index.tsx +7 -0
  186. package/dist/registry/package/components/Transfer/styles.css +6 -0
  187. package/dist/registry/package/components/Tree/index.tsx +8 -0
  188. package/dist/registry/package/components/Tree/styles.css +6 -0
  189. package/dist/registry/package/components/TreeSelect/index.tsx +10 -0
  190. package/dist/registry/package/components/TreeSelect/styles.css +3 -0
  191. package/dist/registry/package/components/Typography/index.tsx +27 -0
  192. package/dist/registry/package/components/Typography/styles.css +16 -0
  193. package/dist/registry/package/components/UnsavedChangesGuard/index.tsx +4 -0
  194. package/dist/registry/package/components/WeekPicker/index.tsx +5 -0
  195. package/dist/registry/package/components/YearPicker/index.tsx +3 -0
  196. package/dist/registry/package/lib/form-adapter-rhf.tsx +141 -0
  197. package/dist/registry/registry.json +1780 -0
  198. package/dist/registry/styles/sia-ui.css +361 -0
  199. package/dist/source.mjs +93 -0
  200. package/package.json +55 -0
@@ -0,0 +1,29 @@
1
+ import { type CSSProperties, type ReactNode } from "react";
2
+ import { cn } from "@sia-ui/utils";
3
+ import "./styles.css";
4
+
5
+ export interface AspectRatioProps {
6
+ /** Le rapport largeur / hauteur. `16 / 9`, `1`, `4 / 3`. */
7
+ ratio?: number;
8
+ children: ReactNode;
9
+ className?: string;
10
+ }
11
+
12
+ /**
13
+ * Une boîte au rapport imposé.
14
+ *
15
+ * Elle existe pour une seule raison : réserver la place avant que l'image ou
16
+ * l'iframe n'arrive. Sans elle, le contenu qui suit saute au chargement — ce
17
+ * que mesure le Cumulative Layout Shift, et ce que les gens ressentent comme
18
+ * une page qui bouge sous le doigt.
19
+ */
20
+ export function AspectRatio({ ratio = 16 / 9, children, className }: AspectRatioProps) {
21
+ return (
22
+ <div
23
+ className={cn("sia-aspect-ratio", className)}
24
+ style={{ "--sia-aspect": String(ratio) } as CSSProperties}
25
+ >
26
+ {children}
27
+ </div>
28
+ );
29
+ }
@@ -0,0 +1,15 @@
1
+ .sia-aspect-ratio {
2
+ position: relative;
3
+ width: 100%;
4
+ overflow: hidden;
5
+ border-radius: var(--sia-radius);
6
+ aspect-ratio: var(--sia-aspect, 1.7778);
7
+ }
8
+
9
+ /* Ce qui est placé dedans remplit la boîte : une image au rapport légèrement
10
+ différent est recadrée plutôt que déformée. */
11
+ .sia-aspect-ratio > * {
12
+ width: 100%;
13
+ height: 100%;
14
+ object-fit: cover;
15
+ }
@@ -0,0 +1,22 @@
1
+ import { type ReactNode } from "react";
2
+ import { Alert } from "../Alert";
3
+ import { Button } from "../Button";
4
+ import { EmptyState } from "../EmptyState";
5
+ import "./styles.css";
6
+
7
+ export interface AsyncStateProps {
8
+ loading?: boolean;
9
+ error?: ReactNode;
10
+ empty?: boolean;
11
+ emptyTitle?: ReactNode;
12
+ emptyDescription?: ReactNode;
13
+ onRetry?: () => void;
14
+ children: ReactNode;
15
+ }
16
+
17
+ export function AsyncState({ loading, error, empty, emptyTitle = "No data", emptyDescription, onRetry, children }: AsyncStateProps) {
18
+ if (loading) return <div className="sia-async-state" role="status"><span className="sia-spinner" /><span>Loading...</span></div>;
19
+ if (error) return <Alert tone="danger" title="Unable to load data" action={onRetry && <Button variant="outline" onClick={onRetry}>Retry</Button>}>{error}</Alert>;
20
+ if (empty) return <EmptyState compact title={emptyTitle} description={emptyDescription} />;
21
+ return <>{children}</>;
22
+ }
@@ -0,0 +1 @@
1
+ .sia-async-state { display:grid; place-items:center; gap:.75rem; min-height:12rem; padding:2rem; text-align:center; color:var(--sia-muted); border:1px dashed var(--sia-border); border-radius:var(--sia-radius); }
@@ -0,0 +1,44 @@
1
+ import { type ReactNode } from "react";
2
+ import { cn, formatDate, formatTimeAgo } from "@sia-ui/utils";
3
+ import "./styles.css";
4
+
5
+ export interface AuditEvent {
6
+ label: ReactNode;
7
+ at: Date | string | number;
8
+ by?: ReactNode;
9
+ detail?: ReactNode;
10
+ }
11
+
12
+ export interface AuditMetaProps {
13
+ events: AuditEvent[];
14
+ locale?: string;
15
+ title?: ReactNode;
16
+ emptyLabel?: ReactNode;
17
+ className?: string;
18
+ }
19
+
20
+ export function AuditMeta({ events, locale = "fr-FR", title = "Historique", emptyLabel = "Aucune activité", className }: AuditMetaProps) {
21
+ return (
22
+ <section className={cn("sia-audit-meta", className)}>
23
+ <h2 className="sia-audit-meta__title">{title}</h2>
24
+ {events.length === 0 ? (
25
+ <p className="sia-audit-meta__empty">{emptyLabel}</p>
26
+ ) : (
27
+ <ol className="sia-audit-meta__list">
28
+ {events.map((event, index) => (
29
+ <li className="sia-audit-meta__event" key={`${new Date(event.at).getTime()}-${index}`}>
30
+ <span className="sia-audit-meta__marker" aria-hidden="true" />
31
+ <div className="sia-audit-meta__content">
32
+ <div className="sia-audit-meta__heading"><strong>{event.label}</strong>{event.by && <span>par {event.by}</span>}</div>
33
+ {event.detail && <div className="sia-audit-meta__detail">{event.detail}</div>}
34
+ <time className="sia-audit-meta__time" dateTime={new Date(event.at).toISOString()} title={formatDate(event.at, { locale, dateStyle: "full", timeStyle: "short" })}>
35
+ {formatTimeAgo(event.at, { locale })}
36
+ </time>
37
+ </div>
38
+ </li>
39
+ ))}
40
+ </ol>
41
+ )}
42
+ </section>
43
+ );
44
+ }
@@ -0,0 +1,12 @@
1
+ .sia-audit-meta { display:grid; gap:1rem; padding:1.25rem; border:1px solid var(--sia-border); border-radius:calc(var(--sia-radius) * 1.25); background:var(--sia-surface); }
2
+ .sia-audit-meta__title { margin:0; font-size:1rem; }
3
+ .sia-audit-meta__empty { margin:0; color:var(--sia-muted); }
4
+ .sia-audit-meta__list { display:grid; gap:0; padding:0; margin:0; list-style:none; }
5
+ .sia-audit-meta__event { position:relative; display:grid; grid-template-columns:1rem minmax(0,1fr); gap:.75rem; padding-bottom:1.25rem; }
6
+ .sia-audit-meta__event:not(:last-child)::before { content:""; position:absolute; top:.75rem; bottom:0; left:.32rem; width:1px; background:var(--sia-border); }
7
+ .sia-audit-meta__event:last-child { padding-bottom:0; }
8
+ .sia-audit-meta__marker { position:relative; z-index:1; width:.7rem; height:.7rem; margin-top:.3rem; border:2px solid var(--sia-surface); border-radius:50%; background:var(--sia-primary); box-shadow:0 0 0 1px var(--sia-primary); }
9
+ .sia-audit-meta__content { display:grid; gap:.25rem; }
10
+ .sia-audit-meta__heading { display:flex; flex-wrap:wrap; gap:.35rem; }
11
+ .sia-audit-meta__heading span,.sia-audit-meta__detail,.sia-audit-meta__time { color:var(--sia-muted); font-size:.84rem; }
12
+ .sia-audit-meta__time { width:max-content; }
@@ -0,0 +1,39 @@
1
+ import { type ReactNode } from "react";
2
+ import { cn } from "@sia-ui/utils";
3
+ import "./styles.css";
4
+
5
+ export interface AuthLayoutProps {
6
+ title: ReactNode;
7
+ description?: ReactNode;
8
+ logo?: ReactNode;
9
+ aside?: ReactNode;
10
+ children: ReactNode;
11
+ footer?: ReactNode;
12
+ className?: string;
13
+ }
14
+ export function AuthLayout({
15
+ title,
16
+ description,
17
+ logo,
18
+ aside,
19
+ children,
20
+ footer,
21
+ className,
22
+ }: AuthLayoutProps) {
23
+ return (
24
+ <main className={cn("sia-auth-layout", className)}>
25
+ {aside && <aside>{aside}</aside>}
26
+ <section>
27
+ <div className="sia-auth-layout__card">
28
+ {logo}
29
+ <header>
30
+ <h1>{title}</h1>
31
+ {description && <p>{description}</p>}
32
+ </header>
33
+ {children}
34
+ {footer && <footer>{footer}</footer>}
35
+ </div>
36
+ </section>
37
+ </main>
38
+ );
39
+ }
@@ -0,0 +1,6 @@
1
+ .sia-auth-layout { min-height:32rem; display:grid; grid-template-columns:1fr 1fr; background:var(--sia-surface); }
2
+ .sia-auth-layout > aside { padding:3rem; color:var(--sia-primary-foreground); background:var(--sia-primary); }
3
+ .sia-auth-layout > section { display:grid; place-items:center; padding:2rem; }
4
+ .sia-auth-layout__card { width:min(100%,26rem); display:grid; gap:1.25rem; }
5
+ .sia-auth-layout h1 { margin:0; }
6
+ .sia-auth-layout header p { color:var(--sia-muted); }
@@ -0,0 +1,8 @@
1
+ import { useMemo, useState } from "react";
2
+ import { cn } from "@sia-ui/utils";
3
+ import type { SelectOption } from "../Select";
4
+ import { useSiaLocale } from "@sia-ui/headless";
5
+ export interface AutocompleteProps { id?: string; options: SelectOption[]; value?: string; defaultValue?: string; onValueChange?: (value: string, option?: SelectOption) => void; placeholder?: string; allowCustomValue?: boolean; disabled?: boolean; className?: string; "aria-describedby"?: string; "aria-labelledby"?: string; "aria-invalid"?: boolean; "aria-required"?: boolean; }
6
+ export function Autocomplete({ id, options, value, defaultValue = "", onValueChange, placeholder: placeholderProp, allowCustomValue = true, disabled, className, ...ariaProps }: AutocompleteProps) {
7
+ const locale = useSiaLocale();
8
+ const placeholder = placeholderProp ?? locale.searchPlaceholder; const initialLabel = options.find((option) => option.value === defaultValue)?.label; const [internal, setInternal] = useState(defaultValue); const [query, setQuery] = useState(initialLabel ? String(initialLabel) : defaultValue); const [open, setOpen] = useState(false); const current = value ?? internal; const filtered = useMemo(() => { const needle = query.toLowerCase(); return options.filter((option) => [String(option.label), ...(option.keywords ?? [])].join(" ").toLowerCase().includes(needle)); }, [options, query]); const select = (option: SelectOption) => { if (value === undefined) setInternal(option.value); setQuery(String(option.label)); setOpen(false); onValueChange?.(option.value, option); }; return <div id={id} className={cn("sia-autocomplete", className)} {...ariaProps}><input value={query} placeholder={placeholder} disabled={disabled} role="combobox" aria-expanded={open} aria-controls={`${id ?? "sia-autocomplete"}-list`} onFocus={() => setOpen(true)} onChange={(event) => { const next = event.target.value; setQuery(next); setOpen(true); if (allowCustomValue) { if (value === undefined) setInternal(next); onValueChange?.(next); } }} onBlur={() => window.setTimeout(() => setOpen(false), 120)} />{open && <div id={`${id ?? "sia-autocomplete"}-list`} role="listbox">{filtered.map((option) => <button type="button" role="option" aria-selected={current === option.value} key={option.value} disabled={option.disabled} onMouseDown={(event) => event.preventDefault()} onClick={() => select(option)}>{option.label}</button>)}</div>}</div>; }
@@ -0,0 +1,23 @@
1
+ import { useState, type ImgHTMLAttributes } from "react";
2
+ import { cn } from "@sia-ui/utils";
3
+ import "./styles.css";
4
+
5
+ export interface AvatarProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, "children"> {
6
+ name: string;
7
+ size?: "sm" | "md" | "lg";
8
+ fallback?: string;
9
+ }
10
+
11
+ function getInitials(name: string) {
12
+ return name.trim().split(/\s+/).slice(0, 2).map((part) => part[0]?.toUpperCase() ?? "").join("");
13
+ }
14
+
15
+ export function Avatar({ name, size = "md", fallback, className, src, alt, onError, ...props }: AvatarProps) {
16
+ const [failed, setFailed] = useState(false);
17
+ return (
18
+ <span className={cn("sia-avatar", `sia-avatar--${size}`, className)} title={name}>
19
+ {src && !failed ? <img src={src} alt={alt ?? name} onError={(event) => { setFailed(true); onError?.(event); }} {...props} /> : <span aria-hidden="true">{fallback ?? getInitials(name)}</span>}
20
+ {!src || failed ? <span className="sia-visually-hidden">{name}</span> : null}
21
+ </span>
22
+ );
23
+ }
@@ -0,0 +1,5 @@
1
+ .sia-avatar { display:inline-grid; place-items:center; overflow:hidden; border-radius:50%; color:var(--sia-primary-foreground); background:var(--sia-primary); font-weight:750; line-height:1; }
2
+ .sia-avatar img { width:100%; height:100%; object-fit:cover; }
3
+ .sia-avatar--sm { width:2rem; height:2rem; font-size:.72rem; }
4
+ .sia-avatar--md { width:2.75rem; height:2.75rem; font-size:.85rem; }
5
+ .sia-avatar--lg { width:4rem; height:4rem; font-size:1.1rem; }
@@ -0,0 +1,62 @@
1
+ import { type HTMLAttributes, type ReactNode } from "react";
2
+ import { cn } from "@sia-ui/utils";
3
+ import { Spinner, type SpinnerProps } from "../Spinner";
4
+ import type { ComponentTone } from "@sia-ui/tokens";
5
+ import { useComponentDefaults } from "@sia-ui/headless";
6
+ import "./styles.css";
7
+
8
+ export interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
9
+ tone?: ComponentTone;
10
+ variant?: "soft" | "solid" | "outline";
11
+ left?: ReactNode;
12
+ right?: ReactNode;
13
+ loading?: boolean;
14
+ loadingLabel?: string;
15
+ spinnerProps?: Omit<SpinnerProps, "label">;
16
+ }
17
+
18
+ export function Badge({
19
+ tone: toneProp,
20
+ variant: variantProp,
21
+ left,
22
+ right,
23
+ loading = false,
24
+ loadingLabel: loadingLabelProp,
25
+ spinnerProps,
26
+ className,
27
+ children,
28
+ ...props
29
+ }: BadgeProps) {
30
+ const defaults = useComponentDefaults<BadgeProps>("badge");
31
+ const tone = toneProp ?? defaults.tone ?? "neutral";
32
+ const variant = variantProp ?? defaults.variant ?? "soft";
33
+ const loadingLabel = loadingLabelProp ?? defaults.loadingLabel ?? "Chargement";
34
+ return (
35
+ <span
36
+ className={cn(
37
+ "sia-badge",
38
+ `sia-badge--${tone}`,
39
+ `sia-badge--${variant}`,
40
+ loading && "sia-badge--loading",
41
+ className,
42
+ )}
43
+ aria-busy={loading || undefined}
44
+ {...props}
45
+ >
46
+ {loading ? (
47
+ <span className="sia-badge__left">
48
+ <Spinner
49
+ size="xs"
50
+ tone="current"
51
+ {...spinnerProps}
52
+ label={loadingLabel}
53
+ />
54
+ </span>
55
+ ) : (
56
+ left && <span className="sia-badge__left">{left}</span>
57
+ )}
58
+ <span className="sia-badge__content">{children}</span>
59
+ {right && <span className="sia-badge__right">{right}</span>}
60
+ </span>
61
+ );
62
+ }
@@ -0,0 +1,26 @@
1
+ .sia-badge { display:inline-flex; align-items:center; min-height:1.5rem; padding:0 .55rem; border:1px solid transparent; border-radius:9999px; font-size:.75rem; font-weight:700; line-height:1; }
2
+ .sia-badge--soft.sia-badge--neutral { color:var(--sia-foreground); background:var(--sia-border); }
3
+ .sia-badge--soft.sia-badge--primary { color:var(--sia-primary); background:color-mix(in srgb,var(--sia-primary) 12%,transparent); }
4
+ .sia-badge--soft.sia-badge--success { color:var(--sia-success); background:color-mix(in srgb,var(--sia-success) 12%,transparent); }
5
+ .sia-badge--soft.sia-badge--warning { color:var(--sia-warning); background:color-mix(in srgb,var(--sia-warning) 12%,transparent); }
6
+ .sia-badge--soft.sia-badge--danger { color:var(--sia-danger); background:color-mix(in srgb,var(--sia-danger) 12%,transparent); }
7
+ .sia-badge--solid.sia-badge--neutral { color:var(--sia-background); background:var(--sia-foreground); }
8
+ .sia-badge--solid.sia-badge--primary { color:var(--sia-primary-foreground); background:var(--sia-primary); }
9
+ .sia-badge--solid.sia-badge--success { color:#fff; background:var(--sia-success); }
10
+ .sia-badge--solid.sia-badge--warning { color:#fff; background:var(--sia-warning); }
11
+ .sia-badge--solid.sia-badge--danger { color:#fff; background:var(--sia-danger); }
12
+ .sia-badge--outline { border-color:currentColor; background:transparent; }
13
+ .sia-badge--outline.sia-badge--primary { color:var(--sia-primary); }
14
+ .sia-badge--outline.sia-badge--success { color:var(--sia-success); }
15
+ .sia-badge--outline.sia-badge--warning { color:var(--sia-warning); }
16
+ .sia-badge--outline.sia-badge--danger { color:var(--sia-danger); }
17
+ .sia-badge__left,.sia-badge__right { display:inline-flex; align-items:center; justify-content:center; flex:none; }
18
+ .sia-badge__left { margin-right:.35rem; }
19
+ .sia-badge__right { margin-left:.35rem; }
20
+ .sia-badge__content { min-width:0; }
21
+ .sia-badge--loading { cursor:progress; }
22
+ /* ComponentTone completeness for components created before the shared contract. */
23
+ .sia-badge--soft.sia-badge--info { color:var(--sia-info); background:color-mix(in srgb,var(--sia-info) 12%,transparent); }
24
+ .sia-badge--solid.sia-badge--info { color:#fff; background:var(--sia-info); }
25
+ .sia-badge--outline.sia-badge--neutral { color:var(--sia-muted); }
26
+ .sia-badge--outline.sia-badge--info { color:var(--sia-info); }
@@ -0,0 +1,131 @@
1
+ import { Fragment, type ReactNode } from "react";
2
+ import { cn } from "@sia-ui/utils";
3
+ import { useSiaLocale } from "@sia-ui/headless";
4
+ import { ChevronRightIcon, MoreHorizontalIcon } from "../Icons";
5
+ import { DropdownMenu, type MenuEntry } from "../DropdownMenu";
6
+ import "./styles.css";
7
+
8
+ export interface BreadcrumbItem {
9
+ key?: string;
10
+ label: ReactNode;
11
+ href?: string;
12
+ icon?: ReactNode;
13
+ onClick?: () => void;
14
+ }
15
+
16
+ export interface BreadcrumbProps {
17
+ items: BreadcrumbItem[];
18
+ /**
19
+ * Au-delà, les niveaux intermédiaires passent dans un menu. Le premier et
20
+ * les deux derniers restent toujours visibles : ce sont les seuls dont on
21
+ * ait vraiment besoin pour se situer.
22
+ */
23
+ maxVisible?: number;
24
+ separator?: ReactNode;
25
+ ariaLabel?: string;
26
+ className?: string;
27
+ }
28
+
29
+ /**
30
+ * Le chemin parcouru.
31
+ *
32
+ * Le dernier élément n'est jamais un lien : c'est la page courante, et un
33
+ * lien vers l'endroit où l'on se trouve déjà n'existe pas.
34
+ */
35
+ export function Breadcrumb({
36
+ items,
37
+ maxVisible = 4,
38
+ separator,
39
+ ariaLabel,
40
+ className,
41
+ }: BreadcrumbProps) {
42
+ const locale = useSiaLocale();
43
+ const mark = separator ?? <ChevronRightIcon />;
44
+
45
+ const overflow = items.length > maxVisible ? items.slice(1, -2) : [];
46
+ const visible =
47
+ overflow.length > 0 ? [items[0]!, null, ...items.slice(-2)] : items;
48
+
49
+ const hidden: MenuEntry[] = overflow.map((item, index) => ({
50
+ key: item.key ?? `overflow-${index}`,
51
+ label: item.label,
52
+ ...(item.icon !== undefined ? { icon: item.icon } : {}),
53
+ onSelect: () => {
54
+ if (item.onClick) item.onClick();
55
+ else if (item.href && typeof window !== "undefined") {
56
+ window.location.assign(item.href);
57
+ }
58
+ },
59
+ }));
60
+
61
+ return (
62
+ <nav
63
+ className={cn("sia-breadcrumb", className)}
64
+ aria-label={ariaLabel ?? locale.breadcrumb}
65
+ >
66
+ <ol>
67
+ {visible.map((item, index) => {
68
+ const last = index === visible.length - 1;
69
+
70
+ return (
71
+ <Fragment key={item ? (item.key ?? index) : "overflow"}>
72
+ <li className="sia-breadcrumb__item">
73
+ {item === null ? (
74
+ <DropdownMenu items={hidden}>
75
+ <button
76
+ type="button"
77
+ className="sia-breadcrumb__more"
78
+ aria-label={locale.more}
79
+ >
80
+ <MoreHorizontalIcon />
81
+ </button>
82
+ </DropdownMenu>
83
+ ) : last ? (
84
+ <span className="sia-breadcrumb__current" aria-current="page">
85
+ {item.icon && (
86
+ <span className="sia-breadcrumb__icon" aria-hidden="true">
87
+ {item.icon}
88
+ </span>
89
+ )}
90
+ {item.label}
91
+ </span>
92
+ ) : item.href ? (
93
+ <a
94
+ className="sia-breadcrumb__link"
95
+ href={item.href}
96
+ onClick={item.onClick}
97
+ >
98
+ {item.icon && (
99
+ <span className="sia-breadcrumb__icon" aria-hidden="true">
100
+ {item.icon}
101
+ </span>
102
+ )}
103
+ {item.label}
104
+ </a>
105
+ ) : (
106
+ <button
107
+ type="button"
108
+ className="sia-breadcrumb__link"
109
+ onClick={item.onClick}
110
+ >
111
+ {item.icon && (
112
+ <span className="sia-breadcrumb__icon" aria-hidden="true">
113
+ {item.icon}
114
+ </span>
115
+ )}
116
+ {item.label}
117
+ </button>
118
+ )}
119
+ </li>
120
+ {!last && (
121
+ <li className="sia-breadcrumb__separator" aria-hidden="true">
122
+ {mark}
123
+ </li>
124
+ )}
125
+ </Fragment>
126
+ );
127
+ })}
128
+ </ol>
129
+ </nav>
130
+ );
131
+ }
@@ -0,0 +1,76 @@
1
+ .sia-breadcrumb { font-size: .825rem; }
2
+ .sia-breadcrumb ol {
3
+ display: flex;
4
+ align-items: center;
5
+ flex-wrap: wrap;
6
+ gap: .25rem;
7
+ margin: 0;
8
+ padding: 0;
9
+ list-style: none;
10
+ }
11
+
12
+ .sia-breadcrumb__item { display: inline-flex; align-items: center; min-width: 0; }
13
+ .sia-breadcrumb__icon { display: grid; place-items: center; margin-right: .3rem; font-size: .9rem; }
14
+
15
+ .sia-breadcrumb__link {
16
+ display: inline-flex;
17
+ align-items: center;
18
+ max-width: 14rem;
19
+ padding: .2rem .35rem;
20
+ border: 0;
21
+ border-radius: var(--sia-radius);
22
+ background: transparent;
23
+ color: var(--sia-muted);
24
+ font: inherit;
25
+ text-decoration: none;
26
+ white-space: nowrap;
27
+ overflow: hidden;
28
+ text-overflow: ellipsis;
29
+ cursor: pointer;
30
+ transition:
31
+ color var(--sia-duration-fast) var(--sia-easing-standard),
32
+ background-color var(--sia-duration-fast) var(--sia-easing-standard);
33
+ }
34
+ .sia-breadcrumb__link:hover { background: var(--sia-surface); color: var(--sia-foreground); }
35
+
36
+ .sia-breadcrumb__current {
37
+ display: inline-flex;
38
+ align-items: center;
39
+ max-width: 18rem;
40
+ padding: .2rem .35rem;
41
+ color: var(--sia-foreground);
42
+ font-weight: 600;
43
+ white-space: nowrap;
44
+ overflow: hidden;
45
+ text-overflow: ellipsis;
46
+ }
47
+
48
+ .sia-breadcrumb__separator {
49
+ display: grid;
50
+ place-items: center;
51
+ color: var(--sia-border-strong);
52
+ font-size: .75rem;
53
+ }
54
+
55
+ .sia-breadcrumb__more {
56
+ display: grid;
57
+ place-items: center;
58
+ padding: .2rem .35rem;
59
+ border: 0;
60
+ border-radius: var(--sia-radius);
61
+ background: transparent;
62
+ color: var(--sia-muted);
63
+ font-size: .95rem;
64
+ cursor: pointer;
65
+ transition:
66
+ background-color var(--sia-duration-fast) var(--sia-easing-standard),
67
+ color var(--sia-duration-fast) var(--sia-easing-standard);
68
+ }
69
+ .sia-breadcrumb__more:hover { background: var(--sia-surface); color: var(--sia-foreground); }
70
+
71
+ /* Sous 480 px seuls les deux derniers niveaux tiennent : le reste se replie
72
+ plutôt que de passer à la ligne et de repousser la page. */
73
+ @media (max-width: 480px) {
74
+ .sia-breadcrumb__link { max-width: 8rem; }
75
+ .sia-breadcrumb__current { max-width: 10rem; }
76
+ }
@@ -0,0 +1,91 @@
1
+ import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from "react";
2
+ import { cn } from "@sia-ui/utils";
3
+ import { Spinner, type SpinnerProps } from "../Spinner";
4
+ import { Tooltip, type TooltipProps } from "../Tooltip";
5
+ import { useComponentDefaults } from "@sia-ui/headless";
6
+ import type { ComponentTone } from "@sia-ui/tokens";
7
+ import "./styles.css";
8
+
9
+ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
10
+ variant?: "solid" | "outline" | "ghost";
11
+ /**
12
+ * Le ton sémantique de l'action.
13
+ *
14
+ * `.sia-button--danger` existait dans la feuille de style sans qu'aucune
15
+ * prop ne permette de l'atteindre : une action de suppression ne pouvait pas
16
+ * être rouge sans `className` manuel.
17
+ */
18
+ tone?: ComponentTone;
19
+ size?: "sm" | "md" | "lg";
20
+ radius?: "default" | "none" | "sm" | "lg" | "full";
21
+ loading?: boolean;
22
+ loadingLabel?: string;
23
+ spinnerProps?: Omit<SpinnerProps, "label">;
24
+ tooltip?: Omit<TooltipProps, "children">;
25
+ leftIcon?: ReactNode;
26
+ rightIcon?: ReactNode;
27
+ }
28
+
29
+ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
30
+ (
31
+ {
32
+ className,
33
+ variant: variantProp,
34
+ tone: toneProp,
35
+ size: sizeProp,
36
+ radius: radiusProp,
37
+ loading,
38
+ loadingLabel: loadingLabelProp,
39
+ spinnerProps,
40
+ tooltip,
41
+ disabled,
42
+ leftIcon,
43
+ rightIcon,
44
+ children,
45
+ ...props
46
+ },
47
+ ref,
48
+ ) => {
49
+ const defaults = useComponentDefaults<ButtonProps>("button");
50
+ const variant = variantProp ?? defaults.variant ?? "solid";
51
+ const tone = toneProp ?? defaults.tone ?? "primary";
52
+ const size = sizeProp ?? defaults.size ?? "md";
53
+ const radius = radiusProp ?? defaults.radius ?? "default";
54
+ const loadingLabel =
55
+ loadingLabelProp ?? defaults.loadingLabel ?? "Chargement";
56
+ const button = (
57
+ <button
58
+ ref={ref}
59
+ type="button"
60
+ className={cn(
61
+ "sia-button",
62
+ `sia-button--${variant}`,
63
+ `sia-button--${tone}`,
64
+ `sia-button--${size}`,
65
+ `sia-radius--${radius}`,
66
+ className,
67
+ )}
68
+ disabled={disabled || loading}
69
+ aria-busy={loading || undefined}
70
+ {...props}
71
+ >
72
+ {loading ? (
73
+ <Spinner
74
+ size="sm"
75
+ tone="current"
76
+ {...spinnerProps}
77
+ label={loadingLabel}
78
+ />
79
+ ) : (
80
+ leftIcon
81
+ )}
82
+ <span>{children}</span>
83
+ {!loading && rightIcon}
84
+ </button>
85
+ );
86
+
87
+ return tooltip ? <Tooltip {...tooltip}>{button}</Tooltip> : button;
88
+ },
89
+ );
90
+
91
+ Button.displayName = "Button";