@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,58 @@
1
+ .sia-select { position:relative; width:100%; min-width:0; }
2
+ .sia-select--disabled { opacity:.6; }
3
+ .sia-select__trigger { display:flex; align-items:center; justify-content:space-between; gap:.75rem; width:100%; min-height:2.5rem; padding:.55rem .7rem .55rem .75rem; border:1px solid var(--sia-border); border-radius:var(--sia-radius); color:var(--sia-foreground); background:var(--sia-background); font:inherit; text-align:left; cursor:pointer; }
4
+ .sia-select__trigger:hover:not(:disabled) { border-color:color-mix(in srgb,var(--sia-primary) 55%,var(--sia-border)); }
5
+ .sia-select__trigger:focus-visible { outline:3px solid color-mix(in srgb,var(--sia-primary) 30%,transparent); outline-offset:2px; }
6
+ .sia-select__trigger--invalid { border-color:var(--sia-danger); }
7
+ .sia-select__trigger:disabled { cursor:not-allowed; }
8
+ .sia-select__value { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
9
+ .sia-select__placeholder { color:var(--sia-muted); }
10
+ .sia-select__actions { display:inline-flex; align-items:center; gap:.35rem; flex:none; color:var(--sia-muted); }
11
+ .sia-select__clear { display:inline-grid; place-items:center; width:1.35rem; height:1.35rem; border-radius:50%; font-size:1.05rem; line-height:1; }
12
+ .sia-select__clear:hover { color:var(--sia-foreground); background:var(--sia-surface); }
13
+ .sia-select__chevron { transform:translateY(-.5rem); font-size:1.1rem; }
14
+ .sia-select__overlay { overflow:hidden; border:1px solid var(--sia-border); border-radius:var(--sia-radius); color:var(--sia-foreground); background:var(--sia-background); box-shadow:0 14px 36px rgb(15 23 42 / .16); }
15
+ .sia-select__panel { display:grid; min-width:12rem; max-height:min(22rem,calc(100vh - 1rem)); }
16
+ .sia-select__search-wrap { display:flex; align-items:center; gap:.5rem; padding:.55rem .65rem; color:var(--sia-muted); border-bottom:1px solid var(--sia-border); }
17
+ .sia-select__search { min-width:0; width:100%; padding:.2rem 0; border:0; outline:0; color:var(--sia-foreground); background:transparent; font:inherit; }
18
+ .sia-select__options { overflow-y:auto; padding:.35rem; }
19
+ .sia-select__option { display:flex; align-items:center; justify-content:space-between; gap:.75rem; width:100%; padding:.55rem .65rem; border:0; border-radius:calc(var(--sia-radius) * .7); color:inherit; background:transparent; font:inherit; text-align:left; cursor:pointer; }
20
+ .sia-select__option:hover:not(:disabled),.sia-select__option--active:not(:disabled) { background:var(--sia-surface); }
21
+ .sia-select__option[aria-selected="true"] { color:var(--sia-primary); background:color-mix(in srgb,var(--sia-primary) 10%,transparent); }
22
+ .sia-select__option:disabled { opacity:.5; cursor:not-allowed; }
23
+ .sia-select__option-content { display:grid; gap:.15rem; min-width:0; }
24
+ .sia-select__option-content small { color:var(--sia-muted); font-size:.78rem; line-height:1.35; }
25
+ .sia-select__status { padding:1rem; color:var(--sia-muted); font-size:.875rem; text-align:center; }
26
+ .sia-select__status--error { color:var(--sia-danger); }
27
+ .sia-select__chevron { display:block; line-height:1; transform:none; }
28
+
29
+ /* Le chevron tourne à l'ouverture — l'affordance standard d'un déclencheur de
30
+ menu. Impossible avec l'ancien caractère : son décalage de ligne de base
31
+ dépendait de la police, donc le centre de rotation aussi. */
32
+ .sia-select__chevron {
33
+ width: 1.05em;
34
+ height: 1.05em;
35
+ flex: none;
36
+ color: var(--sia-muted);
37
+ transition:
38
+ transform var(--sia-duration-base) var(--sia-easing-standard),
39
+ color var(--sia-duration-fast) var(--sia-easing-standard);
40
+ }
41
+
42
+ [aria-expanded="true"] > .sia-select__chevron {
43
+ transform: rotate(180deg);
44
+ color: var(--sia-primary);
45
+ }
46
+
47
+ .sia-select__trigger:hover:not(:disabled) .sia-select__chevron {
48
+ color: var(--sia-foreground);
49
+ }
50
+
51
+ /* Le panneau entre au lieu d'apparaître d'un coup. */
52
+ .sia-select__menu,
53
+ .sia-select__panel,
54
+ .sia-select__list {
55
+ animation: sia-scale-in var(--sia-duration-base) var(--sia-easing-entrance);
56
+ transform-origin: top center;
57
+ box-shadow: var(--sia-elevation-overlay);
58
+ }
@@ -0,0 +1,232 @@
1
+ import { useState, type ReactNode } from "react";
2
+ import { cn } from "@sia-ui/utils";
3
+ import { useAccordionState } from "@sia-ui/headless";
4
+ import { ChevronDownIcon } from "../Icons";
5
+ import "./styles.css";
6
+
7
+ export interface SidebarItem {
8
+ key: string;
9
+ label: ReactNode;
10
+ icon?: ReactNode;
11
+ /** Rend l'entrée comme un lien. Sans lui, c'est un bouton. */
12
+ href?: string;
13
+ badge?: ReactNode;
14
+ disabled?: boolean;
15
+ /** Sous-entrées. Une seule profondeur : au-delà, c'est un menu, pas une barre. */
16
+ items?: Array<Omit<SidebarItem, "items">>;
17
+ onClick?: () => void;
18
+ }
19
+
20
+ export interface SidebarSection {
21
+ key: string;
22
+ /** Le titre du groupe. Masqué en mode réduit. */
23
+ title: ReactNode;
24
+ items: SidebarItem[];
25
+ }
26
+
27
+ export type SidebarEntry = SidebarItem | SidebarSection;
28
+
29
+ function isSection(entry: SidebarEntry): entry is SidebarSection {
30
+ return "title" in entry && Array.isArray(entry.items);
31
+ }
32
+
33
+ export interface SidebarProps {
34
+ items: SidebarEntry[];
35
+ /** La clé de l'entrée courante. Un parent s'ouvre si un enfant l'est. */
36
+ activeKey?: string;
37
+ collapsed?: boolean;
38
+ header?: ReactNode;
39
+ footer?: ReactNode;
40
+ onNavigate?: (item: SidebarItem) => void;
41
+ ariaLabel?: string;
42
+ className?: string;
43
+ }
44
+
45
+ /**
46
+ * La navigation principale.
47
+ *
48
+ * Elle se pose dans le `navigation` d'`AppShell`, qui gère déjà la largeur et
49
+ * le repli. Ici, seulement les entrées : ce qui est actif, ce qui est ouvert,
50
+ * ce qui se replie en icônes.
51
+ */
52
+ export function Sidebar({
53
+ items,
54
+ activeKey,
55
+ collapsed = false,
56
+ header,
57
+ footer,
58
+ onNavigate,
59
+ ariaLabel,
60
+ className,
61
+ }: SidebarProps) {
62
+ return (
63
+ <nav
64
+ className={cn("sia-sidebar", collapsed && "sia-sidebar--collapsed", className)}
65
+ {...(ariaLabel ? { "aria-label": ariaLabel } : {})}
66
+ >
67
+ {header && <div className="sia-sidebar__header">{header}</div>}
68
+
69
+ <div className="sia-sidebar__body">
70
+ {items.map((entry) =>
71
+ isSection(entry) ? (
72
+ <div key={entry.key} className="sia-sidebar__section">
73
+ <div className="sia-sidebar__section-title">{entry.title}</div>
74
+ {entry.items.map((item) => (
75
+ <SidebarNode
76
+ key={item.key}
77
+ item={item}
78
+ {...(activeKey !== undefined ? { activeKey } : {})}
79
+ collapsed={collapsed}
80
+ {...(onNavigate ? { onNavigate } : {})}
81
+ />
82
+ ))}
83
+ </div>
84
+ ) : (
85
+ <SidebarNode
86
+ key={entry.key}
87
+ item={entry}
88
+ {...(activeKey !== undefined ? { activeKey } : {})}
89
+ collapsed={collapsed}
90
+ {...(onNavigate ? { onNavigate } : {})}
91
+ />
92
+ ),
93
+ )}
94
+ </div>
95
+
96
+ {footer && <div className="sia-sidebar__footer">{footer}</div>}
97
+ </nav>
98
+ );
99
+ }
100
+
101
+ interface SidebarNodeProps {
102
+ item: SidebarItem;
103
+ activeKey?: string;
104
+ collapsed: boolean;
105
+ onNavigate?: (item: SidebarItem) => void;
106
+ }
107
+
108
+ function SidebarNode({
109
+ item,
110
+ activeKey,
111
+ collapsed,
112
+ onNavigate,
113
+ }: SidebarNodeProps) {
114
+ const children = item.items ?? [];
115
+ const hasChildren = children.length > 0;
116
+ const holdsActive = children.some((child) => child.key === activeKey);
117
+ const active = item.key === activeKey;
118
+
119
+ // Le groupe qui contient la page courante s'ouvre de lui-même : arriver sur
120
+ // une page par un lien direct doit montrer où l'on est dans l'arbre.
121
+ const disclosure = useAccordionState({
122
+ type: "multiple",
123
+ defaultValue: holdsActive ? [item.key] : [],
124
+ });
125
+ const [everToggled, setEverToggled] = useState(false);
126
+ const open = everToggled ? disclosure.isOpen(item.key) : holdsActive;
127
+
128
+ const content = (
129
+ <>
130
+ {item.icon && <span className="sia-sidebar__icon" aria-hidden="true">{item.icon}</span>}
131
+ <span className="sia-sidebar__label">{item.label}</span>
132
+ {item.badge !== undefined && (
133
+ <span className="sia-sidebar__badge">{item.badge}</span>
134
+ )}
135
+ {hasChildren && (
136
+ <span className="sia-sidebar__chevron" aria-hidden="true">
137
+ <ChevronDownIcon />
138
+ </span>
139
+ )}
140
+ </>
141
+ );
142
+
143
+ // Le titre natif suffit en mode réduit : un vrai tooltip demanderait un
144
+ // portail et un moteur de placement pour afficher trois mots.
145
+ const tooltip =
146
+ collapsed && typeof item.label === "string" ? item.label : undefined;
147
+
148
+ return (
149
+ <div className="sia-sidebar__node" data-open={open || undefined}>
150
+ {item.href && !hasChildren ? (
151
+ <a
152
+ href={item.href}
153
+ className={cn("sia-sidebar__item", active && "sia-sidebar__item--active")}
154
+ aria-current={active ? "page" : undefined}
155
+ {...(tooltip ? { title: tooltip } : {})}
156
+ onClick={() => onNavigate?.(item)}
157
+ >
158
+ {content}
159
+ </a>
160
+ ) : (
161
+ <button
162
+ type="button"
163
+ className={cn(
164
+ "sia-sidebar__item",
165
+ (active || (holdsActive && collapsed)) && "sia-sidebar__item--active",
166
+ )}
167
+ disabled={item.disabled}
168
+ {...(hasChildren ? { "aria-expanded": open } : {})}
169
+ {...(active ? { "aria-current": "page" as const } : {})}
170
+ {...(tooltip ? { title: tooltip } : {})}
171
+ onClick={() => {
172
+ if (hasChildren) {
173
+ setEverToggled(true);
174
+ disclosure.toggle(item.key);
175
+ return;
176
+ }
177
+ item.onClick?.();
178
+ onNavigate?.(item);
179
+ }}
180
+ >
181
+ {content}
182
+ </button>
183
+ )}
184
+
185
+ {hasChildren && (
186
+ <div className="sia-sidebar__panel">
187
+ <div className="sia-sidebar__sub" inert={open ? undefined : true}>
188
+ {children.map((child) => {
189
+ const childActive = child.key === activeKey;
190
+ const childClass = cn(
191
+ "sia-sidebar__item",
192
+ "sia-sidebar__item--child",
193
+ childActive && "sia-sidebar__item--active",
194
+ );
195
+ return child.href ? (
196
+ <a
197
+ key={child.key}
198
+ href={child.href}
199
+ className={childClass}
200
+ aria-current={childActive ? "page" : undefined}
201
+ onClick={() => onNavigate?.(child)}
202
+ >
203
+ <span className="sia-sidebar__label">{child.label}</span>
204
+ {child.badge !== undefined && (
205
+ <span className="sia-sidebar__badge">{child.badge}</span>
206
+ )}
207
+ </a>
208
+ ) : (
209
+ <button
210
+ key={child.key}
211
+ type="button"
212
+ className={childClass}
213
+ disabled={child.disabled}
214
+ {...(childActive ? { "aria-current": "page" as const } : {})}
215
+ onClick={() => {
216
+ child.onClick?.();
217
+ onNavigate?.(child);
218
+ }}
219
+ >
220
+ <span className="sia-sidebar__label">{child.label}</span>
221
+ {child.badge !== undefined && (
222
+ <span className="sia-sidebar__badge">{child.badge}</span>
223
+ )}
224
+ </button>
225
+ );
226
+ })}
227
+ </div>
228
+ </div>
229
+ )}
230
+ </div>
231
+ );
232
+ }
@@ -0,0 +1,140 @@
1
+ .sia-sidebar {
2
+ display: flex;
3
+ flex-direction: column;
4
+ height: 100%;
5
+ min-height: 0;
6
+ padding: .5rem;
7
+ gap: .5rem;
8
+ font-size: .875rem;
9
+ }
10
+
11
+ .sia-sidebar__body { flex: 1; min-height: 0; overflow-y: auto; display: grid; gap: .125rem; align-content: start; }
12
+ .sia-sidebar__header,
13
+ .sia-sidebar__footer { flex: none; padding: .25rem; }
14
+
15
+ .sia-sidebar__section { display: grid; gap: .125rem; }
16
+ .sia-sidebar__section + .sia-sidebar__section { margin-top: .75rem; }
17
+ .sia-sidebar__section-title {
18
+ padding: .5rem .625rem .25rem;
19
+ color: var(--sia-muted);
20
+ font-size: .7rem;
21
+ font-weight: 600;
22
+ letter-spacing: .05em;
23
+ text-transform: uppercase;
24
+ /* En mode réduit le titre disparaît sans laisser de trou. */
25
+ transition:
26
+ opacity var(--sia-duration-fast) var(--sia-easing-standard),
27
+ height var(--sia-duration-base) var(--sia-easing-standard);
28
+ }
29
+
30
+ .sia-sidebar__item {
31
+ position: relative;
32
+ display: flex;
33
+ align-items: center;
34
+ gap: .625rem;
35
+ width: 100%;
36
+ padding: .5rem .625rem;
37
+ border: 0;
38
+ border-radius: var(--sia-radius);
39
+ background: transparent;
40
+ color: var(--sia-muted);
41
+ font: inherit;
42
+ text-align: left;
43
+ text-decoration: none;
44
+ cursor: pointer;
45
+ transition:
46
+ background-color var(--sia-duration-fast) var(--sia-easing-standard),
47
+ color var(--sia-duration-fast) var(--sia-easing-standard);
48
+ }
49
+ .sia-sidebar__item:hover:not(:disabled) { background: var(--sia-surface); color: var(--sia-foreground); }
50
+ .sia-sidebar__item:disabled { opacity: .45; cursor: not-allowed; }
51
+
52
+ .sia-sidebar__item--active {
53
+ background: color-mix(in srgb, var(--sia-primary) 10%, transparent);
54
+ color: var(--sia-primary);
55
+ font-weight: 600;
56
+ }
57
+
58
+ /* Le repère de l'entrée courante : il grandit depuis le centre plutôt que
59
+ d'apparaître d'un coup. */
60
+ .sia-sidebar__item--active::before {
61
+ content: "";
62
+ position: absolute;
63
+ left: 0;
64
+ top: 50%;
65
+ width: 3px;
66
+ height: 1.125rem;
67
+ border-radius: 0 3px 3px 0;
68
+ background: var(--sia-primary);
69
+ transform: translateY(-50%) scaleY(0);
70
+ animation: sia-sidebar-mark var(--sia-duration-base) var(--sia-easing-spring) forwards;
71
+ }
72
+ @keyframes sia-sidebar-mark {
73
+ to { transform: translateY(-50%) scaleY(1); }
74
+ }
75
+
76
+ .sia-sidebar__icon { display: grid; place-items: center; flex: none; width: 1.125rem; font-size: 1.05rem; }
77
+ .sia-sidebar__label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
78
+
79
+ .sia-sidebar__badge {
80
+ flex: none;
81
+ min-width: 1.25rem;
82
+ padding: 0 .35rem;
83
+ border-radius: 999px;
84
+ background: var(--sia-surface);
85
+ color: var(--sia-muted);
86
+ font-size: .7rem;
87
+ font-variant-numeric: tabular-nums;
88
+ line-height: 1.25rem;
89
+ text-align: center;
90
+ }
91
+ .sia-sidebar__item--active .sia-sidebar__badge {
92
+ background: color-mix(in srgb, var(--sia-primary) 16%, transparent);
93
+ color: var(--sia-primary);
94
+ }
95
+
96
+ .sia-sidebar__chevron {
97
+ display: grid;
98
+ place-items: center;
99
+ flex: none;
100
+ color: var(--sia-muted);
101
+ font-size: .8rem;
102
+ transition: transform var(--sia-duration-base) var(--sia-easing-standard);
103
+ }
104
+ .sia-sidebar__node[data-open] .sia-sidebar__chevron { transform: rotate(180deg); }
105
+
106
+ /* Même technique que l'accordéon : la hauteur s'interpole sans être mesurée. */
107
+ .sia-sidebar__panel {
108
+ display: grid;
109
+ grid-template-rows: 0fr;
110
+ transition: grid-template-rows var(--sia-duration-base) var(--sia-easing-standard);
111
+ }
112
+ .sia-sidebar__node[data-open] .sia-sidebar__panel { grid-template-rows: 1fr; }
113
+ .sia-sidebar__sub { overflow: hidden; min-height: 0; display: grid; gap: .125rem; }
114
+
115
+ .sia-sidebar__item--child {
116
+ margin-left: 1.5rem;
117
+ padding-block: .375rem;
118
+ font-size: .825rem;
119
+ }
120
+ /* Le filet qui relie les sous-entrées à leur parent. */
121
+ .sia-sidebar__sub { position: relative; }
122
+ .sia-sidebar__sub::before {
123
+ content: "";
124
+ position: absolute;
125
+ top: .375rem;
126
+ bottom: .375rem;
127
+ left: 1.25rem;
128
+ width: 1px;
129
+ background: var(--sia-border);
130
+ }
131
+
132
+ /* ── Mode réduit ───────────────────────────────────────────────────────── */
133
+
134
+ .sia-sidebar--collapsed .sia-sidebar__item { justify-content: center; padding-inline: .5rem; }
135
+ .sia-sidebar--collapsed .sia-sidebar__label,
136
+ .sia-sidebar--collapsed .sia-sidebar__badge,
137
+ .sia-sidebar--collapsed .sia-sidebar__chevron { display: none; }
138
+ .sia-sidebar--collapsed .sia-sidebar__section-title { height: 0; padding: 0; opacity: 0; overflow: hidden; }
139
+ /* Replier ferme les sous-entrées : elles n'auraient plus de libellé. */
140
+ .sia-sidebar--collapsed .sia-sidebar__panel { grid-template-rows: 0fr; }
@@ -0,0 +1,21 @@
1
+ import { type CSSProperties, type HTMLAttributes } from "react";
2
+ import { cn } from "@sia-ui/utils";
3
+ import "./styles.css";
4
+
5
+ export interface SkeletonProps extends HTMLAttributes<HTMLDivElement> {
6
+ width?: CSSProperties["width"];
7
+ height?: CSSProperties["height"];
8
+ radius?: CSSProperties["borderRadius"];
9
+ circle?: boolean;
10
+ }
11
+
12
+ export function Skeleton({ className, width = "100%", height = "1rem", radius, circle, style, ...props }: SkeletonProps) {
13
+ return (
14
+ <div
15
+ className={cn("sia-skeleton", circle && "sia-skeleton--circle", className)}
16
+ aria-hidden="true"
17
+ style={{ width, height, borderRadius: circle ? "9999px" : radius, ...style }}
18
+ {...props}
19
+ />
20
+ );
21
+ }
@@ -0,0 +1,2 @@
1
+ .sia-skeleton { display:block; max-width:100%; border-radius:var(--sia-radius); background:linear-gradient(90deg,var(--sia-border),color-mix(in srgb,var(--sia-border) 45%,var(--sia-background)),var(--sia-border)); background-size:200% 100%; animation:sia-skeleton 1.4s ease-in-out infinite; }
2
+ .sia-skeleton--circle { aspect-ratio:1; }
@@ -0,0 +1,193 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { cn } from "@sia-ui/utils";
3
+ import { useSliderState, type SliderValue } from "@sia-ui/headless";
4
+
5
+ export type { SliderValue };
6
+ export type SliderRangeValue = [number, number];
7
+
8
+ export interface SliderProps {
9
+ /** Nom du champ, quand le slider est posé dans un formulaire. */
10
+ name?: string | undefined;
11
+ /** Mode contrôlé. Laisser vide pour un slider non contrôlé. */
12
+ value?: SliderValue | undefined;
13
+ /** Valeur de départ en mode non contrôlé. */
14
+ defaultValue?: SliderValue | undefined;
15
+ min?: number | undefined;
16
+ max?: number | undefined;
17
+ step?: number | undefined;
18
+ range?: boolean | undefined;
19
+ onChange?: ((value: SliderValue) => void) | undefined;
20
+ /**
21
+ * Alias de `onChange`, tel que l'attendaient les composants du registre.
22
+ * Les deux sont appelés.
23
+ */
24
+ onValueChange?: ((value: SliderValue) => void) | undefined;
25
+ /** Écart minimal entre les deux poignées, en mode plage. */
26
+ minDistance?: number | undefined;
27
+ /** Affiche la valeur courante à côté de la piste. */
28
+ showValue?: boolean | undefined;
29
+ /** Rendu de la valeur affichée — devise, unité, arrondi. */
30
+ formatValue?: ((value: number) => string) | undefined;
31
+ /** Libellé de la poignée basse, annoncé aux lecteurs d'écran. */
32
+ startLabel?: string | undefined;
33
+ /** Libellé de la poignée haute, en mode plage. */
34
+ endLabel?: string | undefined;
35
+ disabled?: boolean | undefined;
36
+ className?: string | undefined;
37
+ }
38
+ import { Tooltip } from "../Tooltip";
39
+ import "./styles.css";
40
+
41
+ /**
42
+ * Rendu DOM du slider.
43
+ *
44
+ * Tout le calcul — pas, bornes, pourcentages, poignée la plus proche,
45
+ * non-croisement — vit dans `useSliderState`. Ce fichier ne garde que ce qui
46
+ * est propre au web : la mesure de la piste, les événements souris et le JSX.
47
+ */
48
+ export function Slider({
49
+ name,
50
+ value,
51
+ defaultValue,
52
+ min = 0,
53
+ max = 100,
54
+ step = 1,
55
+ range = false,
56
+ minDistance = 0,
57
+ showValue = false,
58
+ formatValue,
59
+ startLabel,
60
+ endLabel,
61
+ onChange,
62
+ onValueChange,
63
+ disabled,
64
+ className,
65
+ }: SliderProps) {
66
+ const trackRef = useRef<HTMLDivElement>(null);
67
+ const [activeThumb, setActiveThumb] = useState<number | null>(null);
68
+
69
+ // `onValueChange` est le nom qu'employaient les composants du registre.
70
+ // Les deux sont notifiés plutôt que d'imposer un renommage à l'appelant.
71
+ const notify = (next: SliderValue) => {
72
+ onChange?.(next);
73
+ onValueChange?.(next);
74
+ };
75
+
76
+ const slider = useSliderState({
77
+ value,
78
+ defaultValue,
79
+ min,
80
+ max,
81
+ step,
82
+ range,
83
+ minDistance,
84
+ onChange: notify,
85
+ });
86
+
87
+ /** Le seul pont entre le pointeur et la logique : un ratio de 0 à 1. */
88
+ const valueFromClientX = (clientX: number) => {
89
+ const rect = trackRef.current?.getBoundingClientRect();
90
+ if (!rect || rect.width === 0) return slider.values[0] ?? min;
91
+ return slider.valueAt((clientX - rect.left) / rect.width);
92
+ };
93
+
94
+ useEffect(() => {
95
+ if (activeThumb === null) return undefined;
96
+
97
+ const onMove = (event: MouseEvent) => {
98
+ slider.moveThumb(activeThumb, valueFromClientX(event.clientX));
99
+ };
100
+ const onUp = () => setActiveThumb(null);
101
+
102
+ window.addEventListener("mousemove", onMove);
103
+ window.addEventListener("mouseup", onUp);
104
+
105
+ return () => {
106
+ window.removeEventListener("mousemove", onMove);
107
+ window.removeEventListener("mouseup", onUp);
108
+ };
109
+ });
110
+
111
+ const onTrackDown = (event: React.MouseEvent) => {
112
+ if (disabled) return;
113
+
114
+ const next = valueFromClientX(event.clientX);
115
+ const thumb = slider.nearestThumb(next);
116
+
117
+ setActiveThumb(thumb);
118
+ slider.moveThumb(thumb, next);
119
+ };
120
+
121
+ const [first = 0, second = 0] = slider.percents;
122
+
123
+ return (
124
+ <div
125
+ className={cn("sia-slider", className, disabled && "is-disabled")}
126
+ data-name={name}
127
+ >
128
+ <div ref={trackRef} className="sia-slider-track" onMouseDown={onTrackDown}>
129
+ <div
130
+ className="sia-slider-range"
131
+ style={
132
+ range
133
+ ? { left: `${first}%`, width: `${second - first}%` }
134
+ : { width: `${first}%` }
135
+ }
136
+ />
137
+ </div>
138
+
139
+ {showValue && (
140
+ <span className="sia-slider-value">
141
+ {slider.values
142
+ .map((one) => (formatValue ? formatValue(one) : String(one)))
143
+ .join(" – ")}
144
+ </span>
145
+ )}
146
+
147
+ {slider.values.map((one, index) => (
148
+ <Thumb
149
+ key={index}
150
+ percent={slider.percents[index] ?? 0}
151
+ value={one}
152
+ label={formatValue ? formatValue(one) : String(one)}
153
+ name={(index === 0 ? startLabel : endLabel) ?? `Valeur ${index + 1}`}
154
+ disabled={disabled}
155
+ onGrab={() => {
156
+ if (!disabled) setActiveThumb(index);
157
+ }}
158
+ />
159
+ ))}
160
+ </div>
161
+ );
162
+ }
163
+
164
+ function Thumb({
165
+ percent,
166
+ value,
167
+ label,
168
+ name,
169
+ disabled,
170
+ onGrab,
171
+ }: {
172
+ percent: number;
173
+ value: number;
174
+ label: string;
175
+ name: string;
176
+ disabled?: boolean | undefined;
177
+ onGrab: () => void;
178
+ }) {
179
+ return (
180
+ <Tooltip content={label} placement="top">
181
+ <span
182
+ role="slider"
183
+ aria-valuenow={value}
184
+ aria-label={name}
185
+ aria-disabled={disabled || undefined}
186
+ tabIndex={disabled ? -1 : 0}
187
+ className="sia-slider-thumb"
188
+ style={{ left: `${percent}%` }}
189
+ onMouseDown={onGrab}
190
+ />
191
+ </Tooltip>
192
+ );
193
+ }
@@ -0,0 +1,53 @@
1
+ .sia-slider {
2
+ position: relative;
3
+ width: 100%;
4
+ height: 32px;
5
+ user-select: none;
6
+ }
7
+
8
+ /* Track */
9
+ .sia-slider-track {
10
+ position: absolute;
11
+ top: 50%;
12
+ width: 100%;
13
+ height: 4px;
14
+ background: var(--sia-color-neutral-200);
15
+ border-radius: 999px;
16
+ transform: translateY(-50%);
17
+ cursor: pointer;
18
+ }
19
+
20
+ .sia-slider-range {
21
+ position: absolute;
22
+ height: 100%;
23
+ background: var(--sia-color-primary-500);
24
+ border-radius: inherit;
25
+ }
26
+
27
+ /* Thumb */
28
+ .sia-slider-thumb {
29
+ position: absolute;
30
+ top: 50%;
31
+ transform: translate(-50%, -50%);
32
+ width: 16px;
33
+ height: 16px;
34
+ border-radius: 50%;
35
+ background: var(--sia-color-primary-500);
36
+ border: 2px solid white;
37
+ cursor: grab;
38
+ }
39
+
40
+ .sia-slider-thumb:active {
41
+ cursor: grabbing;
42
+ }
43
+
44
+ .sia-slider-thumb-trigger {
45
+ position: absolute;
46
+ inset: 0;
47
+ }
48
+
49
+ /* Disabled */
50
+ .sia-slider.is-disabled {
51
+ opacity: 0.5;
52
+ pointer-events: none;
53
+ }