@sia-ui/cli 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/README.md +34 -0
- package/dist/registry/package/components/Calendar/index.tsx +4 -4
- package/dist/registry/package/components/CrudPage/dialogs.tsx +20 -20
- package/dist/registry/package/components/CrudPage/index.tsx +34 -4
- package/dist/registry/package/components/DataTable/index.tsx +5 -1
- package/dist/registry/package/components/DataTable/types.ts +8 -0
- package/dist/registry/package/components/Drawer/index.tsx +9 -3
- package/dist/registry/package/components/Form/index.tsx +69 -3
- package/dist/registry/package/components/Pagination/index.tsx +168 -61
- package/dist/registry/package/components/Pagination/styles.css +133 -38
- package/dist/registry/package/components/PhoneInput/index.tsx +46 -29
- package/dist/registry/package/components/PhoneInput/styles.css +7 -21
- package/dist/registry/package/components/Select/index.tsx +34 -8
- package/dist/registry/package/components/Select/styles.css +18 -0
- package/dist/registry/package/components/Timer/index.tsx +1 -1
- package/dist/registry/package/lib/form-adapter-rhf.tsx +23 -10
- package/dist/registry/registry.json +19 -12
- package/dist/registry/styles/sia-ui.css +6 -0
- package/package.json +5 -5
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
type KeyboardEvent,
|
|
11
11
|
type ReactNode,
|
|
12
12
|
} from "react";
|
|
13
|
-
import { cn } from "@sia-ui/utils";
|
|
13
|
+
import { cn, looseIncludes } from "@sia-ui/utils";
|
|
14
14
|
import { Overlay, type OverlayPlacement } from "../Overlay";
|
|
15
15
|
import { Spinner } from "../Spinner";
|
|
16
16
|
import { ChevronDownIcon, SearchIcon, CheckIcon, XIcon } from "../Icons";
|
|
@@ -46,6 +46,19 @@ export interface SelectProps {
|
|
|
46
46
|
invalid?: boolean;
|
|
47
47
|
required?: boolean;
|
|
48
48
|
placement?: OverlayPlacement;
|
|
49
|
+
/**
|
|
50
|
+
* Ce que montre le déclencheur. Par défaut, le libellé de l'option.
|
|
51
|
+
*
|
|
52
|
+
* La liste garde ses libellés complets — « 🇨🇲 Cameroun (+237) » — pendant
|
|
53
|
+
* que le champ fermé n'affiche que ce qui tient : « 🇨🇲 +237 ».
|
|
54
|
+
*/
|
|
55
|
+
renderValue?: (option: SelectOption | undefined) => ReactNode;
|
|
56
|
+
/**
|
|
57
|
+
* `embedded` retire bordure, fond et largeur pleine du déclencheur : le
|
|
58
|
+
* `Select` se loge alors dans un autre contrôle — l'indicatif d'un
|
|
59
|
+
* `PhoneInput`, la taille de page d'une `Pagination`.
|
|
60
|
+
*/
|
|
61
|
+
variant?: "default" | "embedded";
|
|
49
62
|
className?: string;
|
|
50
63
|
"aria-label"?: string;
|
|
51
64
|
"aria-describedby"?: string;
|
|
@@ -71,8 +84,7 @@ function optionsFromChildren(children: ReactNode): SelectOption[] {
|
|
|
71
84
|
function optionText(option: SelectOption) {
|
|
72
85
|
return [option.label, option.description, ...(option.keywords ?? [])]
|
|
73
86
|
.map((value) => (typeof value === "string" || typeof value === "number" ? String(value) : ""))
|
|
74
|
-
.join(" ")
|
|
75
|
-
.toLocaleLowerCase();
|
|
87
|
+
.join(" ");
|
|
76
88
|
}
|
|
77
89
|
|
|
78
90
|
/**
|
|
@@ -108,6 +120,8 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(function Select
|
|
|
108
120
|
invalid = false,
|
|
109
121
|
required = false,
|
|
110
122
|
placement = "bottom-start",
|
|
123
|
+
renderValue,
|
|
124
|
+
variant = "default",
|
|
111
125
|
className,
|
|
112
126
|
"aria-invalid": ariaInvalid,
|
|
113
127
|
"aria-required": ariaRequired,
|
|
@@ -136,9 +150,10 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(function Select
|
|
|
136
150
|
const sourceOptions = fetcher ? remoteOptions : staticOptions;
|
|
137
151
|
const visibleOptions = useMemo(() => {
|
|
138
152
|
if (fetcher) return sourceOptions;
|
|
139
|
-
|
|
153
|
+
// Accents et casse ignorés : « sen » trouve « Sénégal ».
|
|
154
|
+
const needle = query.trim();
|
|
140
155
|
return needle
|
|
141
|
-
? sourceOptions.filter((option) => optionText(option)
|
|
156
|
+
? sourceOptions.filter((option) => looseIncludes(optionText(option), needle))
|
|
142
157
|
: sourceOptions;
|
|
143
158
|
}, [fetcher, query, sourceOptions]);
|
|
144
159
|
const selectedOption = [...staticOptions, ...remoteOptions].find(
|
|
@@ -168,7 +183,11 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(function Select
|
|
|
168
183
|
|
|
169
184
|
useEffect(() => {
|
|
170
185
|
if (!open) return;
|
|
171
|
-
|
|
186
|
+
// La valeur en vigueur si elle est dans la liste, la première option
|
|
187
|
+
// sinon : après une recherche qui l'écarte, Entrée doit choisir le premier
|
|
188
|
+
// résultat plutôt que ne rien faire.
|
|
189
|
+
const selected = visibleOptions.findIndex((option) => option.value === currentValue);
|
|
190
|
+
setActiveIndex(selected >= 0 ? selected : visibleOptions.findIndex((option) => !option.disabled));
|
|
172
191
|
if (searchable || fetcher) window.setTimeout(() => searchRef.current?.focus(), 0);
|
|
173
192
|
}, [currentValue, fetcher, open, searchable, visibleOptions]);
|
|
174
193
|
|
|
@@ -234,7 +253,14 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(function Select
|
|
|
234
253
|
const activeOption = activeIndex >= 0 ? visibleOptions[activeIndex] : undefined;
|
|
235
254
|
|
|
236
255
|
return (
|
|
237
|
-
<div
|
|
256
|
+
<div
|
|
257
|
+
className={cn(
|
|
258
|
+
"sia-select",
|
|
259
|
+
variant === "embedded" && "sia-select--embedded",
|
|
260
|
+
disabled && "sia-select--disabled",
|
|
261
|
+
className,
|
|
262
|
+
)}
|
|
263
|
+
>
|
|
238
264
|
{name && <input type="hidden" name={name} value={currentValue} />}
|
|
239
265
|
<button
|
|
240
266
|
ref={setTriggerRef}
|
|
@@ -253,7 +279,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(function Select
|
|
|
253
279
|
onKeyDown={handleKeyDown}
|
|
254
280
|
{...ariaProps}
|
|
255
281
|
>
|
|
256
|
-
<span className={cn("sia-select__value", !selectedOption && "sia-select__placeholder")}>{selectedOption?.label ?? placeholder}</span>
|
|
282
|
+
<span className={cn("sia-select__value", !selectedOption && "sia-select__placeholder")}>{renderValue ? renderValue(selectedOption) : (selectedOption?.label ?? placeholder)}</span>
|
|
257
283
|
<span className="sia-select__actions">
|
|
258
284
|
{clearable && currentValue && !disabled && (
|
|
259
285
|
<span
|
|
@@ -56,3 +56,21 @@
|
|
|
56
56
|
transform-origin: top center;
|
|
57
57
|
box-shadow: var(--sia-elevation-overlay);
|
|
58
58
|
}
|
|
59
|
+
|
|
60
|
+
/* ── Logé dans un autre contrôle ───────────────────────────────────────────
|
|
61
|
+
Le déclencheur prend la largeur de son contenu et laisse bordure et fond au
|
|
62
|
+
contrôle hôte. Le panneau, lui, reste celui du Select : `matchAnchorWidth`
|
|
63
|
+
n'impose qu'une largeur minimale, il s'élargit à ses options. */
|
|
64
|
+
.sia-select--embedded { width: auto; }
|
|
65
|
+
.sia-select--embedded .sia-select__trigger {
|
|
66
|
+
width: auto;
|
|
67
|
+
min-height: 0;
|
|
68
|
+
gap: .35rem;
|
|
69
|
+
padding: 0 .35rem;
|
|
70
|
+
border-color: transparent;
|
|
71
|
+
background: transparent;
|
|
72
|
+
}
|
|
73
|
+
.sia-select--embedded .sia-select__trigger:hover:not(:disabled) {
|
|
74
|
+
border-color: transparent;
|
|
75
|
+
background: var(--sia-surface);
|
|
76
|
+
}
|
|
@@ -13,4 +13,4 @@ export function formatDuration(milliseconds: number, showMilliseconds = false) {
|
|
|
13
13
|
* dixième de seconde redessine quarante fois par seconde pour un chiffre
|
|
14
14
|
* que personne ne lit.
|
|
15
15
|
*/
|
|
16
|
-
export function Timer({ duration = 60000, initialValue = 0, direction = "down", running = true, interval = 250, onChange, onFinish, format = formatDuration, className }: TimerProps) { const initial = direction === "down" ? duration : initialValue; const [value, setValue] = useState(initial); const finishCalled = useRef(false); useEffect(() => { setValue(direction === "down" ? duration : initialValue); finishCalled.current = false; }, [direction, duration, initialValue]); useEffect(() => { if (!running) return; const startedAt = Date.now(); const base = value; const update = () => { const elapsed = Date.now() - startedAt; const next = direction === "down" ? Math.max(0, base - elapsed) : base + elapsed; setValue(next); onChange?.(next); if (direction === "down" && next === 0 && !finishCalled.current) { finishCalled.current = true; onFinish?.(); } }; update(); const id = window.setInterval(update, interval); return () => window.clearInterval(id); }, [running, direction, interval]); return <time className={cn("sia-timer", className)} aria-live="polite">{format(value)}</time>; }
|
|
16
|
+
export function Timer({ duration = 60000, initialValue = 0, direction = "down", running = true, interval = 250, onChange, onFinish, format = formatDuration, className }: TimerProps) { const initial = direction === "down" ? duration : initialValue; const [value, setValue] = useState(initial); const finishCalled = useRef(false); /* Lus au démarrage et à chaque tic, sans relancer l'intervalle : la valeur de reprise et des rappels souvent écrits en ligne. */ const latest = useRef({ value, onChange, onFinish }); latest.current = { value, onChange, onFinish }; useEffect(() => { setValue(direction === "down" ? duration : initialValue); finishCalled.current = false; }, [direction, duration, initialValue]); useEffect(() => { if (!running) return; const startedAt = Date.now(); const base = latest.current.value; const update = () => { const elapsed = Date.now() - startedAt; const next = direction === "down" ? Math.max(0, base - elapsed) : base + elapsed; setValue(next); latest.current.onChange?.(next); if (direction === "down" && next === 0 && !finishCalled.current) { finishCalled.current = true; latest.current.onFinish?.(); } }; update(); const id = window.setInterval(update, interval); return () => window.clearInterval(id); }, [running, direction, interval]); return <time className={cn("sia-timer", className)} aria-live="polite">{format(value)}</time>; }
|
|
@@ -22,11 +22,21 @@ import {
|
|
|
22
22
|
type UseFormProps,
|
|
23
23
|
type UseFormReturn,
|
|
24
24
|
} from "react-hook-form";
|
|
25
|
-
import
|
|
25
|
+
import {
|
|
26
|
+
hasFormErrors,
|
|
27
|
+
type FieldBindOptions,
|
|
28
|
+
type FieldBinding,
|
|
29
|
+
type FormAdapter,
|
|
30
|
+
type FormErrors,
|
|
31
|
+
type FormState,
|
|
32
|
+
type FormSubmitResult,
|
|
33
|
+
} from "@sia-ui/headless";
|
|
26
34
|
|
|
27
35
|
export interface RhfAdapterOptions<TValues extends FieldValues>
|
|
28
36
|
extends UseFormProps<TValues> {
|
|
29
|
-
onSubmit?: (
|
|
37
|
+
onSubmit?: (
|
|
38
|
+
values: TValues,
|
|
39
|
+
) => FormSubmitResult<TValues> | Promise<FormSubmitResult<TValues>>;
|
|
30
40
|
}
|
|
31
41
|
|
|
32
42
|
export interface RhfAdapter<TValues extends FieldValues>
|
|
@@ -94,6 +104,12 @@ export function useRhfFormAdapter<TValues extends FieldValues>(
|
|
|
94
104
|
[formOptions.mode, formState.errors, setValue, trigger, watch],
|
|
95
105
|
);
|
|
96
106
|
|
|
107
|
+
const poserErreurs = (next: FormErrors<TValues>) => {
|
|
108
|
+
for (const [key, message] of Object.entries(next)) {
|
|
109
|
+
if (message) setError(key as Path<TValues>, { message: String(message) });
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
97
113
|
const state: FormState<TValues> = {
|
|
98
114
|
values: values as TValues,
|
|
99
115
|
errors,
|
|
@@ -123,17 +139,14 @@ export function useRhfFormAdapter<TValues extends FieldValues>(
|
|
|
123
139
|
if (message) setError(name as Path<TValues>, { message });
|
|
124
140
|
else clearErrors(name as Path<TValues>);
|
|
125
141
|
},
|
|
126
|
-
// Ce que rend `HttpError.
|
|
127
|
-
// les erreurs du serveur se posent sous les champs, pas dans un bandeau.
|
|
128
|
-
setErrors:
|
|
129
|
-
for (const [key, message] of Object.entries(next)) {
|
|
130
|
-
if (message) setError(key as Path<TValues>, { message: String(message) });
|
|
131
|
-
}
|
|
132
|
-
},
|
|
142
|
+
// Ce que rend `HttpError.toFormErrors()` de `@sia-ui/api` entre directement
|
|
143
|
+
// ici : les erreurs du serveur se posent sous les champs, pas dans un bandeau.
|
|
144
|
+
setErrors: poserErreurs,
|
|
133
145
|
reset: (next) => reset(next as TValues | undefined),
|
|
134
146
|
submit: async () => {
|
|
135
147
|
await handleSubmit(async (formValues) => {
|
|
136
|
-
await onSubmit?.(formValues);
|
|
148
|
+
const result = await onSubmit?.(formValues);
|
|
149
|
+
if (hasFormErrors(result)) poserErreurs(result);
|
|
137
150
|
})();
|
|
138
151
|
},
|
|
139
152
|
validate: () => trigger(),
|
|
@@ -240,7 +240,7 @@
|
|
|
240
240
|
"name": "calendar",
|
|
241
241
|
"description": "Localized month and year calendar with range selection and custom cells.",
|
|
242
242
|
"type": "component",
|
|
243
|
-
"version": "0.1.
|
|
243
|
+
"version": "0.1.1",
|
|
244
244
|
"status": "stable",
|
|
245
245
|
"source": "package",
|
|
246
246
|
"files": [
|
|
@@ -383,7 +383,7 @@
|
|
|
383
383
|
"name": "crud-page",
|
|
384
384
|
"description": "List page for a declared resource: create, view, edit and delete open their own dialogs, each removable and permission-aware.",
|
|
385
385
|
"type": "pattern",
|
|
386
|
-
"version": "0.
|
|
386
|
+
"version": "0.3.0",
|
|
387
387
|
"status": "stable",
|
|
388
388
|
"source": "package",
|
|
389
389
|
"files": [
|
|
@@ -426,7 +426,7 @@
|
|
|
426
426
|
"name": "data-table",
|
|
427
427
|
"description": "Data table: row actions with permissions and confirmation, selection, card view, density and sorting.",
|
|
428
428
|
"type": "component",
|
|
429
|
-
"version": "0.
|
|
429
|
+
"version": "0.3.0",
|
|
430
430
|
"status": "stable",
|
|
431
431
|
"source": "package",
|
|
432
432
|
"files": [
|
|
@@ -566,17 +566,21 @@
|
|
|
566
566
|
"name": "drawer",
|
|
567
567
|
"description": "Accessible side panel with focus management and portal rendering.",
|
|
568
568
|
"type": "component",
|
|
569
|
-
"version": "0.1.
|
|
569
|
+
"version": "0.1.1",
|
|
570
570
|
"status": "stable",
|
|
571
571
|
"source": "package",
|
|
572
572
|
"files": [
|
|
573
573
|
"components/Drawer/index.tsx",
|
|
574
|
+
"components/Drawer/context.ts",
|
|
574
575
|
"components/Drawer/styles.css"
|
|
575
576
|
],
|
|
576
577
|
"dependencies": [
|
|
577
578
|
"@sia-ui/headless",
|
|
578
579
|
"@sia-ui/react",
|
|
579
580
|
"@sia-ui/utils"
|
|
581
|
+
],
|
|
582
|
+
"registryDependencies": [
|
|
583
|
+
"icons"
|
|
580
584
|
]
|
|
581
585
|
},
|
|
582
586
|
{
|
|
@@ -755,7 +759,7 @@
|
|
|
755
759
|
"name": "form",
|
|
756
760
|
"description": "Declarative form driven by a field list, wired to any FormAdapter.",
|
|
757
761
|
"type": "pattern",
|
|
758
|
-
"version": "0.
|
|
762
|
+
"version": "0.3.0",
|
|
759
763
|
"status": "stable",
|
|
760
764
|
"source": "package",
|
|
761
765
|
"files": [
|
|
@@ -767,6 +771,7 @@
|
|
|
767
771
|
"@sia-ui/utils"
|
|
768
772
|
],
|
|
769
773
|
"registryDependencies": [
|
|
774
|
+
"alert",
|
|
770
775
|
"button",
|
|
771
776
|
"field"
|
|
772
777
|
]
|
|
@@ -775,7 +780,7 @@
|
|
|
775
780
|
"name": "form-adapter-rhf",
|
|
776
781
|
"description": "react-hook-form adapter for the FormAdapter contract. Requires react-hook-form.",
|
|
777
782
|
"type": "primitive",
|
|
778
|
-
"version": "0.
|
|
783
|
+
"version": "0.3.0",
|
|
779
784
|
"status": "stable",
|
|
780
785
|
"source": "package",
|
|
781
786
|
"files": [
|
|
@@ -1090,7 +1095,7 @@
|
|
|
1090
1095
|
"name": "pagination",
|
|
1091
1096
|
"description": "Accessible controlled pagination with compact page ranges.",
|
|
1092
1097
|
"type": "component",
|
|
1093
|
-
"version": "0.
|
|
1098
|
+
"version": "0.3.0",
|
|
1094
1099
|
"status": "stable",
|
|
1095
1100
|
"source": "package",
|
|
1096
1101
|
"files": [
|
|
@@ -1102,7 +1107,8 @@
|
|
|
1102
1107
|
"@sia-ui/utils"
|
|
1103
1108
|
],
|
|
1104
1109
|
"registryDependencies": [
|
|
1105
|
-
"icons"
|
|
1110
|
+
"icons",
|
|
1111
|
+
"select"
|
|
1106
1112
|
]
|
|
1107
1113
|
},
|
|
1108
1114
|
{
|
|
@@ -1140,7 +1146,7 @@
|
|
|
1140
1146
|
"name": "phone-input",
|
|
1141
1147
|
"description": "International phone input with dial code and grouping.",
|
|
1142
1148
|
"type": "component",
|
|
1143
|
-
"version": "0.
|
|
1149
|
+
"version": "0.3.0",
|
|
1144
1150
|
"status": "stable",
|
|
1145
1151
|
"source": "package",
|
|
1146
1152
|
"files": [
|
|
@@ -1152,7 +1158,8 @@
|
|
|
1152
1158
|
"@sia-ui/utils"
|
|
1153
1159
|
],
|
|
1154
1160
|
"registryDependencies": [
|
|
1155
|
-
"input"
|
|
1161
|
+
"input",
|
|
1162
|
+
"select"
|
|
1156
1163
|
]
|
|
1157
1164
|
},
|
|
1158
1165
|
{
|
|
@@ -1382,7 +1389,7 @@
|
|
|
1382
1389
|
"name": "select",
|
|
1383
1390
|
"description": "Accessible searchable select with positioned overlay and async option loading.",
|
|
1384
1391
|
"type": "component",
|
|
1385
|
-
"version": "0.
|
|
1392
|
+
"version": "0.3.0",
|
|
1386
1393
|
"status": "stable",
|
|
1387
1394
|
"source": "package",
|
|
1388
1395
|
"files": [
|
|
@@ -1647,7 +1654,7 @@
|
|
|
1647
1654
|
"name": "timer",
|
|
1648
1655
|
"description": "Reliable count-up or count-down timer based on absolute time.",
|
|
1649
1656
|
"type": "component",
|
|
1650
|
-
"version": "0.1.
|
|
1657
|
+
"version": "0.1.1",
|
|
1651
1658
|
"status": "stable",
|
|
1652
1659
|
"source": "package",
|
|
1653
1660
|
"files": [
|
|
@@ -144,6 +144,12 @@
|
|
|
144
144
|
--sia-elevation-modal: 0 24px 60px rgb(15 23 42 / 0.22);
|
|
145
145
|
}
|
|
146
146
|
/* @end:theme */
|
|
147
|
+
/* Les composants sont écrits en `border-box` : `width:100%` plus un padding
|
|
148
|
+
ne doit pas déborder. Sans ce reset, un projet qui n'en pose pas lui-même
|
|
149
|
+
gagnait 32 px de défilement horizontal sous `AppShell`, et des champs plus
|
|
150
|
+
larges que leur colonne. Spécificité nulle : la moindre règle de l'hôte
|
|
151
|
+
l'emporte, et un composant qui veut `content-box` le déclare (Chart). */
|
|
152
|
+
:where(*), :where(*)::before, :where(*)::after { box-sizing:border-box; }
|
|
147
153
|
/* Scrollbars SIA UI globales : faibles en spécificité, donc surchargeables. */
|
|
148
154
|
:root {
|
|
149
155
|
--sia-scrollbar-size:.625rem;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sia-ui/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Install SIA UI components progressively into an application.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sia-ui",
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"react-dom": "^19.2.3",
|
|
40
40
|
"typescript": "^5.9.3",
|
|
41
41
|
"vitest": "^4.0.16",
|
|
42
|
-
"@sia-ui/headless": "0.
|
|
43
|
-
"@sia-ui/
|
|
44
|
-
"@sia-ui/tokens": "0.
|
|
45
|
-
"@sia-ui/
|
|
42
|
+
"@sia-ui/headless": "0.5.0",
|
|
43
|
+
"@sia-ui/react": "0.5.0",
|
|
44
|
+
"@sia-ui/tokens": "0.5.0",
|
|
45
|
+
"@sia-ui/utils": "0.5.0"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"build": "node scripts/build.mjs",
|