@hgdc/ui 0.1.1 → 0.2.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 (49) hide show
  1. package/README.md +43 -2
  2. package/dist/avatar.d.ts +11 -0
  3. package/dist/avatar.js +23 -0
  4. package/dist/badge.d.ts +9 -0
  5. package/dist/badge.js +21 -0
  6. package/dist/button.d.ts +19 -0
  7. package/dist/button.js +94 -0
  8. package/dist/cn.d.ts +10 -0
  9. package/dist/cn.js +5 -0
  10. package/dist/dialog.d.ts +14 -0
  11. package/dist/dialog.js +73 -0
  12. package/dist/drawer.d.ts +59 -0
  13. package/dist/drawer.js +77 -0
  14. package/dist/dropdown-menu.d.ts +36 -0
  15. package/dist/dropdown-menu.js +71 -0
  16. package/dist/empty.d.ts +12 -0
  17. package/dist/empty.js +33 -0
  18. package/dist/field.d.ts +34 -0
  19. package/dist/field.js +102 -0
  20. package/dist/floating.d.ts +40 -0
  21. package/dist/floating.js +40 -0
  22. package/dist/input-otp.d.ts +18 -0
  23. package/dist/input-otp.js +26 -0
  24. package/dist/input.d.ts +3 -0
  25. package/dist/input.js +36 -0
  26. package/dist/label.d.ts +3 -0
  27. package/dist/label.js +7 -0
  28. package/dist/segmented-control.d.ts +32 -0
  29. package/dist/segmented-control.js +25 -0
  30. package/dist/select.d.ts +21 -0
  31. package/dist/select.js +73 -0
  32. package/dist/separator.d.ts +3 -0
  33. package/dist/separator.js +7 -0
  34. package/dist/sonner.d.ts +3 -0
  35. package/dist/sonner.js +33 -0
  36. package/dist/spinner.d.ts +9 -0
  37. package/dist/spinner.js +11 -0
  38. package/dist/switch.d.ts +17 -0
  39. package/dist/switch.js +25 -0
  40. package/dist/textarea.d.ts +9 -0
  41. package/dist/textarea.js +16 -0
  42. package/dist/theme.d.ts +21 -0
  43. package/dist/theme.js +68 -0
  44. package/dist/use-media-query.d.ts +23 -0
  45. package/dist/use-media-query.js +51 -0
  46. package/package.json +27 -9
  47. package/styles.css +374 -0
  48. package/dist/index.d.ts +0 -1
  49. package/dist/index.js +0 -2
