@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,103 @@
1
+ .sia-pagination { display: flex; align-items: center; gap: .3rem; flex-wrap: wrap; }
2
+
3
+ .sia-pagination button {
4
+ display: inline-flex;
5
+ align-items: center;
6
+ justify-content: center;
7
+ gap: .35rem;
8
+ min-width: 2.25rem;
9
+ min-height: 2.25rem;
10
+ padding: 0 .65rem;
11
+ border: 1px solid var(--sia-border);
12
+ border-radius: var(--sia-radius);
13
+ color: var(--sia-foreground);
14
+ background: var(--sia-background);
15
+ font: inherit;
16
+ font-variant-numeric: tabular-nums;
17
+ cursor: pointer;
18
+ transition:
19
+ background-color var(--sia-duration-fast) var(--sia-easing-standard),
20
+ border-color var(--sia-duration-fast) var(--sia-easing-standard),
21
+ color var(--sia-duration-fast) var(--sia-easing-standard),
22
+ transform var(--sia-duration-instant) var(--sia-easing-standard);
23
+ }
24
+ .sia-pagination button:hover:not(:disabled) { background: var(--sia-surface); }
25
+ .sia-pagination button:active:not(:disabled) { transform: scale(.94); }
26
+ .sia-pagination button:disabled { cursor: not-allowed; opacity: .5; }
27
+ .sia-pagination button:focus-visible {
28
+ outline: 3px solid color-mix(in srgb, var(--sia-primary) 30%, transparent);
29
+ outline-offset: 2px;
30
+ }
31
+
32
+ .sia-pagination button[aria-current="page"] {
33
+ color: var(--sia-primary-foreground);
34
+ border-color: var(--sia-primary);
35
+ background: var(--sia-primary);
36
+ font-weight: 600;
37
+ /* Un rebond court à l'arrivée sur la page : sur une barre de dix numéros,
38
+ le seul changement de couleur se remarque mal. */
39
+ animation: sia-pagination-current var(--sia-duration-base) var(--sia-easing-spring);
40
+ }
41
+ @keyframes sia-pagination-current {
42
+ 0% { transform: scale(.85); }
43
+ 60% { transform: scale(1.06); }
44
+ 100% { transform: scale(1); }
45
+ }
46
+
47
+ .sia-pagination__step svg { flex: none; font-size: .9em; }
48
+ .sia-pagination__gap { padding: 0 .25rem; color: var(--sia-muted); }
49
+
50
+ .sia-pagination--compact .sia-pagination__step { padding: 0 .5rem; }
51
+
52
+ @media (max-width: 480px) {
53
+ /* Sur un écran étroit, seuls les pas et la page courante tiennent. */
54
+ .sia-pagination__step span { display: none; }
55
+ }
56
+
57
+ /* ── Saut direct ───────────────────────────────────────────────────────── */
58
+
59
+ .sia-pagination__total {
60
+ margin-left: .5rem;
61
+ color: var(--sia-muted);
62
+ font-size: .8rem;
63
+ font-variant-numeric: tabular-nums;
64
+ white-space: nowrap;
65
+ }
66
+
67
+ .sia-pagination__jump {
68
+ display: flex;
69
+ align-items: center;
70
+ gap: .4rem;
71
+ margin-left: .5rem;
72
+ color: var(--sia-muted);
73
+ font-size: .8rem;
74
+ white-space: nowrap;
75
+ }
76
+ .sia-pagination__jump label { cursor: pointer; }
77
+
78
+ .sia-pagination__jump input {
79
+ width: 3.25rem;
80
+ min-height: 2.25rem;
81
+ padding: 0 .5rem;
82
+ border: 1px solid var(--sia-border);
83
+ border-radius: var(--sia-radius);
84
+ background: var(--sia-background);
85
+ color: var(--sia-foreground);
86
+ font: inherit;
87
+ font-variant-numeric: tabular-nums;
88
+ text-align: center;
89
+ transition: border-color var(--sia-duration-fast) var(--sia-easing-standard);
90
+ }
91
+ .sia-pagination__jump input:focus-visible {
92
+ border-color: var(--sia-primary);
93
+ outline: 3px solid color-mix(in srgb, var(--sia-primary) 30%, transparent);
94
+ outline-offset: 1px;
95
+ }
96
+
97
+ .sia-pagination__jump-total { font-variant-numeric: tabular-nums; }
98
+
99
+ @media (max-width: 480px) {
100
+ /* Le saut direct vaut mieux que la liste des numéros sur un écran étroit :
101
+ il passe à la ligne entier plutôt que d'être tronqué. */
102
+ .sia-pagination__jump { width: 100%; margin-left: 0; justify-content: flex-end; }
103
+ }
@@ -0,0 +1,125 @@
1
+ import { forwardRef, useMemo, useState } from "react";
2
+ import { cn } from "@sia-ui/utils";
3
+ import { useSiaLocale } from "@sia-ui/headless";
4
+ import { Input, type InputProps } from "../Input";
5
+ import { EyeIcon, EyeOffIcon } from "../Icons";
6
+ import "./styles.css";
7
+
8
+ export interface PasswordStrength {
9
+ /** De 0 à 4. */
10
+ score: number;
11
+ label: string;
12
+ }
13
+
14
+ export interface PasswordInputProps extends Omit<InputProps, "type" | "right"> {
15
+ /** Affiche une jauge sous le champ. */
16
+ showStrength?: boolean;
17
+ /** Retire le bouton d'affichage — pour un champ de confirmation. */
18
+ hideToggle?: boolean;
19
+ /**
20
+ * Remplace l'évaluation par défaut. Un projet qui utilise `zxcvbn` branche
21
+ * son résultat ici; le composant ne dépend de rien.
22
+ */
23
+ evaluate?: (value: string) => PasswordStrength;
24
+ }
25
+
26
+ const LABELS = ["Très faible", "Faible", "Moyen", "Bon", "Excellent"];
27
+
28
+ /**
29
+ * L'évaluation par défaut : longueur et variété.
30
+ *
31
+ * Elle ne prétend pas mesurer une entropie réelle — c'est le travail de
32
+ * `zxcvbn`, qui pèse 800 ko. Elle décourage les mots de passe visiblement
33
+ * courts, ce qui est déjà l'essentiel du gain.
34
+ */
35
+ function defaultEvaluate(value: string): PasswordStrength {
36
+ if (!value) return { score: 0, label: "" };
37
+
38
+ let score = 0;
39
+ if (value.length >= 8) score += 1;
40
+ if (value.length >= 12) score += 1;
41
+ if (/[a-z]/.test(value) && /[A-Z]/.test(value)) score += 1;
42
+ if (/\d/.test(value) && /[^\w\s]/.test(value)) score += 1;
43
+
44
+ return { score, label: LABELS[score] ?? "" };
45
+ }
46
+
47
+ /**
48
+ * Un champ de mot de passe.
49
+ *
50
+ * Le bouton d'affichage n'est pas un confort : sur mobile, taper une phrase
51
+ * de passe à l'aveugle est la première raison pour laquelle les gens en
52
+ * choisissent une courte.
53
+ */
54
+ export const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>(
55
+ (
56
+ {
57
+ showStrength = false,
58
+ hideToggle = false,
59
+ evaluate = defaultEvaluate,
60
+ className,
61
+ value,
62
+ onChange,
63
+ ...props
64
+ },
65
+ ref,
66
+ ) => {
67
+ const locale = useSiaLocale();
68
+ const [visible, setVisible] = useState(false);
69
+ const [internal, setInternal] = useState("");
70
+ const current = value !== undefined ? String(value) : internal;
71
+
72
+ const strength = useMemo(
73
+ () => (showStrength ? evaluate(current) : null),
74
+ [current, evaluate, showStrength],
75
+ );
76
+
77
+ return (
78
+ <div className={cn("sia-password", className)}>
79
+ <Input
80
+ {...props}
81
+ ref={ref}
82
+ type={visible ? "text" : "password"}
83
+ {...(value !== undefined ? { value } : {})}
84
+ onChange={(event) => {
85
+ if (value === undefined) setInternal(event.target.value);
86
+ onChange?.(event);
87
+ }}
88
+ right={
89
+ hideToggle ? undefined : (
90
+ <button
91
+ type="button"
92
+ className="sia-password__toggle"
93
+ // Hors du parcours de tabulation : entre le champ et le
94
+ // bouton d'envoi, personne n'attend une étape de plus.
95
+ tabIndex={-1}
96
+ aria-label={
97
+ visible ? locale.hidePassword : locale.showPassword
98
+ }
99
+ aria-pressed={visible}
100
+ onClick={() => setVisible(!visible)}
101
+ >
102
+ {visible ? <EyeOffIcon /> : <EyeIcon />}
103
+ </button>
104
+ )
105
+ }
106
+ />
107
+
108
+ {strength && current && (
109
+ <div className="sia-password__strength" data-score={strength.score}>
110
+ <div className="sia-password__bars" aria-hidden="true">
111
+ {[0, 1, 2, 3].map((index) => (
112
+ <span key={index} data-filled={index < strength.score || undefined} />
113
+ ))}
114
+ </div>
115
+ <span className="sia-password__label" role="status">
116
+ {strength.label}
117
+ </span>
118
+ </div>
119
+ )}
120
+ </div>
121
+ );
122
+ },
123
+ );
124
+
125
+ PasswordInput.displayName = "PasswordInput";
@@ -0,0 +1,58 @@
1
+ .sia-password { display: grid; gap: .4rem; }
2
+
3
+ .sia-password__toggle {
4
+ display: grid;
5
+ place-items: center;
6
+ padding: .2rem;
7
+ border: 0;
8
+ background: transparent;
9
+ color: var(--sia-muted);
10
+ cursor: pointer;
11
+ transition:
12
+ color var(--sia-duration-fast) var(--sia-easing-standard),
13
+ transform var(--sia-duration-base) var(--sia-easing-spring);
14
+ }
15
+ .sia-password__toggle:hover { color: var(--sia-foreground); }
16
+ .sia-password__toggle[aria-pressed="true"] { color: var(--sia-primary); }
17
+ /* L'œil pivote légèrement au basculement : sans cela, le changement d'icône
18
+ passe inaperçu et on clique deux fois. */
19
+ .sia-password__toggle:active { transform: scale(.85); }
20
+
21
+ .sia-password__strength {
22
+ --sia-strength-color: var(--sia-danger);
23
+ display: flex;
24
+ align-items: center;
25
+ gap: .6rem;
26
+ animation: sia-fade-in var(--sia-duration-base) var(--sia-easing-standard);
27
+ }
28
+ .sia-password__strength[data-score="2"] { --sia-strength-color: var(--sia-warning); }
29
+ .sia-password__strength[data-score="3"] { --sia-strength-color: var(--sia-info); }
30
+ .sia-password__strength[data-score="4"] { --sia-strength-color: var(--sia-success); }
31
+
32
+ .sia-password__bars { display: flex; flex: 1; gap: 3px; }
33
+ .sia-password__bars span {
34
+ flex: 1;
35
+ height: 3px;
36
+ border-radius: 999px;
37
+ background: var(--sia-border);
38
+ transform-origin: left;
39
+ transition:
40
+ background-color var(--sia-duration-base) var(--sia-easing-standard);
41
+ }
42
+ .sia-password__bars span[data-filled] {
43
+ background: var(--sia-strength-color);
44
+ animation: sia-password-bar var(--sia-duration-base) var(--sia-easing-standard);
45
+ }
46
+ @keyframes sia-password-bar {
47
+ from { transform: scaleX(0); }
48
+ to { transform: scaleX(1); }
49
+ }
50
+
51
+ .sia-password__label {
52
+ flex: none;
53
+ min-width: 5rem;
54
+ color: var(--sia-strength-color);
55
+ font-size: .75rem;
56
+ font-weight: 600;
57
+ text-align: right;
58
+ }
@@ -0,0 +1,3 @@
1
+ import { type ReactNode } from "react";
2
+ export interface PermissionGateProps { permissions: string[]; require: string | string[]; mode?: "all" | "any"; fallback?: ReactNode; children: ReactNode; }
3
+ export function PermissionGate({ permissions, require, mode = "all", fallback = null, children }: PermissionGateProps) { const expected = Array.isArray(require) ? require : [require]; const allowed = mode === "all" ? expected.every((item) => permissions.includes(item)) : expected.some((item) => permissions.includes(item)); return <>{allowed ? children : fallback}</>; }
@@ -0,0 +1,189 @@
1
+ import { forwardRef, useMemo, useState } from "react";
2
+ import { cn } from "@sia-ui/utils";
3
+ import { useSiaLocale } from "@sia-ui/headless";
4
+ import { Input, type InputProps } from "../Input";
5
+ import "./styles.css";
6
+
7
+ export interface PhoneCountry {
8
+ /** Code ISO 3166-1 alpha-2. */
9
+ code: string;
10
+ /** Indicatif, avec le `+`. */
11
+ dial: string;
12
+ label: string;
13
+ flag: string;
14
+ /** Nombre de chiffres attendus après l'indicatif. */
15
+ digits?: number;
16
+ /** Découpage visuel du numéro national : `[1, 2, 2, 2, 2]` → `6 12 34 56 78`. */
17
+ groups?: number[];
18
+ }
19
+
20
+ /**
21
+ * Les pays servis par défaut.
22
+ *
23
+ * Volontairement court et centré sur la zone franc : une liste mondiale pèse
24
+ * 40 ko pour être fausse six mois plus tard. La prop `countries` la remplace
25
+ * entièrement quand un projet en a besoin d'une autre.
26
+ */
27
+ export const DEFAULT_COUNTRIES: PhoneCountry[] = [
28
+ { code: "CM", dial: "+237", label: "Cameroun", flag: "🇨🇲", digits: 9, groups: [1, 2, 2, 2, 2] },
29
+ { code: "CI", dial: "+225", label: "Côte d'Ivoire", flag: "🇨🇮", digits: 10, groups: [2, 2, 2, 2, 2] },
30
+ { code: "SN", dial: "+221", label: "Sénégal", flag: "🇸🇳", digits: 9, groups: [2, 3, 2, 2] },
31
+ { code: "GA", dial: "+241", label: "Gabon", flag: "🇬🇦", digits: 8, groups: [2, 2, 2, 2] },
32
+ { code: "CG", dial: "+242", label: "Congo", flag: "🇨🇬", digits: 9, groups: [2, 3, 2, 2] },
33
+ { code: "CD", dial: "+243", label: "RD Congo", flag: "🇨🇩", digits: 9, groups: [3, 3, 3] },
34
+ { code: "BF", dial: "+226", label: "Burkina Faso", flag: "🇧🇫", digits: 8, groups: [2, 2, 2, 2] },
35
+ { code: "ML", dial: "+223", label: "Mali", flag: "🇲🇱", digits: 8, groups: [2, 2, 2, 2] },
36
+ { code: "BJ", dial: "+229", label: "Bénin", flag: "🇧🇯", digits: 10, groups: [2, 2, 2, 2, 2] },
37
+ { code: "TG", dial: "+228", label: "Togo", flag: "🇹🇬", digits: 8, groups: [2, 2, 2, 2] },
38
+ { code: "TD", dial: "+235", label: "Tchad", flag: "🇹🇩", digits: 8, groups: [2, 2, 2, 2] },
39
+ { code: "FR", dial: "+33", label: "France", flag: "🇫🇷", digits: 9, groups: [1, 2, 2, 2, 2] },
40
+ { code: "BE", dial: "+32", label: "Belgique", flag: "🇧🇪", digits: 9, groups: [3, 2, 2, 2] },
41
+ { code: "CA", dial: "+1", label: "Canada", flag: "🇨🇦", digits: 10, groups: [3, 3, 4] },
42
+ ];
43
+
44
+ export interface PhoneInputProps
45
+ extends Omit<InputProps, "value" | "defaultValue" | "onChange" | "type" | "left"> {
46
+ /** Le numéro complet, indicatif compris : `+237612345678`. */
47
+ value?: string;
48
+ defaultValue?: string;
49
+ /** Reçoit toujours la forme E.164, sans espaces. */
50
+ onValueChange?: (value: string) => void;
51
+ countries?: PhoneCountry[];
52
+ /** Pays présélectionné, par code ISO. */
53
+ defaultCountry?: string;
54
+ /** Verrouille le pays et masque le sélecteur. */
55
+ lockCountry?: boolean;
56
+ }
57
+
58
+ function digitsOnly(value: string) {
59
+ return value.replace(/\D/g, "");
60
+ }
61
+
62
+ /** Découpe le numéro national selon le format du pays. */
63
+ function group(value: string, groups: number[] | undefined) {
64
+ if (!groups || groups.length === 0) return value;
65
+ const parts: string[] = [];
66
+ let rest = value;
67
+ for (const size of groups) {
68
+ if (!rest) break;
69
+ parts.push(rest.slice(0, size));
70
+ rest = rest.slice(size);
71
+ }
72
+ if (rest) parts.push(rest);
73
+ return parts.join(" ");
74
+ }
75
+
76
+ /** Retrouve le pays d'un numéro E.164 — le plus long indicatif d'abord. */
77
+ function detect(value: string, countries: PhoneCountry[]) {
78
+ return [...countries]
79
+ .sort((a, b) => b.dial.length - a.dial.length)
80
+ .find((country) => value.startsWith(country.dial));
81
+ }
82
+
83
+ /**
84
+ * Un numéro de téléphone international.
85
+ *
86
+ * L'indicatif et le numéro national sont deux champs distincts, parce qu'une
87
+ * seule zone de saisie oblige à taper le `+237` à chaque fois — ce que
88
+ * personne ne fait, d'où des numéros enregistrés sans indicatif.
89
+ *
90
+ * Aucune validation réelle n'est tentée : `libphonenumber-js` fait cela bien,
91
+ * pèse 145 ko, et se branche par-dessus si un projet en a besoin. Ici, le
92
+ * découpage visuel et la forme E.164 en sortie.
93
+ */
94
+ export const PhoneInput = forwardRef<HTMLInputElement, PhoneInputProps>(
95
+ (
96
+ {
97
+ value: valueProp,
98
+ defaultValue = "",
99
+ onValueChange,
100
+ countries = DEFAULT_COUNTRIES,
101
+ defaultCountry,
102
+ lockCountry = false,
103
+ disabled,
104
+ className,
105
+ ...props
106
+ },
107
+ ref,
108
+ ) => {
109
+ const locale = useSiaLocale();
110
+ const controlled = valueProp !== undefined;
111
+ const [internal, setInternal] = useState(defaultValue);
112
+ const value = controlled ? valueProp : internal;
113
+
114
+ const initial =
115
+ countries.find((entry) => entry.code === defaultCountry) ??
116
+ countries[0]!;
117
+ const [picked, setPicked] = useState(initial.code);
118
+
119
+ const country = useMemo(
120
+ () =>
121
+ detect(value, countries) ??
122
+ countries.find((entry) => entry.code === picked) ??
123
+ initial,
124
+ [countries, initial, picked, value],
125
+ );
126
+
127
+ const national = value.startsWith(country.dial)
128
+ ? value.slice(country.dial.length)
129
+ : digitsOnly(value);
130
+
131
+ const commit = (next: string) => {
132
+ if (!controlled) setInternal(next);
133
+ onValueChange?.(next);
134
+ };
135
+
136
+ return (
137
+ <Input
138
+ {...props}
139
+ ref={ref}
140
+ type="tel"
141
+ inputMode="tel"
142
+ autoComplete="tel-national"
143
+ className={cn("sia-phone__field", className)}
144
+ disabled={disabled}
145
+ value={group(national, country.groups)}
146
+ {...(country.digits ? { maxLength: country.digits + 8 } : {})}
147
+ onChange={(event) => {
148
+ let next = digitsOnly(event.target.value);
149
+ if (country.digits) next = next.slice(0, country.digits);
150
+ commit(next ? `${country.dial}${next}` : "");
151
+ }}
152
+ left={
153
+ <span className="sia-phone__country">
154
+ <span className="sia-phone__flag" aria-hidden="true">
155
+ {country.flag}
156
+ </span>
157
+ <span className="sia-phone__dial">{country.dial}</span>
158
+ {!lockCountry && (
159
+ <select
160
+ className="sia-phone__select"
161
+ aria-label={locale.countryCode}
162
+ value={country.code}
163
+ disabled={disabled}
164
+ onChange={(event) => {
165
+ const next = countries.find(
166
+ (entry) => entry.code === event.target.value,
167
+ );
168
+ if (!next) return;
169
+ setPicked(next.code);
170
+ // Le numéro national suit le changement d'indicatif : il
171
+ // est rare mais courant de corriger le pays après coup.
172
+ commit(national ? `${next.dial}${national}` : "");
173
+ }}
174
+ >
175
+ {countries.map((entry) => (
176
+ <option key={entry.code} value={entry.code}>
177
+ {entry.flag} {entry.label} ({entry.dial})
178
+ </option>
179
+ ))}
180
+ </select>
181
+ )}
182
+ </span>
183
+ }
184
+ />
185
+ );
186
+ },
187
+ );
188
+
189
+ PhoneInput.displayName = "PhoneInput";
@@ -0,0 +1,35 @@
1
+ .sia-phone__field { font-variant-numeric: tabular-nums; letter-spacing: .02em; }
2
+
3
+ .sia-phone__country {
4
+ position: relative;
5
+ display: inline-flex;
6
+ align-items: center;
7
+ gap: .3rem;
8
+ padding-right: .6rem;
9
+ border-right: 1px solid var(--sia-border);
10
+ color: var(--sia-foreground);
11
+ font-size: .85rem;
12
+ white-space: nowrap;
13
+ transition: background-color var(--sia-duration-fast) var(--sia-easing-standard);
14
+ border-radius: var(--sia-radius) 0 0 var(--sia-radius);
15
+ }
16
+ .sia-phone__country:hover { background: var(--sia-surface); }
17
+
18
+ .sia-phone__flag { font-size: 1.05rem; line-height: 1; }
19
+ .sia-phone__dial { color: var(--sia-muted); font-variant-numeric: tabular-nums; }
20
+
21
+ /* Le `select` natif recouvre l'affichage : il ouvre la liste du système, qui
22
+ est la seule à fonctionner correctement sur mobile — un menu réimplémenté
23
+ se retrouve sous le clavier virtuel. */
24
+ .sia-phone__select {
25
+ position: absolute;
26
+ inset: 0;
27
+ width: 100%;
28
+ height: 100%;
29
+ border: 0;
30
+ opacity: 0;
31
+ cursor: pointer;
32
+ appearance: none;
33
+ }
34
+ .sia-phone__select:disabled { cursor: not-allowed; }
35
+ .sia-phone__select:focus-visible + * { outline: 2px solid var(--sia-primary); }
@@ -0,0 +1,12 @@
1
+ import { cloneElement, isValidElement, useRef, useState, type ReactElement, type ReactNode } from "react";
2
+ import { cn, toneClass } from "@sia-ui/utils";
3
+ import type { ComponentTone } from "@sia-ui/tokens";
4
+ import { Overlay, type OverlayPlacement } from "../Overlay";
5
+ import "./styles.css";
6
+
7
+ export interface PopoverProps { content: ReactNode; title?: ReactNode; children: ReactElement<{ onClick?: () => void; "aria-expanded"?: boolean }>; open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean) => void; placement?: OverlayPlacement; trigger?: "click" | "hover"; variant?: "solid" | "outline"; tone?: ComponentTone; disabled?: boolean; className?: string; }
8
+ export function Popover({ content, title, children, open: openProp, defaultOpen = false, onOpenChange, placement = "bottom-start", trigger = "click", variant = "outline", tone = "neutral", disabled = false, className }: PopoverProps) {
9
+ const anchorRef = useRef<HTMLElement>(null); const [internalOpen, setInternalOpen] = useState(defaultOpen); const open = openProp ?? internalOpen; const setOpen = (next: boolean) => { if (disabled) return; if (openProp === undefined) setInternalOpen(next); onOpenChange?.(next); };
10
+ const child = isValidElement(children) ? cloneElement(children, { ref: anchorRef, "aria-expanded": open, ...(trigger === "click" ? { onClick: () => { children.props.onClick?.(); setOpen(!open); } } : {}) } as never) : children;
11
+ return <span className="sia-popover-anchor" onMouseEnter={trigger === "hover" ? () => setOpen(true) : undefined} onMouseLeave={trigger === "hover" ? () => setOpen(false) : undefined}>{child}<Overlay open={open} anchorRef={anchorRef} onOpenChange={setOpen} placement={placement} className={cn("sia-popover", `sia-popover--${variant}`, toneClass("sia-tone", tone), className)} role="dialog">{title && <strong className="sia-popover__title">{title}</strong>}<div className="sia-popover__content">{content}</div></Overlay></span>;
12
+ }
@@ -0,0 +1,12 @@
1
+ .sia-popover-anchor { display:inline-flex; }
2
+ .sia-popover { --sia-accent:var(--sia-foreground); display:grid; gap:.4rem; max-width:min(22rem,calc(100vw - 1rem)); padding:.85rem; border-radius:var(--sia-radius); box-shadow:0 16px 42px rgb(15 23 42 / .18); }
3
+ .sia-popover--outline { color:var(--sia-foreground); border:1px solid var(--sia-border); background:var(--sia-background); }
4
+ .sia-popover--solid { color:var(--sia-accent-foreground); background:var(--sia-accent); }
5
+ .sia-popover__title { font-size:.9rem; }
6
+ .sia-popover__content { color:inherit; font-size:.85rem; line-height:1.5; }
7
+
8
+ .sia-popover__content,
9
+ .sia-popover {
10
+ animation: sia-scale-in var(--sia-duration-base) var(--sia-easing-entrance);
11
+ box-shadow: var(--sia-elevation-overlay);
12
+ }