package/dist/empty.js ADDED
@@ -0,0 +1,33 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cva } from "class-variance-authority";
3
+ import { cn } from "./cn.js";
4
+ function Empty({ className, ...props }) {
5
+ return (_jsx("div", { "data-slot": "empty", className: cn("flex w-full min-w-0 flex-1 flex-col items-center justify-center gap-4 rounded-2xl p-12 text-center text-balance", className), ...props }));
6
+ }
7
+ function EmptyHeader({ className, ...props }) {
8
+ return (_jsx("div", { "data-slot": "empty-header", className: cn("flex max-w-sm flex-col items-center gap-2", className), ...props }));
9
+ }
10
+ const emptyMediaVariants = cva("mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0", {
11
+ variants: {
12
+ variant: {
13
+ default: "bg-transparent",
14
+ icon: "flex size-14 shrink-0 items-center justify-center rounded-2xl bg-muted text-foreground [&_svg:not([class*='size-'])]:size-7",
15
+ },
16
+ },
17
+ defaultVariants: {
18
+ variant: "default",
19
+ },
20
+ });
21
+ function EmptyMedia({ className, variant = "default", ...props }) {
22
+ return (_jsx("div", { "data-slot": "empty-icon", "data-variant": variant, className: cn(emptyMediaVariants({ variant, className })), ...props }));
23
+ }
24
+ function EmptyTitle({ className, ...props }) {
25
+ return (_jsx("div", { "data-slot": "empty-title", className: cn("font-heading text-lg font-medium tracking-tight", className), ...props }));
26
+ }
27
+ function EmptyDescription({ className, ...props }) {
28
+ return (_jsx("div", { "data-slot": "empty-description", className: cn("text-sm/relaxed text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary", className), ...props }));
29
+ }
30
+ function EmptyContent({ className, ...props }) {
31
+ return (_jsx("div", { "data-slot": "empty-content", className: cn("flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance", className), ...props }));
32
+ }
33
+ export { Empty, EmptyHeader, EmptyTitle, EmptyDescription, EmptyContent, EmptyMedia };
@@ -0,0 +1,34 @@
1
+ import { type VariantProps } from "class-variance-authority";
2
+ import { Label } from "./label.tsx";
3
+ import { type Variants } from "./cn.ts";
4
+ /** Identifiant du champ courant, s'il y en a un. */
5
+ declare function useFieldId(): string | undefined;
6
+ /**
7
+ * Identifiants de la description et de l'erreur du champ courant, pour
8
+ * `aria-describedby`. `FieldDescription` et `FieldError` les portent en `id` ;
9
+ * le contrôle les cite. Un identifiant sans élément est ignoré par les
10
+ * lecteurs d'écran, donc un champ sans description ne cite rien de faux.
11
+ */
12
+ declare function useFieldDescribedBy(): string | undefined;
13
+ declare function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">): React.JSX.Element;
14
+ declare function FieldLegend({ className, variant, ...props }: React.ComponentProps<"legend"> & {
15
+ variant?: "legend" | "label";
16
+ }): React.JSX.Element;
17
+ declare function FieldGroup({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
18
+ declare const fieldVariants: Variants<{
19
+ orientation: "vertical" | "horizontal" | "responsive";
20
+ }>;
21
+ declare function Field({ className, orientation, ...props }: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>): React.JSX.Element;
22
+ declare function FieldContent({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
23
+ declare function FieldLabel({ className, htmlFor, ...props }: React.ComponentProps<typeof Label>): React.JSX.Element;
24
+ declare function FieldTitle({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
25
+ declare function FieldDescription({ className, id, ...props }: React.ComponentProps<"p">): React.JSX.Element;
26
+ declare function FieldSeparator({ children, className, ...props }: React.ComponentProps<"div"> & {
27
+ children?: React.ReactNode;
28
+ }): React.JSX.Element;
29
+ declare function FieldError({ className, children, errors, id, ...props }: React.ComponentProps<"div"> & {
30
+ errors?: Array<{
31
+ message?: string;
32
+ } | undefined>;
33
+ }): React.JSX.Element | null;
34
+ export { Field, FieldLabel, FieldDescription, FieldError, FieldGroup, FieldLegend, FieldSeparator, FieldSet, FieldContent, FieldTitle, useFieldDescribedBy, useFieldId, };
package/dist/field.js ADDED
@@ -0,0 +1,102 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { cva } from "class-variance-authority";
4
+ import { createContext, useContext, useId, useMemo } from "react";
5
+ import { Label } from "./label.js";
6
+ import { Separator } from "./separator.js";
7
+ import { cn } from "./cn.js";
8
+ /**
9
+ * Identifiant partagé entre un libellé et son champ.
10
+ *
11
+ * `Field` n'était qu'un `<div role="group">` : la liaison entre le libellé et
12
+ * le contrôle reposait entièrement sur l'appelant, qui devait poser `htmlFor`
13
+ * d'un côté et `id` de l'autre. Sur cent soixante-cinq libellés du portail,
14
+ * trente l'avaient oubliée — autant de champs qu'un lecteur d'écran annonce
15
+ * sans dire de quoi il s'agit, et sur lesquels cliquer le libellé ne fait rien.
16
+ *
17
+ * Le champ fabrique donc son identifiant et le passe aux deux. Un `htmlFor` ou
18
+ * un `id` explicite l'emporte toujours : les cent trente-cinq appels qui
19
+ * faisaient le travail à la main continuent de fonctionner tels quels.
20
+ */
21
+ const FieldIdContext = createContext(undefined);
22
+ /** Identifiant du champ courant, s'il y en a un. */
23
+ function useFieldId() {
24
+ return useContext(FieldIdContext);
25
+ }
26
+ /**
27
+ * Identifiants de la description et de l'erreur du champ courant, pour
28
+ * `aria-describedby`. `FieldDescription` et `FieldError` les portent en `id` ;
29
+ * le contrôle les cite. Un identifiant sans élément est ignoré par les
30
+ * lecteurs d'écran, donc un champ sans description ne cite rien de faux.
31
+ */
32
+ function useFieldDescribedBy() {
33
+ const id = useContext(FieldIdContext);
34
+ return id ? `${id}-description ${id}-error` : undefined;
35
+ }
36
+ function FieldSet({ className, ...props }) {
37
+ return (_jsx("fieldset", { "data-slot": "field-set", className: cn("flex flex-col gap-6 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3", className), ...props }));
38
+ }
39
+ function FieldLegend({ className, variant = "legend", ...props }) {
40
+ return (_jsx("legend", { "data-slot": "field-legend", "data-variant": variant, className: cn("mb-3 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base", className), ...props }));
41
+ }
42
+ function FieldGroup({ className, ...props }) {
43
+ return (_jsx("div", { "data-slot": "field-group", className: cn("group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4", className), ...props }));
44
+ }
45
+ const fieldVariants = cva("group/field flex w-full gap-3 data-[invalid=true]:text-destructive", {
46
+ variants: {
47
+ orientation: {
48
+ vertical: "flex-col *:w-full [&>.sr-only]:w-auto",
49
+ horizontal: "flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
50
+ responsive: "flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
51
+ },
52
+ },
53
+ defaultVariants: {
54
+ orientation: "vertical",
55
+ },
56
+ });
57
+ function Field({ className, orientation = "vertical", ...props }) {
58
+ const id = useId();
59
+ return (_jsx(FieldIdContext, { value: id, children: _jsx("div", { role: "group", "data-slot": "field", "data-orientation": orientation, className: cn(fieldVariants({ orientation }), className), ...props }) }));
60
+ }
61
+ function FieldContent({ className, ...props }) {
62
+ return (_jsx("div", { "data-slot": "field-content", className: cn("group/field-content flex flex-1 flex-col gap-1 leading-snug", className), ...props }));
63
+ }
64
+ function FieldLabel({ className, htmlFor, ...props }) {
65
+ const fieldId = useFieldId();
66
+ return (_jsx(Label, { htmlFor: htmlFor ?? fieldId, "data-slot": "field-label", className: cn("group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-data-checked:bg-input/30 has-[>[data-slot=field]]:rounded-2xl has-[>[data-slot=field]]:border *:data-[slot=field]:p-4", "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col", className), ...props }));
67
+ }
68
+ function FieldTitle({ className, ...props }) {
69
+ return (_jsx("div", { "data-slot": "field-label", className: cn("flex w-fit items-center gap-2 text-sm font-medium group-data-[disabled=true]/field:opacity-50", className), ...props }));
70
+ }
71
+ function FieldDescription({ className, id, ...props }) {
72
+ const fieldId = useFieldId();
73
+ return (_jsx("p", { id: id ?? (fieldId ? `${fieldId}-description` : undefined), "data-slot": "field-description", className: cn("text-left text-sm leading-normal font-normal text-muted-foreground group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5", "last:mt-0 nth-last-2:-mt-1", "[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary", className), ...props }));
74
+ }
75
+ function FieldSeparator({ children, className, ...props }) {
76
+ return (_jsxs("div", { "data-slot": "field-separator", "data-content": !!children, className: cn("relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2", className), ...props, children: [_jsx(Separator, { className: "absolute inset-0 top-1/2" }), children && (_jsx("span", { className: "relative mx-auto block w-fit bg-background px-2 text-muted-foreground", "data-slot": "field-separator-content", children: children }))] }));
77
+ }
78
+ function FieldError({ className, children, errors, id, ...props }) {
79
+ const fieldId = useFieldId();
80
+ const content = useMemo(() => {
81
+ if (children) {
82
+ return children;
83
+ }
84
+ if (!errors?.length) {
85
+ return null;
86
+ }
87
+ const uniqueErrors = [...new Map(errors.map((error) => [error?.message, error])).values()];
88
+ if (uniqueErrors.length === 1) {
89
+ return uniqueErrors[0]?.message;
90
+ }
91
+ return (_jsx("ul", { className: "ml-4 flex list-disc flex-col gap-1", children: uniqueErrors.map((error, index) => error?.message && _jsx("li", { children: error.message }, index)) }));
92
+ }, [children, errors]);
93
+ if (!content) {
94
+ return null;
95
+ }
96
+ return (_jsx("div", { role: "alert", id: id ?? (fieldId ? `${fieldId}-error` : undefined), "data-slot": "field-error",
97
+ // `soft-in` : un message d'erreur surgissait sous le champ et poussait
98
+ // tout le formulaire d'un cran. Deux dixièmes de seconde suffisent à
99
+ // faire lire une arrivée plutôt qu'un sursaut.
100
+ className: cn("soft-in text-sm font-normal text-destructive", className), ...props, children: content }));
101
+ }
102
+ export { Field, FieldLabel, FieldDescription, FieldError, FieldGroup, FieldLegend, FieldSeparator, FieldSet, FieldContent, FieldTitle, useFieldDescribedBy, useFieldId, };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * La règle unique des surfaces flottantes : menus, sélecteurs, combobox,
3
+ * popovers (2026-09-27).
4
+ *
5
+ * Base UI retourne une surface qui manque de place (`flip`) mais, par défaut,
6
+ * ne la décale pas le long de son ancre : un menu de 16 rem aligné sur un
7
+ * bouton près du bord d'un téléphone sortait de l'écran (menu d'une phase,
8
+ * choix de l'organisation d'un document). `align: "shift"` la ramène dans la
9
+ * fenêtre, `fallbackAxisSide: "none"` évite qu'elle saute sur le côté de
10
+ * l'ancre, et la marge garde 8 px entre la surface et le bord.
11
+ */
12
+ export declare const FLOATING_COLLISION_AVOIDANCE: {
13
+ readonly side: "flip";
14
+ readonly align: "shift";
15
+ readonly fallbackAxisSide: "none";
16
+ };
17
+ export declare const FLOATING_COLLISION_PADDING = 8;
18
+ /**
19
+ * Le fond de toute surface flottante, opaque (2026-10-05) : le verre dépoli
20
+ * (`bg-popover/80` + flou) laissait voir un bouton blanc posé dessous.
21
+ * `--popover` et `--muted` sont redéfinis sur la surface : un pied collé en
22
+ * `bg-popover` (sélecteurs de projet et de client) prend la même teinte, et le
23
+ * survol des items (`bg-muted`) reste lisible sur le fond plus clair du thème
24
+ * sombre (jetons `--floating`, `--floating-muted` dans `styles.css`).
25
+ */
26
+ export declare const FLOATING_SURFACE = "bg-popover text-popover-foreground shadow-xl shadow-black/5 [--muted:var(--floating-muted)] [--popover:var(--floating)]";
27
+ /**
28
+ * Jamais plus large que la fenêtre, marges comprises : une largeur fixe
29
+ * (`w-72`) passée par l'appelant ne peut plus déborder d'un écran de 320 px.
30
+ */
31
+ export declare const FLOATING_MAX_WIDTH = "max-w-[calc(100vw-1rem)]";
32
+ /**
33
+ * Un sous-menu (« Déplacer vers ») n'a souvent pas la place de se retourner
34
+ * sur un téléphone : il glisse dans la fenêtre, quitte à chevaucher son menu.
35
+ */
36
+ export declare const FLOATING_SUBMENU_COLLISION_AVOIDANCE: {
37
+ readonly side: "shift";
38
+ readonly align: "shift";
39
+ readonly fallbackAxisSide: "none";
40
+ };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * La règle unique des surfaces flottantes : menus, sélecteurs, combobox,
3
+ * popovers (2026-09-27).
4
+ *
5
+ * Base UI retourne une surface qui manque de place (`flip`) mais, par défaut,
6
+ * ne la décale pas le long de son ancre : un menu de 16 rem aligné sur un
7
+ * bouton près du bord d'un téléphone sortait de l'écran (menu d'une phase,
8
+ * choix de l'organisation d'un document). `align: "shift"` la ramène dans la
9
+ * fenêtre, `fallbackAxisSide: "none"` évite qu'elle saute sur le côté de
10
+ * l'ancre, et la marge garde 8 px entre la surface et le bord.
11
+ */
12
+ export const FLOATING_COLLISION_AVOIDANCE = {
13
+ side: "flip",
14
+ align: "shift",
15
+ fallbackAxisSide: "none",
16
+ };
17
+ export const FLOATING_COLLISION_PADDING = 8;
18
+ /**
19
+ * Le fond de toute surface flottante, opaque (2026-10-05) : le verre dépoli
20
+ * (`bg-popover/80` + flou) laissait voir un bouton blanc posé dessous.
21
+ * `--popover` et `--muted` sont redéfinis sur la surface : un pied collé en
22
+ * `bg-popover` (sélecteurs de projet et de client) prend la même teinte, et le
23
+ * survol des items (`bg-muted`) reste lisible sur le fond plus clair du thème
24
+ * sombre (jetons `--floating`, `--floating-muted` dans `styles.css`).
25
+ */
26
+ export const FLOATING_SURFACE = "bg-popover text-popover-foreground shadow-xl shadow-black/5 [--muted:var(--floating-muted)] [--popover:var(--floating)]";
27
+ /**
28
+ * Jamais plus large que la fenêtre, marges comprises : une largeur fixe
29
+ * (`w-72`) passée par l'appelant ne peut plus déborder d'un écran de 320 px.
30
+ */
31
+ export const FLOATING_MAX_WIDTH = "max-w-[calc(100vw-1rem)]";
32
+ /**
33
+ * Un sous-menu (« Déplacer vers ») n'a souvent pas la place de se retourner
34
+ * sur un téléphone : il glisse dans la fenêtre, quitte à chevaucher son menu.
35
+ */
36
+ export const FLOATING_SUBMENU_COLLISION_AVOIDANCE = {
37
+ side: "shift",
38
+ align: "shift",
39
+ fallbackAxisSide: "none",
40
+ };
@@ -0,0 +1,18 @@
1
+ import { OTPInput } from "input-otp";
2
+ import * as React from "react";
3
+ /**
4
+ * Saisie d'un code à usage unique, case par case (shadcn `input-otp`, base
5
+ * `input-otp`). Un seul vrai `<input>` porte le clavier, le collage d'un code
6
+ * entier et l'`autocomplete="one-time-code"` ; les cases ne font que l'afficher.
7
+ * Habillage du champ du site : fond de page, filet à 70 %, anneau vert sur
8
+ * la case active.
9
+ */
10
+ declare function InputOTP({ className, containerClassName, ...props }: React.ComponentProps<typeof OTPInput> & {
11
+ containerClassName?: string;
12
+ }): React.JSX.Element;
13
+ declare function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
14
+ declare function InputOTPSlot({ index, className, ...props }: React.ComponentProps<"div"> & {
15
+ index: number;
16
+ }): React.JSX.Element;
17
+ declare function InputOTPSeparator(props: React.ComponentProps<"div">): React.JSX.Element;
18
+ export { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot };
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { OTPInput, OTPInputContext } from "input-otp";
3
+ import * as React from "react";
4
+ import { cn } from "./cn.js";
5
+ /**
6
+ * Saisie d'un code à usage unique, case par case (shadcn `input-otp`, base
7
+ * `input-otp`). Un seul vrai `<input>` porte le clavier, le collage d'un code
8
+ * entier et l'`autocomplete="one-time-code"` ; les cases ne font que l'afficher.
9
+ * Habillage du champ du site : fond de page, filet à 70 %, anneau vert sur
10
+ * la case active.
11
+ */
12
+ function InputOTP({ className, containerClassName, ...props }) {
13
+ return (_jsx(OTPInput, { "data-slot": "input-otp", containerClassName: cn("flex items-center has-disabled:opacity-50", containerClassName), spellCheck: false, className: cn("disabled:cursor-not-allowed", className), ...props }));
14
+ }
15
+ function InputOTPGroup({ className, ...props }) {
16
+ return _jsx("div", { "data-slot": "input-otp-group", className: cn("flex items-center gap-2", className), ...props });
17
+ }
18
+ function InputOTPSlot({ index, className, ...props }) {
19
+ const context = React.useContext(OTPInputContext);
20
+ const { char, hasFakeCaret, isActive } = context.slots[index] ?? {};
21
+ return (_jsxs("div", { "data-slot": "input-otp-slot", "data-active": isActive, className: cn("relative flex h-13 w-11 items-center justify-center rounded-2xl bg-background text-xl font-medium tabular-nums ring-1 ring-border/70 transition-[box-shadow] duration-(--duration-fast) ring-inset aria-invalid:ring-2 aria-invalid:ring-destructive/60 data-[active=true]:ring-2 data-[active=true]:ring-ring sm:h-12 sm:w-10 sm:text-lg", className), ...props, children: [char, hasFakeCaret ? (_jsx("div", { className: "pointer-events-none absolute inset-0 flex items-center justify-center", children: _jsx("div", { className: "h-5 w-px bg-foreground motion-safe:animate-pulse" }) })) : null] }));
22
+ }
23
+ function InputOTPSeparator(props) {
24
+ return (_jsx("div", { "data-slot": "input-otp-separator", role: "separator", className: "h-px w-2.5 bg-muted-foreground/40", ...props }));
25
+ }
26
+ export { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot };
@@ -0,0 +1,3 @@
1
+ import * as React from "react";
2
+ declare function Input({ className, type, ...props }: React.ComponentProps<"input">): React.JSX.Element;
3
+ export { Input };
package/dist/input.js ADDED
@@ -0,0 +1,36 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Input as InputPrimitive } from "@base-ui/react/input";
3
+ import Calendar03Icon from "@hugeicons/core-free-icons/Calendar03Icon";
4
+ import Clock01Icon from "@hugeicons/core-free-icons/Clock01Icon";
5
+ import { HugeiconsIcon } from "@hugeicons/react";
6
+ import * as React from "react";
7
+ import { useFieldDescribedBy, useFieldId } from "./field.js";
8
+ import { cn } from "./cn.js";
9
+ /**
10
+ * Les champs de date et d'heure portent leur icône à gauche.
11
+ *
12
+ * iOS n'en dessine aucune : une échéance ressemblait à un champ texte vide. Et
13
+ * sans `appearance: none` (classe `date-field`, `styles.css`), Safari donne au
14
+ * champ une largeur propre qui débordait de la feuille du bas, son filet
15
+ * courant jusqu'au bord de l'écran.
16
+ */
17
+ const PICKER_ICONS = {
18
+ date: Calendar03Icon,
19
+ month: Calendar03Icon,
20
+ "datetime-local": Calendar03Icon,
21
+ time: Clock01Icon,
22
+ };
23
+ const INPUT_CLASS =
24
+ // Le champ du site : fond de page, un filet à 70 % au repos, et l'anneau
25
+ // vert plein au focus — le seul endroit où l'accent dit « c'est ici ».
26
+ "h-11 lg:h-10 touch-field w-full min-w-0 rounded-2xl bg-background px-3.5 py-1 text-base ring-1 ring-border/70 transition-[color,box-shadow,background-color] duration-(--duration-fast) outline-none ring-inset file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-2 aria-invalid:ring-destructive/60 md:text-sm";
27
+ function Input({ className, type, ...props }) {
28
+ const fieldId = useFieldId();
29
+ const describedBy = useFieldDescribedBy();
30
+ const pickerIcon = type ? PICKER_ICONS[type] : undefined;
31
+ const input = (_jsx(InputPrimitive, { type: type, "data-slot": "input", "aria-describedby": props["aria-describedby"] ?? describedBy, className: cn(INPUT_CLASS, pickerIcon && "relative date-field pl-10", className), id: props.id ?? fieldId, ...props }));
32
+ if (!pickerIcon)
33
+ return input;
34
+ return (_jsxs("div", { className: "relative w-full min-w-0", children: [input, _jsx(HugeiconsIcon, { icon: pickerIcon, strokeWidth: 2, "aria-hidden": true, className: "pointer-events-none absolute top-1/2 left-3.5 size-4 -translate-y-1/2 text-muted-foreground" })] }));
35
+ }
36
+ export { Input };
@@ -0,0 +1,3 @@
1
+ import * as React from "react";
2
+ declare function Label({ className, ...props }: React.ComponentProps<"label">): React.JSX.Element;
3
+ export { Label };
package/dist/label.js ADDED
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { cn } from "./cn.js";
4
+ function Label({ className, ...props }) {
5
+ return (_jsx("label", { "data-slot": "label", className: cn("flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50", className), ...props }));
6
+ }
7
+ export { Label };
@@ -0,0 +1,32 @@
1
+ import { type IconSvgElement } from "@hugeicons/react";
2
+ type SegmentedControlProps<T extends string> = {
3
+ value: T;
4
+ onValueChange: (value: T) => void;
5
+ options: Array<{
6
+ value: T;
7
+ label: string;
8
+ /** Remplace le libellé, qui devient alors l'étiquette accessible. */
9
+ icon?: IconSvgElement;
10
+ }>;
11
+ /** Compact variant — smaller padding and text. */
12
+ size?: "md" | "sm";
13
+ /**
14
+ * Nom du groupe pour les technologies d'assistance, quand aucun `<label>`
15
+ * ne peut le désigner : un titre de champ est un `<div>`, rien ne le relie
16
+ * au groupe autrement.
17
+ */
18
+ label?: string;
19
+ className?: string;
20
+ };
21
+ /**
22
+ * Horizontal segmented toggle: a row of mutually-exclusive options sharing a
23
+ * single pill container. Accessible via a button group with `aria-pressed`.
24
+ *
25
+ * Une option peut porter une icône au lieu de son libellé. Trois mots côte à
26
+ * côte obligeaient à élargir le conteneur pour que le plus long tienne, et
27
+ * « Clair / Sombre / Système » se lit de toute façon plus vite en soleil, lune
28
+ * et écran. Le libellé devient alors l'étiquette accessible et l'infobulle : la
29
+ * signification reste atteignable, elle n'occupe plus la place.
30
+ */
31
+ export declare function SegmentedControl<T extends string>({ value, onValueChange, options, size, label, className, }: SegmentedControlProps<T>): React.JSX.Element;
32
+ export {};
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // Même dessin dans tout l'écosystème.
3
+ import { HugeiconsIcon } from "@hugeicons/react";
4
+ import { cn } from "./cn.js";
5
+ /**
6
+ * Horizontal segmented toggle: a row of mutually-exclusive options sharing a
7
+ * single pill container. Accessible via a button group with `aria-pressed`.
8
+ *
9
+ * Une option peut porter une icône au lieu de son libellé. Trois mots côte à
10
+ * côte obligeaient à élargir le conteneur pour que le plus long tienne, et
11
+ * « Clair / Sombre / Système » se lit de toute façon plus vite en soleil, lune
12
+ * et écran. Le libellé devient alors l'étiquette accessible et l'infobulle : la
13
+ * signification reste atteignable, elle n'occupe plus la place.
14
+ */
15
+ export function SegmentedControl({ value, onValueChange, options, size = "md", label, className, }) {
16
+ return (_jsx("div", { role: "group", "aria-label": label, className: cn(
17
+ // Fond `muted` plutôt qu'un contour (ADR 0013).
18
+ "inline-flex w-fit max-w-full shrink-0 overflow-hidden rounded-3xl bg-muted p-1", className), children: options.map((opt) => {
19
+ const active = value === opt.value;
20
+ return (_jsx("button", { type: "button", "aria-pressed": active, "aria-label": opt.icon ? opt.label : undefined, title: opt.icon ? opt.label : undefined, onClick: () => onValueChange(opt.value), className: cn(
21
+ // Pas de flex-1 : une demi-colonne trop étroite rend le segment
22
+ // actif circulaire et casse « Follow-up » au tiret.
23
+ "touch-target shrink-0 rounded-full px-3.5 font-medium whitespace-nowrap", "transition-colors duration-150 ease-out", opt.icon && "flex items-center justify-center", size === "sm" ? "py-1.5 text-xs" : "py-2 text-sm", active ? "bg-foreground text-background" : "text-muted-foreground hover:text-foreground"), children: opt.icon ? (_jsx(HugeiconsIcon, { icon: opt.icon, strokeWidth: 2, className: size === "sm" ? "size-4" : "size-[1.15rem]" })) : (opt.label) }, opt.value));
24
+ }) }));
25
+ }
@@ -0,0 +1,21 @@
1
+ import { Select as SelectPrimitive } from "@base-ui/react/select";
2
+ import * as React from "react";
3
+ declare function Select<Value>(props: SelectPrimitive.Root.Props<Value>): React.JSX.Element;
4
+ declare function SelectTrigger({ className, children, ...props }: SelectPrimitive.Trigger.Props): React.JSX.Element;
5
+ type SelectValueProps = Omit<SelectPrimitive.Value.Props, "children"> & {
6
+ /**
7
+ * Map value → label. Lorsque fourni sans `children`, le wrapper résout
8
+ * automatiquement le label à afficher. Évite que le SelectValue affiche
9
+ * une valeur brute (ex: ID Convex) quand `items` du Root ne couvre pas le
10
+ * cas ou quand la signature attendue par Base UI diffère.
11
+ */
12
+ items?: Record<string, React.ReactNode>;
13
+ children?: SelectPrimitive.Value.Props["children"];
14
+ };
15
+ declare function SelectValue({ className, items, children, ...props }: SelectValueProps): React.JSX.Element;
16
+ declare function SelectContent({ className, align, alignOffset, side, sideOffset, ...props }: SelectPrimitive.Popup.Props & Pick<SelectPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset">): React.JSX.Element;
17
+ declare function SelectItem({ className, children, ...props }: SelectPrimitive.Item.Props): React.JSX.Element;
18
+ declare function SelectGroup({ ...props }: SelectPrimitive.Group.Props): React.JSX.Element;
19
+ declare function SelectGroupLabel({ className, ...props }: SelectPrimitive.GroupLabel.Props): React.JSX.Element;
20
+ declare function SelectSeparator({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
21
+ export { Select, SelectTrigger, SelectValue, SelectContent, SelectItem, SelectGroup, SelectGroupLabel, SelectSeparator };
package/dist/select.js ADDED
@@ -0,0 +1,73 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Select as SelectPrimitive } from "@base-ui/react/select";
3
+ import ArrowDown01Icon from "@hugeicons/core-free-icons/ArrowDown01Icon";
4
+ import Tick02Icon from "@hugeicons/core-free-icons/Tick02Icon";
5
+ import { HugeiconsIcon } from "@hugeicons/react";
6
+ import * as React from "react";
7
+ import { useFieldDescribedBy } from "./field.js";
8
+ import { FLOATING_COLLISION_AVOIDANCE, FLOATING_COLLISION_PADDING, FLOATING_MAX_WIDTH, FLOATING_SURFACE, } from "./floating.js";
9
+ import { cn } from "./cn.js";
10
+ /**
11
+ * Base UI Select wrapper. Replaces the previous native <select> wrapper so
12
+ * the dropdown is consistent across platforms (desktop + Android Chrome get
13
+ * the custom popover).
14
+ *
15
+ * iOS compris : la molette du système a été retirée (ADR 0045, révision du
16
+ * 2026-09-23), le menu dessiné tient la DA sur tous les appareils.
17
+ *
18
+ * Popup DA matches DropdownMenuContent: rounded-2xl, opaque
19
+ * `FLOATING_SURFACE`, a soft shadow and no ring.
20
+ *
21
+ * Typical usage:
22
+ *
23
+ * <Select value={x} onValueChange={setX}>
24
+ * <SelectTrigger id="x"><SelectValue placeholder="–" /></SelectTrigger>
25
+ * <SelectContent>
26
+ * <SelectItem value="a">A</SelectItem>
27
+ * </SelectContent>
28
+ * </Select>
29
+ */
30
+ // Même recette que `Input` : filet à 70 % au repos, anneau vert au focus et
31
+ // quand le menu est ouvert.
32
+ const triggerClasses = "flex h-11 lg:h-10 touch-field w-full min-w-0 items-center justify-between gap-2 rounded-2xl bg-background px-3.5 text-base ring-1 ring-border/70 transition-[color,box-shadow,background-color] duration-(--duration-fast) outline-none ring-inset placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-2 aria-invalid:ring-destructive/60 data-popup-open:ring-2 data-popup-open:ring-ring md:text-sm";
33
+ function Select(props) {
34
+ return _jsx(SelectPrimitive.Root, { ...props });
35
+ }
36
+ function SelectTrigger({ className, children, ...props }) {
37
+ const describedBy = useFieldDescribedBy();
38
+ return (_jsxs(SelectPrimitive.Trigger, { "data-slot": "select-trigger", "aria-describedby": props["aria-describedby"] ?? describedBy, className: cn(triggerClasses, className), ...props, children: [children, _jsx(SelectPrimitive.Icon, { className: "shrink-0 text-muted-foreground", children: _jsx(HugeiconsIcon, { icon: ArrowDown01Icon, strokeWidth: 2, className: "size-4" }) })] }));
39
+ }
40
+ function SelectValue({ className, items, children, ...props }) {
41
+ const renderChildren = children ??
42
+ (items
43
+ ? (value) => {
44
+ if (value == null || value === "")
45
+ return null;
46
+ if (typeof value !== "string" && typeof value !== "number")
47
+ return null;
48
+ const key = String(value);
49
+ return items[key] ?? key;
50
+ }
51
+ : undefined);
52
+ return (_jsx(SelectPrimitive.Value, { "data-slot": "select-value", className: cn("truncate text-left data-empty:text-muted-foreground", className), ...props, children: renderChildren }));
53
+ }
54
+ function SelectContent({ className, align = "start", alignOffset = 0, side = "bottom", sideOffset = 4, ...props }) {
55
+ return (_jsx(SelectPrimitive.Portal, { children: _jsx(SelectPrimitive.Positioner, { className: "isolate z-50 outline-none", align: align, alignOffset: alignOffset, side: side, sideOffset: sideOffset, collisionPadding: FLOATING_COLLISION_PADDING, collisionAvoidance: FLOATING_COLLISION_AVOIDANCE, children: _jsx(SelectPrimitive.Popup, { "data-slot": "select-content", className: cn(FLOATING_MAX_WIDTH, FLOATING_SURFACE,
56
+ // Transitions (pas de keyframes) : un open→close rapide retarget
57
+ // depuis la position courante au lieu de rejouer l'anim entière.
58
+ // Exit (100ms) plus court que l'enter (150ms).
59
+ "relative z-50 max-h-(--available-height) min-w-(--anchor-width) origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-2xl p-1.5 transition-[scale,opacity] duration-(--duration-fast) ease-ios outline-none data-ending-style:scale-[0.98] data-ending-style:opacity-0 data-ending-style:duration-(--duration-instant) data-starting-style:scale-[0.98] data-starting-style:opacity-0", className), ...props }) }) }));
60
+ }
61
+ function SelectItem({ className, children, ...props }) {
62
+ return (_jsxs(SelectPrimitive.Item, { "data-slot": "select-item", className: cn("relative flex cursor-default items-center gap-2.5 rounded-xl py-2 pr-8 pl-3 text-sm font-medium outline-hidden select-none focus:bg-muted data-highlighted:bg-muted data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className), ...props, children: [_jsx(SelectPrimitive.ItemText, { children: children }), _jsx(SelectPrimitive.ItemIndicator, { className: "pointer-events-none absolute right-2 flex items-center justify-center", children: _jsx(HugeiconsIcon, { icon: Tick02Icon, strokeWidth: 2, className: "size-4" }) })] }));
63
+ }
64
+ function SelectGroup({ ...props }) {
65
+ return _jsx(SelectPrimitive.Group, { "data-slot": "select-group", ...props });
66
+ }
67
+ function SelectGroupLabel({ className, ...props }) {
68
+ return (_jsx(SelectPrimitive.GroupLabel, { "data-slot": "select-group-label", className: cn("px-3 py-2 text-xs text-muted-foreground", className), ...props }));
69
+ }
70
+ function SelectSeparator({ className, ...props }) {
71
+ return _jsx("div", { "data-slot": "select-separator", className: cn("-mx-1.5 my-1.5 h-px bg-border/50", className), ...props });
72
+ }
73
+ export { Select, SelectTrigger, SelectValue, SelectContent, SelectItem, SelectGroup, SelectGroupLabel, SelectSeparator };
@@ -0,0 +1,3 @@
1
+ import { Separator as SeparatorPrimitive } from "@base-ui/react/separator";
2
+ declare function Separator({ className, orientation, ...props }: SeparatorPrimitive.Props): React.JSX.Element;
3
+ export { Separator };
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Separator as SeparatorPrimitive } from "@base-ui/react/separator";
3
+ import { cn } from "./cn.js";
4
+ function Separator({ className, orientation = "horizontal", ...props }) {
5
+ return (_jsx(SeparatorPrimitive, { "data-slot": "separator", orientation: orientation, className: cn("shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch", className), ...props }));
6
+ }
7
+ export { Separator };
@@ -0,0 +1,3 @@
1
+ import { type ToasterProps } from "sonner";
2
+ declare function Toaster(props: ToasterProps): React.JSX.Element;
3
+ export { Toaster };
package/dist/sonner.js ADDED
@@ -0,0 +1,33 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import Alert02Icon from "@hugeicons/core-free-icons/Alert02Icon";
3
+ import CheckmarkCircle02Icon from "@hugeicons/core-free-icons/CheckmarkCircle02Icon";
4
+ import InformationCircleIcon from "@hugeicons/core-free-icons/InformationCircleIcon";
5
+ import Loading03Icon from "@hugeicons/core-free-icons/Loading03Icon";
6
+ import MultiplicationSignCircleIcon from "@hugeicons/core-free-icons/MultiplicationSignCircleIcon";
7
+ import { HugeiconsIcon } from "@hugeicons/react";
8
+ import { Toaster as Sonner } from "sonner";
9
+ import { useTheme } from "./theme.js";
10
+ function Toaster(props) {
11
+ const { theme } = useTheme();
12
+ // Offset = hauteur navbar (3.25rem) + safe-area-inset-top + petit gap (0.5rem)
13
+ // pour que les toasts top-right ne soient pas mangés par le header sticky
14
+ // ni par le notch iOS PWA.
15
+ const topOffset = "calc(env(safe-area-inset-top) + 3.75rem)";
16
+ return (_jsx(Sonner, { theme: theme, className: "toaster group", position: "top-right", offset: { top: topOffset, right: "1rem", left: "1rem", bottom: "1rem" }, mobileOffset: { top: topOffset, right: "0.75rem", left: "0.75rem", bottom: "1rem" }, icons: {
17
+ success: _jsx(HugeiconsIcon, { icon: CheckmarkCircle02Icon, strokeWidth: 2, className: "size-4" }),
18
+ info: _jsx(HugeiconsIcon, { icon: InformationCircleIcon, strokeWidth: 2, className: "size-4" }),
19
+ warning: _jsx(HugeiconsIcon, { icon: Alert02Icon, strokeWidth: 2, className: "size-4" }),
20
+ error: _jsx(HugeiconsIcon, { icon: MultiplicationSignCircleIcon, strokeWidth: 2, className: "size-4" }),
21
+ loading: _jsx(HugeiconsIcon, { icon: Loading03Icon, strokeWidth: 2, className: "size-4 animate-spin" }),
22
+ }, style: {
23
+ "--normal-bg": "var(--popover)",
24
+ "--normal-text": "var(--popover-foreground)",
25
+ "--normal-border": "var(--border)",
26
+ "--border-radius": "var(--radius)",
27
+ }, toastOptions: {
28
+ classNames: {
29
+ toast: "cn-toast",
30
+ },
31
+ }, ...props }));
32
+ }
33
+ export { Toaster };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Inline spinner - Hugeicons `Loading03Icon` + animate-spin.
3
+ * Hugeicons gardent l'identite DA, contrairement a un raw SVG ou un Tailwind border-spin.
4
+ */
5
+ export declare function Spinner({ className, label, size, }: {
6
+ className?: string;
7
+ label?: string;
8
+ size?: 3 | 4 | 5 | 6;
9
+ }): React.JSX.Element;
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import Loading03Icon from "@hugeicons/core-free-icons/Loading03Icon";
3
+ import { HugeiconsIcon } from "@hugeicons/react";
4
+ import { cn } from "./cn.js";
5
+ /**
6
+ * Inline spinner - Hugeicons `Loading03Icon` + animate-spin.
7
+ * Hugeicons gardent l'identite DA, contrairement a un raw SVG ou un Tailwind border-spin.
8
+ */
9
+ export function Spinner({ className, label, size = 4, }) {
10
+ return (_jsxs("span", { className: cn("inline-flex items-center gap-2", className), children: [_jsx(HugeiconsIcon, { icon: Loading03Icon, strokeWidth: 2, className: cn("animate-spin", size === 3 && "size-3", size === 4 && "size-4", size === 5 && "size-5", size === 6 && "size-6") }), label ? _jsx("span", { children: label }) : null] }));
11
+ }
@@ -0,0 +1,17 @@
1
+ import { Switch as SwitchPrimitive } from "@base-ui/react/switch";
2
+ /**
3
+ * Interrupteur — la forme qu'a un réglage qu'on allume et qu'on éteint.
4
+ *
5
+ * Il remplace le couple « phrase qui décrit l'état + bouton qui le change ».
6
+ * Ces deux-là disaient la même chose deux fois — « Notifications activées »
7
+ * au-dessus de « Désactiver » — et l'un des deux mentait toujours au premier
8
+ * coup d'œil, puisqu'un bouton porte l'action et la phrase porte l'état.
9
+ *
10
+ * Une pastille de texte avait d'abord pris leur place ; elle disait bien l'état
11
+ * mais ne se lisait pas comme un contrôle. L'interrupteur, lui, n'a pas besoin
12
+ * d'être expliqué : sa forme est son mode d'emploi.
13
+ *
14
+ * Dimensions calées sur iOS — 51 × 31 points, soit 3,25 × 2 rem — pour que le
15
+ * pouce le trouve sans viser.
16
+ */
17
+ export declare function Switch({ checked, onCheckedChange, disabled, className, ...props }: SwitchPrimitive.Root.Props): React.JSX.Element;