@hgdc/ui 0.20.0 → 0.22.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/README.md +20 -19
- package/dist/app-shell.js +3 -3
- package/dist/assistant-chat.d.ts +77 -0
- package/dist/assistant-chat.js +70 -0
- package/dist/board.d.ts +9 -0
- package/dist/board.js +92 -17
- package/dist/command-menu.js +1 -1
- package/dist/date-range-picker.js +1 -1
- package/dist/dropdown-menu.js +4 -4
- package/dist/input-otp.js +1 -1
- package/dist/input.js +6 -3
- package/dist/org-switcher.js +2 -2
- package/dist/page-header.js +1 -1
- package/dist/product-rail.js +1 -1
- package/dist/resource-list.d.ts +1 -1
- package/dist/resource-list.js +1 -1
- package/dist/select.js +4 -4
- package/dist/switch.js +1 -1
- package/dist/table.js +1 -1
- package/dist/tabs.d.ts +5 -1
- package/dist/tabs.js +38 -2
- package/dist/textarea.js +1 -1
- package/package.json +1 -1
- package/styles.css +31 -4
package/README.md
CHANGED
|
@@ -80,25 +80,26 @@ Sur ordinateur, la coque ouvre sur la barre des produits (ADR 0011) : organisati
|
|
|
80
80
|
|
|
81
81
|
## Contenu et données
|
|
82
82
|
|
|
83
|
-
| Import
|
|
84
|
-
|
|
|
85
|
-
| `card`
|
|
86
|
-
| `alert`
|
|
87
|
-
| `table`
|
|
88
|
-
| `resource-list`
|
|
89
|
-
| `stat`
|
|
90
|
-
| `time-chart`
|
|
91
|
-
| `status`
|
|
92
|
-
| `uptime-bar`
|
|
93
|
-
| `timeline`
|
|
94
|
-
| `relative-time`
|
|
95
|
-
| `copy-button`
|
|
96
|
-
| `progress`
|
|
97
|
-
| `rank-bar`
|
|
98
|
-
| `secret-reveal`
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
83
|
+
| Import | Exemple |
|
|
84
|
+
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
85
|
+
| `card` | `<Card><CardHeader><CardTitle>Domaines</CardTitle><CardAction>…</CardAction></CardHeader><CardContent>…</CardContent><CardFooter>…</CardFooter></Card>` |
|
|
86
|
+
| `alert` | `<Alert tone="warning" title="Quota presque atteint" action={<Button size="sm">Voir</Button>}>90 % utilisés.</Alert>` |
|
|
87
|
+
| `table` | `<TableHead sort="descending" onSort={…}>Date</TableHead>`, `<TableRowLink render={<Link … />}>Production</TableRowLink>`, `<TableEmpty colSpan={4}>…</TableEmpty>`, `<TableSkeleton columns={4} />` |
|
|
88
|
+
| `resource-list` | `<ResourceList><ResourceItem icon={…} title="Site vitrine" meta="acme.ch" badge={…} actions={…} href="/p/site" /></ResourceList>` |
|
|
89
|
+
| `stat` | `<StatGrid><StatTile label="Erreurs" value="12" delta={{ value: "8 %", direction: "up", good: "down" }} /></StatGrid>` |
|
|
90
|
+
| `time-chart` | `<TimeChart label="Envois par jour" timestamps={days} series={[{ key: "sent", label: "Envoyés", values }]} kind="bar" lastIncomplete />` (dernier point en pointillés : la journée n'est pas finie) |
|
|
91
|
+
| `status` | `<StatusBadge status="degraded" />` (Opérationnel, Dégradé, Panne, Maintenance, Inconnu) ; hors services, `<StatusBadge tone="success" label="Remis" />` (`neutral`, `success`, `warning`, `danger`, `info`, `pending`) |
|
|
92
|
+
| `uptime-bar` | `<UptimeBar days={[{ date, status: "operational", uptime: "100 %" }]} summary="99,95 % sur 90 jours" />` |
|
|
93
|
+
| `timeline` | `<Timeline entries={[{ id, title: "Résolu", time, status: "operational" }]} />` |
|
|
94
|
+
| `relative-time` | `<RelativeTime date={deployment._creationTime} />` (« il y a 3 min », date exacte au survol) |
|
|
95
|
+
| `copy-button` | `<Snippet value="bun add @hgdc/ui" prompt />`, `<CopyButton value={url} />`, `<CopyButton value={all} label="Tout copier" showLabel />` |
|
|
96
|
+
| `progress` | `<Progress label="Envoi de la campagne" value={120} max={400} />` (`value={null}` : durée inconnue), `<Meter label="E-mails ce mois" value={8600} max={10000} />` (orange puis rouge près de la limite) |
|
|
97
|
+
| `rank-bar` | `<RankBar label="Pages les plus vues" items={[{ key, label: "/tarifs", value: 640, render: <Link … /> }]} limit={5} />` |
|
|
98
|
+
| `secret-reveal` | `<SecretReveal label="Clé d'API" value={key} onDismiss={forget} />` : affichée une fois, puis retirée de la page |
|
|
99
|
+
| `assistant-chat` | `<AssistantConversation><AssistantUserMessage>…</AssistantUserMessage><AssistantReply tools={chips} waiting={pending} actions={<AssistantActionCard title=… status="pending" onConfirm={…} onCancel={…} />}><Markdown>{text}</Markdown></AssistantReply></AssistantConversation>`, `<AssistantComposer value={v} onValueChange={setV} onSubmit={send} busy={busy} />`, `<AssistantExamples examples={…} onPick={setV} />` : le produit rend le Markdown |
|
|
100
|
+
| `family-loader` | `<FamilyLoaderRoot>{children}</FamilyLoaderRoot>` une fois dans le `body` de la route racine : le seul H d'un chargement de page, fixe sur la zone de contenu de la coque, rendu par le serveur, calé sur l'horloge (même pixel et même phase d'une page et d'un produit à l'autre, ADR 0015). `<FamilyLoader size="lg" />` : le H en ligne (une boucle de 2 s, `transform` et `opacity`). Dans un bouton : `Spinner` |
|
|
101
|
+
| `page-pending` | `createRouter({ routeTree, ...routerPendingDefaults })` (préchargement au survol, chargement après 50 ms, au moins 300 ms) ; `AppShell {...useRouterState({ select: shellNavigation, structuralSharing: true })}` : la zone de contenu se couvre dès qu'une navigation attend, la coque reste ; `PagePending` : marque la place d'une page qui attend, le H du document s'affiche |
|
|
102
|
+
| `skeleton` | `<Pending pending={isLoading} fallback={<TableSkeleton columns={3} />}>…</Pending>` : après 200 ms, tenu 400 ms au moins |
|
|
102
103
|
|
|
103
104
|
## Listes, journaux et retours
|
|
104
105
|
|
package/dist/app-shell.js
CHANGED
|
@@ -92,7 +92,7 @@ export function SidebarNavItem({ icon, label, active, href, render, }) {
|
|
|
92
92
|
href,
|
|
93
93
|
"aria-current": active ? "page" : undefined,
|
|
94
94
|
onClick: closeMobileNav,
|
|
95
|
-
className: cn("flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium whitespace-nowrap text-muted-foreground outline-none transition-colors duration-(--duration-fast) ease-out", "hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50", "aria-[current=page]:bg-muted aria-[current=page]:text-foreground data-[status=active]:bg-muted data-[status=active]:text-foreground", "lg:min-h-9 lg:gap-2.5 lg:py-2 lg:text-shell-muted lg:hover:bg-shell-hover lg:hover:text-shell-foreground", "lg:aria-[current=page]:bg-shell-active lg:aria-[current=page]:text-shell-foreground lg:data-[status=active]:bg-shell-active lg:data-[status=active]:text-shell-foreground", "lg:rail:justify-center lg:rail:px-0"),
|
|
95
|
+
className: cn("flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium whitespace-nowrap text-muted-foreground outline-none transition-colors duration-(--duration-fast) ease-out", "hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50", "aria-[current=page]:bg-muted aria-[current=page]:text-foreground data-[status=active]:bg-muted data-[status=active]:text-foreground", "min-h-11 lg:min-h-9 coarse:min-h-11 lg:gap-2.5 lg:py-2 lg:text-shell-muted lg:hover:bg-shell-hover lg:hover:text-shell-foreground", "lg:aria-[current=page]:bg-shell-active lg:aria-[current=page]:text-shell-foreground lg:data-[status=active]:bg-shell-active lg:data-[status=active]:text-shell-foreground", "lg:rail:justify-center lg:rail:px-0"),
|
|
96
96
|
children: (_jsxs(_Fragment, { children: [_jsx(HugeiconsIcon, { icon: icon, strokeWidth: 1.8, "aria-hidden": true, className: "size-5 shrink-0 lg:size-[1.125rem]" }), _jsx("span", { className: "truncate lg:rail:sr-only", children: label })] })),
|
|
97
97
|
},
|
|
98
98
|
});
|
|
@@ -104,7 +104,7 @@ export function SidebarNavItem({ icon, label, active, href, render, }) {
|
|
|
104
104
|
const COLLAPSE_ICON = PanelLeftOpenIcon;
|
|
105
105
|
const EXPAND_ICON = PanelLeftCloseIcon;
|
|
106
106
|
/** Même boîte qu'une entrée de la barre en rail : 40 × 36 px, icône de 18 px centrée. */
|
|
107
|
-
const TOGGLE = "relative grid h-9 w-10 shrink-0 place-items-center rounded-xl text-shell-muted outline-none transition-colors duration-(--duration-fast) ease-out hover:bg-shell-hover hover:text-shell-foreground focus-visible:ring-2 focus-visible:ring-ring/50";
|
|
107
|
+
const TOGGLE = "touch-target relative grid h-9 w-10 shrink-0 place-items-center rounded-xl text-shell-muted outline-none transition-colors duration-(--duration-fast) ease-out hover:bg-shell-hover hover:text-shell-foreground focus-visible:ring-2 focus-visible:ring-ring/50";
|
|
108
108
|
function ShortcutHint({ label }) {
|
|
109
109
|
return (_jsxs("span", { className: "flex items-center gap-2", children: [label, _jsxs(KbdGroup, { children: [_jsx(Kbd, { children: isApplePlatform() ? "⌘" : "Ctrl" }), _jsx(Kbd, { children: "B" })] })] }));
|
|
110
110
|
}
|
|
@@ -211,5 +211,5 @@ export function AppShell({ product, organization, productRail = true, accountHre
|
|
|
211
211
|
if (loading && contentRef.current)
|
|
212
212
|
contentRef.current.scrollTop = 0;
|
|
213
213
|
}, [loading]);
|
|
214
|
-
return (_jsx(ShellContext.Provider, { value: { closeMobileNav: () => setMenuOpen(false) }, children: _jsx(ProductRailShownContext, { value: productRail, children: _jsx(ShellLeaveContext, { value: leave, children: _jsxs("div", { "data-slot": "app-shell", "data-rail-peek": railPeek ? "" : undefined, "data-product-rail": productRail ? "" : undefined, "data-loading": loading ? "" : undefined, "data-hydrated": hydrated ? "" : undefined, className: cn("min-h-dvh lg:grid lg:h-dvh lg:bg-shell", productRail ? "lg:grid-cols-[auto_auto_minmax(0,1fr)]" : "lg:grid-cols-[auto_minmax(0,1fr)]"), children: [_jsx("a", { href: "#contenu", className: "sr-only z-50 rounded-xl bg-background px-4 py-2 text-sm font-medium focus:not-sr-only focus:fixed focus:top-3 focus:left-3 focus:ring-2 focus:ring-ring", children: "Aller au contenu" }), productRail ? (_jsx(ProductRail, { current: product, organization: organization, accountHref: accountHref, onExpandedChange: setRailPeek })) : null, _jsxs("aside", { "aria-label": "Barre lat\u00E9rale", className: "group/sidebar hidden overflow-hidden text-shell-foreground transition-[width] duration-(--duration-base) ease-ios motion-reduce:transition-none lg:flex lg:w-64 lg:flex-col lg:rail:w-16", children: [_jsx(SidebarHead, { product: product }), !productRail && organization ? _jsx("div", { className: "shrink-0 px-3 pb-1", children: organization }) : null, nav, sidebarFooter ? _jsx("div", { className: "grid gap-0.5 px-3 pt-1 pb-3", children: sidebarFooter }) : null] }), _jsx("div", { className: "flex min-h-dvh flex-col lg:min-h-0 lg:p-2 lg:pl-0", children: _jsxs("div", { ref: contentRef, className: "relative flex flex-1 flex-col lg:min-h-0 lg:overflow-y-auto lg:rounded-xl lg:bg-background lg:[scrollbar-gutter:stable]", children: [_jsxs("header", { className: "sticky top-[env(safe-area-inset-top,0px)] z-30 flex h-14 shrink-0 items-center gap-1 border-b border-foreground/5 bg-background px-2 sm:px-4 lg:top-0", children: [_jsx("button", { type: "button", onClick: () => setMenuOpen(true), "aria-label": "Ouvrir le menu", className: "touch-target grid size-10 shrink-0 place-items-center rounded-xl text-muted-foreground outline-none hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 lg:hidden", children: _jsx(HugeiconsIcon, { icon: Menu01Icon, "aria-hidden": true, className: "size-5" }) }), _jsxs("div", { "data-slot": "app-shell-brand", className: "flex shrink-0 items-center gap-2 pr-1 lg:hidden", children: [_jsx(ProductBadge, { product: product, className: "size-6" }), _jsx("span", { className: "font-heading text-[0.9375rem] font-semibold", children: productName(product) })] }), _jsx("div", { className: "flex min-w-0
|
|
214
|
+
return (_jsx(ShellContext.Provider, { value: { closeMobileNav: () => setMenuOpen(false) }, children: _jsx(ProductRailShownContext, { value: productRail, children: _jsx(ShellLeaveContext, { value: leave, children: _jsxs("div", { "data-slot": "app-shell", "data-rail-peek": railPeek ? "" : undefined, "data-product-rail": productRail ? "" : undefined, "data-loading": loading ? "" : undefined, "data-hydrated": hydrated ? "" : undefined, className: cn("min-h-dvh lg:grid lg:h-dvh lg:bg-shell", productRail ? "lg:grid-cols-[auto_auto_minmax(0,1fr)]" : "lg:grid-cols-[auto_minmax(0,1fr)]"), children: [_jsx("a", { href: "#contenu", className: "sr-only z-50 rounded-xl bg-background px-4 py-2 text-sm font-medium focus:not-sr-only focus:fixed focus:top-3 focus:left-3 focus:ring-2 focus:ring-ring", children: "Aller au contenu" }), productRail ? (_jsx(ProductRail, { current: product, organization: organization, accountHref: accountHref, onExpandedChange: setRailPeek })) : null, _jsxs("aside", { "aria-label": "Barre lat\u00E9rale", className: "group/sidebar hidden overflow-hidden text-shell-foreground transition-[width] duration-(--duration-base) ease-ios motion-reduce:transition-none lg:flex lg:w-64 lg:flex-col lg:rail:w-16", children: [_jsx(SidebarHead, { product: product }), !productRail && organization ? _jsx("div", { className: "shrink-0 px-3 pb-1", children: organization }) : null, nav, sidebarFooter ? _jsx("div", { className: "grid gap-0.5 px-3 pt-1 pb-3", children: sidebarFooter }) : null] }), _jsx("div", { className: "flex min-h-dvh flex-col lg:min-h-0 lg:p-2 lg:pl-0", children: _jsxs("div", { ref: contentRef, className: "relative flex flex-1 flex-col lg:min-h-0 lg:overflow-y-auto lg:rounded-xl lg:bg-background lg:[scrollbar-gutter:stable]", children: [_jsxs("header", { className: "sticky top-[env(safe-area-inset-top,0px)] z-30 flex min-h-14 shrink-0 flex-wrap items-center gap-x-1 gap-y-1 border-b border-foreground/5 bg-background px-2 py-1.5 sm:px-4 lg:top-0", children: [_jsx("button", { type: "button", onClick: () => setMenuOpen(true), "aria-label": "Ouvrir le menu", className: "touch-target grid size-10 shrink-0 place-items-center rounded-xl text-muted-foreground outline-none hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 lg:hidden", children: _jsx(HugeiconsIcon, { icon: Menu01Icon, "aria-hidden": true, className: "size-5" }) }), _jsxs("div", { "data-slot": "app-shell-brand", className: "flex shrink-0 items-center gap-2 pr-1 lg:hidden", children: [_jsx(ProductBadge, { product: product, className: "size-6" }), _jsx("span", { className: "font-heading text-[0.9375rem] font-semibold", children: productName(product) })] }), _jsx("div", { className: cn("flex min-w-0 items-center gap-2", header ? "flex-[1_1_7rem]" : "flex-1"), children: header }), onSearch ? (_jsx(CommandMenuButton, { onClick: onSearch, compact: "mobile", className: "shrink-0 lg:w-96 lg:max-w-none", ...(searchLabel !== undefined ? { label: searchLabel } : {}) })) : null] }), _jsx("div", { "data-slot": "app-shell-loading", "aria-hidden": !loading, className: CONTENT_LOADING_LAYER, children: loading ? (_jsx("span", { role: "status", className: "sr-only", children: "Chargement\u2026" })) : null }), _jsx("main", { id: "contenu", tabIndex: -1, "aria-busy": loading ? true : undefined, inert: loading || undefined, className: cn("mx-auto w-full max-w-6xl flex-1 px-4 pt-6 pb-16 outline-none sm:px-6 lg:px-10 lg:pt-8", mainClassName), children: _jsx(ContentLoadingContext, { value: registerPagePending, children: contentKey === undefined ? (children) : (_jsx(Suspense, { fallback: _jsx(PagePending, {}), children: children }, contentKey)) }) })] }) }), hasRoot ? null : _jsx(FamilyLoaderHost, {}), _jsx(Drawer, { side: "bottom", open: menuOpen, onOpenChange: setMenuOpen, children: _jsxs(DrawerContent, { className: "lg:hidden", children: [_jsx(DrawerTitle, { className: "sr-only", children: "Menu" }), organization ? _jsx("div", { className: "px-3 pt-1", children: organization }) : null, nav, sidebarFooter ? (_jsx("div", { className: "grid gap-0.5 border-t border-foreground/5 px-3 pt-2 pb-[max(env(safe-area-inset-bottom),0.75rem)]", children: sidebarFooter })) : null] }) })] }) }) }) }));
|
|
215
215
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export type AssistantToolStatus = "running" | "done" | "error";
|
|
3
|
+
export type AssistantTool = {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
status: AssistantToolStatus;
|
|
7
|
+
};
|
|
8
|
+
export type AssistantActionStatus = "pending" | "confirmed" | "cancelled" | "failed";
|
|
9
|
+
/** La liste des messages, dans l'ordre. */
|
|
10
|
+
export declare function AssistantConversation({ className, ...props }: React.ComponentProps<"ol">): React.JSX.Element;
|
|
11
|
+
/** Ce que la personne a écrit, en bulle à droite. */
|
|
12
|
+
export declare function AssistantUserMessage({ children }: {
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
}): React.JSX.Element;
|
|
15
|
+
/** Les outils consultés pendant une réponse ; une pastille tourne tant que l'outil travaille. */
|
|
16
|
+
export declare function AssistantToolChips({ tools, label, }: {
|
|
17
|
+
tools: ReadonlyArray<AssistantTool>;
|
|
18
|
+
label?: string;
|
|
19
|
+
}): React.JSX.Element | null;
|
|
20
|
+
/**
|
|
21
|
+
* Une réponse : pastilles, puis le texte rendu par le produit (`children`), puis les cartes.
|
|
22
|
+
* `waiting` : rien d'écrit encore, une ligne d'attente à la place du texte.
|
|
23
|
+
*/
|
|
24
|
+
export declare function AssistantReply({ tools, waiting, waitingLabel, error, actions, children, }: {
|
|
25
|
+
tools?: ReadonlyArray<AssistantTool>;
|
|
26
|
+
waiting?: boolean;
|
|
27
|
+
waitingLabel?: string;
|
|
28
|
+
/** Réponse en échec : le texte passe en couleur d'erreur. */
|
|
29
|
+
error?: boolean;
|
|
30
|
+
/** Cartes d'action rattachées à cette réponse. */
|
|
31
|
+
actions?: ReactNode;
|
|
32
|
+
children?: ReactNode;
|
|
33
|
+
}): React.JSX.Element;
|
|
34
|
+
/**
|
|
35
|
+
* Une écriture proposée par l'assistant, qui n'a lieu qu'au clic sur « Confirmer ». Une fois
|
|
36
|
+
* traitée, la carte garde son état (fait, annulé, échec et sa raison).
|
|
37
|
+
*/
|
|
38
|
+
export declare function AssistantActionCard({ title, details, status, result, onConfirm, onCancel, confirming, cancelling, confirmLabel, cancelLabel, }: {
|
|
39
|
+
title: ReactNode;
|
|
40
|
+
details?: ReadonlyArray<{
|
|
41
|
+
label: string;
|
|
42
|
+
value: string;
|
|
43
|
+
}>;
|
|
44
|
+
status: AssistantActionStatus;
|
|
45
|
+
/** Raison d'un échec. */
|
|
46
|
+
result?: string | null;
|
|
47
|
+
onConfirm: () => void;
|
|
48
|
+
onCancel: () => void;
|
|
49
|
+
confirming?: boolean;
|
|
50
|
+
cancelling?: boolean;
|
|
51
|
+
confirmLabel?: string;
|
|
52
|
+
cancelLabel?: string;
|
|
53
|
+
}): React.JSX.Element;
|
|
54
|
+
/** Des demandes d'exemple, pour une conversation vide : un clic remplit la saisie. */
|
|
55
|
+
export declare function AssistantExamples({ examples, onPick, disabled, label, }: {
|
|
56
|
+
examples: ReadonlyArray<string>;
|
|
57
|
+
onPick: (example: string) => void;
|
|
58
|
+
disabled?: boolean;
|
|
59
|
+
label?: string;
|
|
60
|
+
}): React.JSX.Element;
|
|
61
|
+
/**
|
|
62
|
+
* La saisie : Entrée envoie, Maj+Entrée va à la ligne (et rien pendant une composition IME).
|
|
63
|
+
* `busy` : une réponse est en cours, l'envoi attend.
|
|
64
|
+
*/
|
|
65
|
+
export declare function AssistantComposer({ value, onValueChange, onSubmit, disabled, busy, sending, label, placeholder, sendLabel, hint, maxLength, }: {
|
|
66
|
+
value: string;
|
|
67
|
+
onValueChange: (value: string) => void;
|
|
68
|
+
onSubmit: () => void;
|
|
69
|
+
disabled?: boolean;
|
|
70
|
+
busy?: boolean;
|
|
71
|
+
sending?: boolean;
|
|
72
|
+
label?: string;
|
|
73
|
+
placeholder?: string;
|
|
74
|
+
sendLabel?: string;
|
|
75
|
+
hint?: ReactNode;
|
|
76
|
+
maxLength?: number;
|
|
77
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import ArrowUp02Icon from "@hugeicons/core-free-icons/ArrowUp02Icon";
|
|
3
|
+
import Cancel01Icon from "@hugeicons/core-free-icons/Cancel01Icon";
|
|
4
|
+
import CheckmarkCircle02Icon from "@hugeicons/core-free-icons/CheckmarkCircle02Icon";
|
|
5
|
+
import { HugeiconsIcon } from "@hugeicons/react";
|
|
6
|
+
import { useId } from "react";
|
|
7
|
+
import { Badge } from "./badge.js";
|
|
8
|
+
import { Button } from "./button.js";
|
|
9
|
+
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "./card.js";
|
|
10
|
+
import { cn } from "./cn.js";
|
|
11
|
+
import { Spinner } from "./spinner.js";
|
|
12
|
+
import { Textarea } from "./textarea.js";
|
|
13
|
+
/** La liste des messages, dans l'ordre. */
|
|
14
|
+
export function AssistantConversation({ className, ...props }) {
|
|
15
|
+
return (_jsx("ol", { "data-slot": "assistant-conversation", "aria-label": "Conversation", className: cn("grid gap-6", className), ...props }));
|
|
16
|
+
}
|
|
17
|
+
/** Ce que la personne a écrit, en bulle à droite. */
|
|
18
|
+
export function AssistantUserMessage({ children }) {
|
|
19
|
+
return (_jsx("li", { "data-slot": "assistant-user-message", className: "flex min-w-0 justify-end", children: _jsx("p", { className: "max-w-[85%] rounded-2xl bg-muted px-4 py-2.5 text-sm break-words whitespace-pre-wrap", children: children }) }));
|
|
20
|
+
}
|
|
21
|
+
/** Les outils consultés pendant une réponse ; une pastille tourne tant que l'outil travaille. */
|
|
22
|
+
export function AssistantToolChips({ tools, label = "Ce que l'assistant a consulté", }) {
|
|
23
|
+
if (!tools.length)
|
|
24
|
+
return null;
|
|
25
|
+
return (_jsx("ul", { className: "flex flex-wrap gap-1.5", "aria-label": label, children: tools.map((tool) => (_jsx("li", { children: _jsxs(Badge, { tone: tool.status === "error" ? "destructive" : "neutral", children: [tool.status === "running" ? _jsx(Spinner, { size: 3 }) : null, tool.label] }) }, tool.id))) }));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Une réponse : pastilles, puis le texte rendu par le produit (`children`), puis les cartes.
|
|
29
|
+
* `waiting` : rien d'écrit encore, une ligne d'attente à la place du texte.
|
|
30
|
+
*/
|
|
31
|
+
export function AssistantReply({ tools = [], waiting = false, waitingLabel = "L'assistant réfléchit…", error = false, actions, children, }) {
|
|
32
|
+
return (_jsxs("li", { "data-slot": "assistant-reply", className: "grid min-w-0 gap-3", children: [_jsx(AssistantToolChips, { tools: tools }), waiting ? (_jsxs("p", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [_jsx(Spinner, { size: 4 }), waitingLabel] })) : (_jsx("div", { className: cn("text-sm leading-relaxed break-words", error && "text-destructive"), children: children })), actions] }));
|
|
33
|
+
}
|
|
34
|
+
const ACTION_BADGE = {
|
|
35
|
+
confirmed: { tone: "success", label: "Fait" },
|
|
36
|
+
failed: { tone: "destructive", label: "Échec" },
|
|
37
|
+
cancelled: { tone: "neutral", label: "Annulé" },
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Une écriture proposée par l'assistant, qui n'a lieu qu'au clic sur « Confirmer ». Une fois
|
|
41
|
+
* traitée, la carte garde son état (fait, annulé, échec et sa raison).
|
|
42
|
+
*/
|
|
43
|
+
export function AssistantActionCard({ title, details = [], status, result, onConfirm, onCancel, confirming = false, cancelling = false, confirmLabel = "Confirmer", cancelLabel = "Annuler", }) {
|
|
44
|
+
const badge = status === "pending" ? null : ACTION_BADGE[status];
|
|
45
|
+
return (_jsxs(Card, { "data-slot": "assistant-action-card", className: "max-w-xl", children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: title }) }), details.length ? (_jsx(CardContent, { children: _jsx("dl", { className: "grid gap-2 text-sm", children: details.map((row) => (_jsxs("div", { className: "grid gap-0.5", children: [_jsx("dt", { className: "text-muted-foreground", children: row.label }), _jsx("dd", { className: "break-words whitespace-pre-wrap", children: row.value })] }, row.label))) }) })) : null, _jsx(CardFooter, { className: "justify-start", children: badge ? (_jsx(Badge, { tone: badge.tone, children: status === "failed" && result ? `${badge.label} : ${result}` : badge.label })) : (_jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsxs(Button, { size: "sm", isLoading: confirming, disabled: cancelling, onClick: onConfirm, children: [_jsx(HugeiconsIcon, { icon: CheckmarkCircle02Icon, "data-icon": "inline-start" }), confirmLabel] }), _jsxs(Button, { size: "sm", variant: "ghost", isLoading: cancelling, disabled: confirming, onClick: onCancel, children: [_jsx(HugeiconsIcon, { icon: Cancel01Icon, "data-icon": "inline-start" }), cancelLabel] })] })) })] }));
|
|
46
|
+
}
|
|
47
|
+
/** Des demandes d'exemple, pour une conversation vide : un clic remplit la saisie. */
|
|
48
|
+
export function AssistantExamples({ examples, onPick, disabled = false, label = "Par exemple :", }) {
|
|
49
|
+
return (_jsxs("div", { className: "grid gap-2", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: label }), _jsx("ul", { className: "flex flex-wrap gap-2", children: examples.map((example) => (_jsx("li", { children: _jsx(Button, { size: "sm", variant: "outline", disabled: disabled, onClick: () => onPick(example), children: example }) }, example))) })] }));
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* La saisie : Entrée envoie, Maj+Entrée va à la ligne (et rien pendant une composition IME).
|
|
53
|
+
* `busy` : une réponse est en cours, l'envoi attend.
|
|
54
|
+
*/
|
|
55
|
+
export function AssistantComposer({ value, onValueChange, onSubmit, disabled = false, busy = false, sending = false, label = "Ta demande", placeholder = "Demande quelque chose à l'assistant…", sendLabel = "Envoyer", hint = "Entrée pour envoyer, Maj+Entrée pour aller à la ligne.", maxLength = 4000, }) {
|
|
56
|
+
const fieldId = useId();
|
|
57
|
+
const canSend = !disabled && !busy && !sending && value.trim().length > 0;
|
|
58
|
+
function submit(event) {
|
|
59
|
+
event?.preventDefault();
|
|
60
|
+
if (canSend)
|
|
61
|
+
onSubmit();
|
|
62
|
+
}
|
|
63
|
+
function onKeyDown(event) {
|
|
64
|
+
if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
|
|
65
|
+
event.preventDefault();
|
|
66
|
+
submit();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return (_jsxs("form", { "data-slot": "assistant-composer", onSubmit: submit, className: "grid gap-2", children: [_jsx("label", { htmlFor: fieldId, className: "sr-only", children: label }), _jsxs("div", { className: "relative", children: [_jsx(Textarea, { id: fieldId, value: value, onChange: (event) => onValueChange(event.target.value), onKeyDown: onKeyDown, rows: 3, maxLength: maxLength, disabled: disabled, placeholder: placeholder, className: "pr-14" }), _jsx(Button, { type: "submit", size: "icon", "aria-label": sendLabel, className: "absolute right-2 bottom-2", disabled: !canSend, isLoading: sending, children: _jsx(HugeiconsIcon, { icon: ArrowUp02Icon }) })] }), hint ? _jsx("p", { className: "text-xs text-muted-foreground", children: hint }) : null] }));
|
|
70
|
+
}
|
package/dist/board.d.ts
CHANGED
|
@@ -14,6 +14,15 @@ export type BoardMove = {
|
|
|
14
14
|
toColumnId: string;
|
|
15
15
|
toIndex: number;
|
|
16
16
|
};
|
|
17
|
+
/**
|
|
18
|
+
* Défilement d'une image quand le pointeur tient une carte en `x` : négatif vers la gauche, positif
|
|
19
|
+
* vers la droite, nul hors des bandes de bord. Le bord est celui de la partie visible du tableau
|
|
20
|
+
* (`rect` rogné à l'écran) : sur téléphone, le tableau déborde de la page.
|
|
21
|
+
*/
|
|
22
|
+
export declare function edgeScrollSpeed(x: number, rect: {
|
|
23
|
+
left: number;
|
|
24
|
+
right: number;
|
|
25
|
+
}, viewportWidth: number): number;
|
|
17
26
|
/**
|
|
18
27
|
* Tableau de colonnes (Kanban) dont les cartes se déplacent à la souris, au doigt ou au clavier,
|
|
19
28
|
* par leur poignée (`handle`, à placer dans la carte) : le reste de la carte garde ses clics et le
|
package/dist/board.js
CHANGED
|
@@ -1,8 +1,50 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useId, useRef, useState } from "react";
|
|
2
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
3
3
|
import { cn } from "./cn.js";
|
|
4
4
|
/** Distance avant qu'un appui devienne un glisser : un clic reste un clic. */
|
|
5
5
|
const DRAG_THRESHOLD = 6;
|
|
6
|
+
/** Bande, près du bord visible du tableau, où une carte tenue fait défiler les colonnes. */
|
|
7
|
+
const EDGE_ZONE = 56;
|
|
8
|
+
/** Vitesse au ras du bord, en pixels par image ; elle croît à mesure que le doigt s'en approche. */
|
|
9
|
+
const EDGE_MAX_SPEED = 14;
|
|
10
|
+
/**
|
|
11
|
+
* Défilement d'une image quand le pointeur tient une carte en `x` : négatif vers la gauche, positif
|
|
12
|
+
* vers la droite, nul hors des bandes de bord. Le bord est celui de la partie visible du tableau
|
|
13
|
+
* (`rect` rogné à l'écran) : sur téléphone, le tableau déborde de la page.
|
|
14
|
+
*/
|
|
15
|
+
export function edgeScrollSpeed(x, rect, viewportWidth) {
|
|
16
|
+
const left = Math.max(rect.left, 0);
|
|
17
|
+
const right = Math.min(rect.right, viewportWidth);
|
|
18
|
+
const width = right - left;
|
|
19
|
+
if (width <= 0)
|
|
20
|
+
return 0;
|
|
21
|
+
const zone = Math.min(EDGE_ZONE, width / 4);
|
|
22
|
+
if (x < left + zone)
|
|
23
|
+
return -Math.ceil(EDGE_MAX_SPEED * Math.min(1, (left + zone - x) / zone));
|
|
24
|
+
if (x > right - zone)
|
|
25
|
+
return Math.ceil(EDGE_MAX_SPEED * Math.min(1, (x - (right - zone)) / zone));
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
/** Centre horizontal de la carte fantôme, gardée dans l'écran avec une marge de 8 px. */
|
|
29
|
+
function clampGhostX(x, width) {
|
|
30
|
+
const half = width / 2 + 8;
|
|
31
|
+
const max = window.innerWidth - half;
|
|
32
|
+
return max < half ? window.innerWidth / 2 : Math.min(Math.max(x, half), max);
|
|
33
|
+
}
|
|
34
|
+
/** Colonne et place sous le pointeur, d'après le milieu des cartes. */
|
|
35
|
+
function positionAt(x, y, cardId) {
|
|
36
|
+
const element = document.elementFromPoint(x, y);
|
|
37
|
+
const column = element?.closest("[data-board-column]");
|
|
38
|
+
const columnId = column?.dataset.boardColumn;
|
|
39
|
+
if (!column || !columnId)
|
|
40
|
+
return null;
|
|
41
|
+
const cards = [...column.querySelectorAll("[data-board-card]")].filter((node) => node.dataset.boardCard !== cardId);
|
|
42
|
+
const index = cards.findIndex((node) => {
|
|
43
|
+
const rect = node.getBoundingClientRect();
|
|
44
|
+
return y < rect.top + rect.height / 2;
|
|
45
|
+
});
|
|
46
|
+
return { columnId, index: index === -1 ? cards.length : index };
|
|
47
|
+
}
|
|
6
48
|
function moveInColumns(columns, cardKey, grab) {
|
|
7
49
|
const base = columns.map((column) => ({ column, cards: [...column.cards] }));
|
|
8
50
|
if (!grab)
|
|
@@ -33,6 +75,10 @@ export function Board({ label, columns, cardKey, cardLabel, renderCard, onMove,
|
|
|
33
75
|
const [announcement, setAnnouncement] = useState("");
|
|
34
76
|
const [ghost, setGhost] = useState(null);
|
|
35
77
|
const pressed = useRef(null);
|
|
78
|
+
const scrollerRef = useRef(null);
|
|
79
|
+
const lastPoint = useRef(null);
|
|
80
|
+
const scrollFrame = useRef(0);
|
|
81
|
+
useEffect(() => () => cancelAnimationFrame(scrollFrame.current), []);
|
|
36
82
|
const instructionsId = useId();
|
|
37
83
|
const view = moveInColumns(columns, cardKey, grab);
|
|
38
84
|
const byId = new Map(columns.flatMap((column) => column.cards.map((card) => [cardKey(card), card])));
|
|
@@ -64,6 +110,7 @@ export function Board({ label, columns, cardKey, cardLabel, renderCard, onMove,
|
|
|
64
110
|
: `${cardLabel(card)} : déplacement annulé.`
|
|
65
111
|
: "");
|
|
66
112
|
const id = grab.cardId;
|
|
113
|
+
stopAutoScroll();
|
|
67
114
|
setGrab(null);
|
|
68
115
|
setGhost(null);
|
|
69
116
|
if (grab.via === "keyboard")
|
|
@@ -114,19 +161,41 @@ export function Board({ label, columns, cardKey, cardLabel, renderCard, onMove,
|
|
|
114
161
|
setAnnouncement(where(next));
|
|
115
162
|
focusCard(id);
|
|
116
163
|
}
|
|
117
|
-
/**
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
164
|
+
/**
|
|
165
|
+
* Défilement automatique : une carte tenue près du bord visible fait défiler le tableau, image
|
|
166
|
+
* par image, tant que le doigt y reste. Sans lui, sur téléphone, seule la colonne voisine était
|
|
167
|
+
* atteignable (2026-10-18). La boucle s'arrête d'elle-même hors des bords ou en bout de course.
|
|
168
|
+
*/
|
|
169
|
+
function autoScroll() {
|
|
170
|
+
if (scrollFrame.current !== 0)
|
|
171
|
+
return;
|
|
172
|
+
const step = () => {
|
|
173
|
+
scrollFrame.current = 0;
|
|
174
|
+
const scroller = scrollerRef.current;
|
|
175
|
+
const press = pressed.current;
|
|
176
|
+
const point = lastPoint.current;
|
|
177
|
+
if (!scroller || !press || !point)
|
|
178
|
+
return;
|
|
179
|
+
const speed = edgeScrollSpeed(point.x, scroller.getBoundingClientRect(), window.innerWidth);
|
|
180
|
+
if (speed === 0)
|
|
181
|
+
return;
|
|
182
|
+
const before = scroller.scrollLeft;
|
|
183
|
+
scroller.scrollLeft = before + speed;
|
|
184
|
+
if (scroller.scrollLeft === before)
|
|
185
|
+
return;
|
|
186
|
+
// Les colonnes ont glissé sous le doigt immobile : la place visée a changé.
|
|
187
|
+
const next = positionAt(point.x, point.y, press.cardId);
|
|
188
|
+
if (next)
|
|
189
|
+
setGrab((current) => (current ? { ...current, to: next } : current));
|
|
190
|
+
scrollFrame.current = requestAnimationFrame(step);
|
|
191
|
+
};
|
|
192
|
+
scrollFrame.current = requestAnimationFrame(step);
|
|
193
|
+
}
|
|
194
|
+
function stopAutoScroll() {
|
|
195
|
+
if (scrollFrame.current !== 0)
|
|
196
|
+
cancelAnimationFrame(scrollFrame.current);
|
|
197
|
+
scrollFrame.current = 0;
|
|
198
|
+
lastPoint.current = null;
|
|
130
199
|
}
|
|
131
200
|
function onPointerDown(event, card, position) {
|
|
132
201
|
if (event.button !== 0 || !canMove(card))
|
|
@@ -158,6 +227,8 @@ export function Board({ label, columns, cardKey, cardLabel, renderCard, onMove,
|
|
|
158
227
|
const next = positionAt(event.clientX, event.clientY, press.cardId);
|
|
159
228
|
if (next)
|
|
160
229
|
setGrab((current) => (current ? { ...current, to: next } : current));
|
|
230
|
+
lastPoint.current = { x: event.clientX, y: event.clientY };
|
|
231
|
+
autoScroll();
|
|
161
232
|
}
|
|
162
233
|
function onPointerUp(event) {
|
|
163
234
|
const press = pressed.current;
|
|
@@ -168,7 +239,9 @@ export function Board({ label, columns, cardKey, cardLabel, renderCard, onMove,
|
|
|
168
239
|
finish(true);
|
|
169
240
|
}
|
|
170
241
|
const ghostCard = grab?.via === "pointer" ? byId.get(grab.cardId) : undefined;
|
|
171
|
-
return (_jsxs("div", { className: cn("grid gap-2", className), children: [_jsx("p", { id: instructionsId, className: "sr-only", children: "Espace ou Entr\u00E9e pour saisir une carte, fl\u00E8ches pour la d\u00E9placer, Espace pour la poser, \u00C9chap pour annuler." }), _jsx("div", { role: "region", "aria-label": label, className: "-mx-1 flex snap-x snap-mandatory gap-3 overflow-x-auto px-1 pb-2 sm:snap-none",
|
|
242
|
+
return (_jsxs("div", { className: cn("grid gap-2", className), children: [_jsx("p", { id: instructionsId, className: "sr-only", children: "Espace ou Entr\u00E9e pour saisir une carte, fl\u00E8ches pour la d\u00E9placer, Espace pour la poser, \u00C9chap pour annuler." }), _jsx("div", { ref: scrollerRef, role: "region", "aria-label": label, className: cn("-mx-1 flex snap-x snap-mandatory gap-3 overflow-x-auto px-1 pb-2 sm:snap-none",
|
|
243
|
+
// L'aimantation ramènerait le tableau à la colonne de départ après chaque pas du défilement automatique.
|
|
244
|
+
ghostCard !== undefined && "snap-none"), children: view.map(({ column, cards }) => (_jsxs("section", { "data-board-column": column.id, "aria-label": column.label, className: cn("grid w-[85vw] max-w-80 shrink-0 snap-start content-start gap-2 rounded-3xl bg-foreground/[0.04] p-2 sm:w-72", grab?.to.columnId === column.id && "ring-2 ring-ring/40"), children: [_jsx("header", { className: "flex items-center justify-between gap-2 px-2 pt-1 text-sm font-medium", children: column.title }), _jsx("ul", { className: "grid min-h-12 gap-2", children: cards.map((card, index) => {
|
|
172
245
|
const id = cardKey(card);
|
|
173
246
|
const dragging = grab?.cardId === id;
|
|
174
247
|
const movable = canMove(card);
|
|
@@ -178,7 +251,9 @@ export function Board({ label, columns, cardKey, cardLabel, renderCard, onMove,
|
|
|
178
251
|
pressed.current = null;
|
|
179
252
|
if (grab)
|
|
180
253
|
finish(false);
|
|
181
|
-
}, className: "inline-flex size-7 shrink-0 cursor-grab touch-none items-center justify-center rounded-lg text-muted-foreground outline-none hover:bg-foreground/[0.06] focus-visible:ring-2 focus-visible:ring-ring active:cursor-grabbing", children: _jsxs("svg", { viewBox: "0 0 16 16", "aria-hidden": true, className: "size-4 fill-current", children: [_jsx("circle", { cx: "5.5", cy: "3.5", r: "1.25" }), _jsx("circle", { cx: "10.5", cy: "3.5", r: "1.25" }), _jsx("circle", { cx: "5.5", cy: "8", r: "1.25" }), _jsx("circle", { cx: "10.5", cy: "8", r: "1.25" }), _jsx("circle", { cx: "5.5", cy: "12.5", r: "1.25" }), _jsx("circle", { cx: "10.5", cy: "12.5", r: "1.25" })] }) })) : null,
|
|
254
|
+
}, className: "touch-target inline-flex size-7 shrink-0 cursor-grab touch-none items-center justify-center rounded-lg text-muted-foreground outline-none hover:bg-foreground/[0.06] focus-visible:ring-2 focus-visible:ring-ring active:cursor-grabbing", children: _jsxs("svg", { viewBox: "0 0 16 16", "aria-hidden": true, className: "size-4 fill-current", children: [_jsx("circle", { cx: "5.5", cy: "3.5", r: "1.25" }), _jsx("circle", { cx: "10.5", cy: "3.5", r: "1.25" }), _jsx("circle", { cx: "5.5", cy: "8", r: "1.25" }), _jsx("circle", { cx: "10.5", cy: "8", r: "1.25" }), _jsx("circle", { cx: "5.5", cy: "12.5", r: "1.25" }), _jsx("circle", { cx: "10.5", cy: "12.5", r: "1.25" })] }) })) : null,
|
|
182
255
|
}) }, id));
|
|
183
|
-
}) }), column.footer] }, column.id))) }), ghostCard !== undefined && ghost ? (_jsx("div", { "aria-hidden": true, className: "pointer-events-none fixed z-50 -translate-x-1/2 -translate-y-1/2 rotate-1 opacity-90",
|
|
256
|
+
}) }), column.footer] }, column.id))) }), ghostCard !== undefined && ghost ? (_jsx("div", { "aria-hidden": true, className: "pointer-events-none fixed z-50 -translate-x-1/2 -translate-y-1/2 rotate-1 opacity-90",
|
|
257
|
+
// Tenue dans l'écran : une carte de 85 vw centrée sur un doigt au bord débordait de moitié.
|
|
258
|
+
style: { left: clampGhostX(ghost.x, ghost.width), top: ghost.y, width: ghost.width }, children: renderCard(ghostCard, { dragging: true, handle: null }) })) : null, _jsx("div", { "aria-live": "assertive", className: "sr-only", children: announcement })] }));
|
|
184
259
|
}
|
package/dist/command-menu.js
CHANGED
|
@@ -64,7 +64,7 @@ export function CommandMenu({ groups, open: openProp, onOpenChange, placeholder
|
|
|
64
64
|
return (_jsx(DialogPrimitive.Root, { open: open, onOpenChange: setOpen, children: _jsxs(DialogPrimitive.Portal, { children: [_jsx(DialogPrimitive.Backdrop, { className: "fixed inset-0 z-50 bg-foreground/30 dark:bg-black/60" }), _jsx(DialogPrimitive.Viewport, { className: "fixed inset-0 z-50 flex items-start justify-center px-2 pt-[min(12dvh,6rem)] pb-2", children: _jsx(DialogPrimitive.Popup, { "data-slot": "command-menu", "aria-label": "Palette de commandes", className: cn(FLOATING_SURFACE, "flex max-h-[min(36rem,calc(100dvh-2rem))] w-full max-w-xl flex-col overflow-hidden rounded-3xl shadow-2xl shadow-black/10 outline-none"), children: _jsxs(Autocomplete.Root, { open: true, inline: true, items: visibleGroups, itemToStringValue: (command) => command.label, filter: (command, query) => matchCommand(command, query), autoHighlight: "always", keepHighlight: true, children: [_jsxs(Autocomplete.InputGroup, { className: "flex items-center gap-3 border-b border-foreground/5 px-4", children: [_jsx(HugeiconsIcon, { icon: Search01Icon, strokeWidth: 2, "aria-hidden": true, className: "size-4.5 shrink-0 text-muted-foreground" }), _jsx(Autocomplete.Input, { "aria-label": "Rechercher une commande", placeholder: placeholder, className: "h-14 w-full min-w-0 bg-transparent text-base outline-none placeholder:text-muted-foreground" }), _jsx(DialogPrimitive.Close, { className: "sr-only", children: "Fermer la palette" })] }), _jsxs("div", { className: "min-h-0 flex-1 overflow-y-auto overscroll-contain p-1.5 [scroll-padding-block:0.375rem]", children: [_jsx(Autocomplete.Empty, { className: "px-3 py-10 text-center text-sm text-muted-foreground empty:hidden", children: emptyLabel }), _jsx(Autocomplete.List, { children: (group) => (_jsxs(Autocomplete.Group, { items: group.items, className: "not-last:mb-1.5", children: [_jsx(Autocomplete.GroupLabel, { className: "px-3 pt-2 pb-1.5 text-xs font-medium text-muted-foreground", children: group.label }), _jsx(Autocomplete.Collection, { children: (command) => (_jsxs(Autocomplete.Item, { value: command, onClick: () => {
|
|
65
65
|
setOpen(false);
|
|
66
66
|
command.onSelect();
|
|
67
|
-
}, className: "flex min-h-11 cursor-default items-center gap-3 rounded-xl px-3 py-2 text-sm font-medium outline-none select-none data-highlighted:bg-muted lg:min-h-10", children: [command.badge ? (_jsx(ProductBadge, { product: command.badge, className: "size-4.5" })) : command.icon ? (_jsx(HugeiconsIcon, { icon: command.icon, strokeWidth: 1.8, "aria-hidden": true, className: "size-4.5 shrink-0 text-muted-foreground" })) : null, _jsx("span", { className: "min-w-0 flex-1 truncate", children: command.label }), command.hint ? (_jsx("span", { className: "shrink-0 truncate text-xs font-normal text-muted-foreground", children: command.hint })) : null, command.shortcut ? (_jsx(KbdGroup, { className: "hidden shrink-0 sm:inline-flex", children: command.shortcut.map((key) => (_jsx(Kbd, { children: key }, key))) })) : null] }, command.id)) })] }, group.label)) })] }), _jsxs("div", { className: "hidden items-center gap-4 border-t border-foreground/5 px-4 py-2.5 text-xs text-muted-foreground sm:flex", children: [_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsxs(KbdGroup, { children: [_jsx(Kbd, { children: "\u2191" }), _jsx(Kbd, { children: "\u2193" })] }), "Choisir"] }), _jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx(Kbd, { children: "Entr\u00E9e" }), "Ouvrir"] }), _jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx(Kbd, { children: "\u00C9chap" }), "Fermer"] })] })] }) }) })] }) }));
|
|
67
|
+
}, className: "flex min-h-11 cursor-default items-center gap-3 rounded-xl px-3 py-2 text-sm font-medium outline-none select-none data-highlighted:bg-muted lg:min-h-10 coarse:min-h-11", children: [command.badge ? (_jsx(ProductBadge, { product: command.badge, className: "size-4.5" })) : command.icon ? (_jsx(HugeiconsIcon, { icon: command.icon, strokeWidth: 1.8, "aria-hidden": true, className: "size-4.5 shrink-0 text-muted-foreground" })) : null, _jsx("span", { className: "min-w-0 flex-1 truncate", children: command.label }), command.hint ? (_jsx("span", { className: "shrink-0 truncate text-xs font-normal text-muted-foreground", children: command.hint })) : null, command.shortcut ? (_jsx(KbdGroup, { className: "hidden shrink-0 sm:inline-flex", children: command.shortcut.map((key) => (_jsx(Kbd, { children: key }, key))) })) : null] }, command.id)) })] }, group.label)) })] }), _jsxs("div", { className: "hidden items-center gap-4 border-t border-foreground/5 px-4 py-2.5 text-xs text-muted-foreground sm:flex", children: [_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsxs(KbdGroup, { children: [_jsx(Kbd, { children: "\u2191" }), _jsx(Kbd, { children: "\u2193" })] }), "Choisir"] }), _jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx(Kbd, { children: "Entr\u00E9e" }), "Ouvrir"] }), _jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx(Kbd, { children: "\u00C9chap" }), "Fermer"] })] })] }) }) })] }) }));
|
|
68
68
|
}
|
|
69
69
|
function noop() { }
|
|
70
70
|
function subscribeNever() {
|
|
@@ -113,6 +113,6 @@ export function DateRangePicker({ value, onChange, presets = DEFAULT_DATE_RANGE_
|
|
|
113
113
|
return (_jsx("li", { children: _jsx("button", { type: "button", "aria-pressed": pressed, onClick: () => {
|
|
114
114
|
onChange(preset.range(today), preset.key);
|
|
115
115
|
setOpen(false);
|
|
116
|
-
}, className: cn("flex min-h-
|
|
116
|
+
}, className: cn("flex min-h-11 w-full items-center rounded-xl px-3 text-left text-sm outline-none transition-colors duration-(--duration-fast) ease-out hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring/50 lg:min-h-9 coarse:min-h-11", pressed && "bg-muted font-medium"), children: preset.label }) }, preset.key));
|
|
117
117
|
}) }), _jsxs("form", { onSubmit: apply, noValidate: true, className: "grid content-start gap-3 p-4", children: [_jsx("p", { className: "text-sm font-medium", children: "P\u00E9riode libre" }), _jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: `${id}-from`, children: "Du" }), _jsx(Input, { id: `${id}-from`, type: "date", value: draft.from, ...(min ? { min } : {}), max: upper, "aria-invalid": error ? true : undefined, "aria-describedby": error ? `${id}-error` : undefined, onChange: (event) => setDraft((d) => ({ ...d, from: event.target.value })) })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: `${id}-to`, children: "Au" }), _jsx(Input, { id: `${id}-to`, type: "date", value: draft.to, ...(min ? { min } : {}), max: upper, "aria-invalid": error ? true : undefined, "aria-describedby": error ? `${id}-error` : undefined, onChange: (event) => setDraft((d) => ({ ...d, to: event.target.value })) })] })] }), error ? (_jsx("p", { id: `${id}-error`, role: "alert", className: "soft-in text-sm text-destructive", children: error })) : null, _jsx(Button, { type: "submit", size: "sm", className: "justify-self-end", children: "Appliquer" })] })] }) })] }));
|
|
118
118
|
}
|
package/dist/dropdown-menu.js
CHANGED
|
@@ -34,19 +34,19 @@ function DropdownMenuLabel({ className, inset, ...props }) {
|
|
|
34
34
|
return (_jsx(MenuPrimitive.GroupLabel, { "data-slot": "dropdown-menu-label", "data-inset": inset, className: cn("px-3 py-2.5 text-xs text-muted-foreground data-inset:pl-9.5", className), ...props }));
|
|
35
35
|
}
|
|
36
36
|
function DropdownMenuItem({ className, inset, variant = "default", ...props }) {
|
|
37
|
-
return (_jsx(MenuPrimitive.Item, { "data-slot": "dropdown-menu-item", "data-inset": inset, "data-variant": variant, className: cn("group/dropdown-menu-item relative flex cursor-default items-center gap-2.5 rounded-xl px-3 py-2 text-sm font-medium outline-hidden select-none focus:bg-muted focus:text-foreground data-inset:pl-9.5 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive max-lg:min-h-11 dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive", className), ...props }));
|
|
37
|
+
return (_jsx(MenuPrimitive.Item, { "data-slot": "dropdown-menu-item", "data-inset": inset, "data-variant": variant, className: cn("group/dropdown-menu-item relative flex cursor-default items-center gap-2.5 rounded-xl px-3 py-2 text-sm font-medium outline-hidden select-none focus:bg-muted focus:text-foreground data-inset:pl-9.5 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive max-lg:min-h-11 coarse:min-h-11 dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive", className), ...props }));
|
|
38
38
|
}
|
|
39
39
|
function DropdownMenuSub({ ...props }) {
|
|
40
40
|
return _jsx(MenuPrimitive.SubmenuRoot, { "data-slot": "dropdown-menu-sub", ...props });
|
|
41
41
|
}
|
|
42
42
|
function DropdownMenuSubTrigger({ className, inset, children, ...props }) {
|
|
43
|
-
return (_jsxs(MenuPrimitive.SubmenuTrigger, { "data-slot": "dropdown-menu-sub-trigger", "data-inset": inset, className: cn("flex cursor-default items-center gap-2 rounded-xl px-3 py-2 text-sm font-medium outline-hidden select-none focus:bg-muted focus:text-foreground data-inset:pl-9.5 data-popup-open:bg-muted data-popup-open:text-foreground data-open:bg-muted data-open:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className), ...props, children: [children, _jsx(HugeiconsIcon, { icon: ArrowRight01Icon, strokeWidth: 2, className: "ml-auto" })] }));
|
|
43
|
+
return (_jsxs(MenuPrimitive.SubmenuTrigger, { "data-slot": "dropdown-menu-sub-trigger", "data-inset": inset, className: cn("flex cursor-default items-center gap-2 rounded-xl px-3 py-2 text-sm font-medium outline-hidden select-none focus:bg-muted focus:text-foreground data-inset:pl-9.5 max-lg:min-h-11 coarse:min-h-11 data-popup-open:bg-muted data-popup-open:text-foreground data-open:bg-muted data-open:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className), ...props, children: [children, _jsx(HugeiconsIcon, { icon: ArrowRight01Icon, strokeWidth: 2, className: "ml-auto" })] }));
|
|
44
44
|
}
|
|
45
45
|
function DropdownMenuSubContent({ align = "start", alignOffset = -3, side = "right", sideOffset = 0, className, ...props }) {
|
|
46
46
|
return (_jsx(DropdownMenuContent, { "data-slot": "dropdown-menu-sub-content", collisionAvoidance: FLOATING_SUBMENU_COLLISION_AVOIDANCE, className: cn("relative w-auto min-w-36 rounded-2xl p-1.5 **:data-[slot$=-item]:focus:bg-muted **:data-[slot$=-item]:data-highlighted:bg-muted **:data-[slot$=-separator]:bg-foreground/5 **:data-[slot$=-trigger]:focus:bg-muted **:data-[slot$=-trigger]:aria-expanded:bg-muted! **:data-[variant=destructive]:focus:bg-destructive/10!", className), align: align, alignOffset: alignOffset, side: side, sideOffset: sideOffset, ...props }));
|
|
47
47
|
}
|
|
48
48
|
function DropdownMenuCheckboxItem({ className, children, checked, inset, ...props }) {
|
|
49
|
-
return (_jsxs(MenuPrimitive.CheckboxItem, { "data-slot": "dropdown-menu-checkbox-item", "data-inset": inset, 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 focus:text-foreground data-inset:pl-9.5 max-lg:min-h-11 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className), checked: checked, ...props, children: [_jsx("span", { className: "pointer-events-none absolute right-2 flex items-center justify-center", "data-slot": "dropdown-menu-checkbox-item-indicator", children: _jsx(MenuPrimitive.CheckboxItemIndicator, { children: _jsx(HugeiconsIcon, { icon: Tick02Icon, strokeWidth: 2 }) }) }), children] }));
|
|
49
|
+
return (_jsxs(MenuPrimitive.CheckboxItem, { "data-slot": "dropdown-menu-checkbox-item", "data-inset": inset, 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 focus:text-foreground data-inset:pl-9.5 max-lg:min-h-11 coarse:min-h-11 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className), checked: checked, ...props, children: [_jsx("span", { className: "pointer-events-none absolute right-2 flex items-center justify-center", "data-slot": "dropdown-menu-checkbox-item-indicator", children: _jsx(MenuPrimitive.CheckboxItemIndicator, { children: _jsx(HugeiconsIcon, { icon: Tick02Icon, strokeWidth: 2 }) }) }), children] }));
|
|
50
50
|
}
|
|
51
51
|
/**
|
|
52
52
|
* `label` titre le groupe. `Menu.RadioGroup` ne pose pas le contexte de
|
|
@@ -60,7 +60,7 @@ function DropdownMenuRadioGroup({ label, ...props }) {
|
|
|
60
60
|
return (_jsxs(DropdownMenuGroup, { children: [_jsx(DropdownMenuLabel, { children: label }), group] }));
|
|
61
61
|
}
|
|
62
62
|
function DropdownMenuRadioItem({ className, children, inset, ...props }) {
|
|
63
|
-
return (_jsxs(MenuPrimitive.RadioItem, { "data-slot": "dropdown-menu-radio-item", "data-inset": inset, 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 focus:text-foreground data-inset:pl-9.5 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("span", { className: "pointer-events-none absolute right-2 flex items-center justify-center", "data-slot": "dropdown-menu-radio-item-indicator", children: _jsx(MenuPrimitive.RadioItemIndicator, { children: _jsx(HugeiconsIcon, { icon: Tick02Icon, strokeWidth: 2 }) }) }), children] }));
|
|
63
|
+
return (_jsxs(MenuPrimitive.RadioItem, { "data-slot": "dropdown-menu-radio-item", "data-inset": inset, 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 focus:text-foreground data-inset:pl-9.5 max-lg:min-h-11 coarse:min-h-11 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("span", { className: "pointer-events-none absolute right-2 flex items-center justify-center", "data-slot": "dropdown-menu-radio-item-indicator", children: _jsx(MenuPrimitive.RadioItemIndicator, { children: _jsx(HugeiconsIcon, { icon: Tick02Icon, strokeWidth: 2 }) }) }), children] }));
|
|
64
64
|
}
|
|
65
65
|
function DropdownMenuSeparator({ className, ...props }) {
|
|
66
66
|
return (_jsx(MenuPrimitive.Separator, { "data-slot": "dropdown-menu-separator", className: cn("-mx-1.5 my-1.5 h-px bg-border/50", className), ...props }));
|
package/dist/input-otp.js
CHANGED
|
@@ -18,7 +18,7 @@ function InputOTPGroup({ className, ...props }) {
|
|
|
18
18
|
function InputOTPSlot({ index, className, ...props }) {
|
|
19
19
|
const context = React.useContext(OTPInputContext);
|
|
20
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-
|
|
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-control-off 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
22
|
}
|
|
23
23
|
function InputOTPSeparator(props) {
|
|
24
24
|
return (_jsx("div", { "data-slot": "input-otp-separator", role: "separator", className: "h-px w-2.5 bg-muted-foreground/40", ...props }));
|
package/dist/input.js
CHANGED
|
@@ -21,9 +21,12 @@ const PICKER_ICONS = {
|
|
|
21
21
|
time: Clock01Icon,
|
|
22
22
|
};
|
|
23
23
|
const INPUT_CLASS =
|
|
24
|
-
// Le champ du site : fond de page, un filet
|
|
25
|
-
//
|
|
26
|
-
|
|
24
|
+
// Le champ du site : fond de page, un filet `control-off` au repos (3:1 du
|
|
25
|
+
// fond, WCAG 1.4.11 : à 70 % de `border`, un champ vide disparaissait sur une
|
|
26
|
+
// page blanche), et l'anneau plein au focus, le seul endroit où l'accent dit
|
|
27
|
+
// « c'est ici ». Texte à 16 px à toutes les largeurs : sous 16 px, Safari
|
|
28
|
+
// zoome au focus, et un iPad (768, 1024) en recevait 14.
|
|
29
|
+
"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-control-off 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";
|
|
27
30
|
function Input({ className, type, ...props }) {
|
|
28
31
|
const fieldId = useFieldId();
|
|
29
32
|
const describedBy = useFieldDescribedBy();
|
package/dist/org-switcher.js
CHANGED
|
@@ -22,7 +22,7 @@ export function RoleBadge({ orgRole: role, title, className, }) {
|
|
|
22
22
|
return (_jsx(Badge, { tone: "neutral", "data-role": role, title: title, className: className, children: ORG_ROLE_LABEL[role] }));
|
|
23
23
|
}
|
|
24
24
|
/** Un lien dans un menu, à l'allure d'un `DropdownMenuItem`. */
|
|
25
|
-
export const MENU_LINK_ITEM = "relative flex cursor-default items-center gap-2.5 rounded-xl px-3 py-2 text-sm font-medium outline-hidden select-none focus:bg-muted focus:text-foreground data-highlighted:bg-muted max-lg:min-h-11 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4";
|
|
25
|
+
export const MENU_LINK_ITEM = "relative flex cursor-default items-center gap-2.5 rounded-xl px-3 py-2 text-sm font-medium outline-hidden select-none focus:bg-muted focus:text-foreground data-highlighted:bg-muted max-lg:min-h-11 coarse:min-h-11 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4";
|
|
26
26
|
/** Marque d'une organisation : son logo, ou ses initiales (`initials`) sur un carré arrondi. */
|
|
27
27
|
export function OrgMark({ org, className, }) {
|
|
28
28
|
if (org.logo)
|
|
@@ -43,7 +43,7 @@ export function OrgSwitcher({ organizations, activeId, onSelect, createHref, onC
|
|
|
43
43
|
return null;
|
|
44
44
|
// En haut de la barre des produits : la marque seule dans 40 px, le nom quand la barre s'élargit.
|
|
45
45
|
const inRail = placement === "product-rail";
|
|
46
|
-
return (_jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { render: _jsx("button", { type: "button", "aria-label": `Organisation ${active.name}, changer d'organisation`, className: cn("flex h-10 w-full min-w-0 items-center gap-2.5 rounded-xl px-1.5 text-left outline-none transition-colors duration-(--duration-fast) ease-out hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring/50 aria-expanded:bg-muted", inRail
|
|
46
|
+
return (_jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { render: _jsx("button", { type: "button", "aria-label": `Organisation ${active.name}, changer d'organisation`, className: cn("flex h-10 w-full min-w-0 items-center gap-2.5 rounded-xl px-1.5 text-left coarse:h-11 outline-none transition-colors duration-(--duration-fast) ease-out hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring/50 aria-expanded:bg-muted", inRail
|
|
47
47
|
? "shrink-0 gap-3 px-1 hover:bg-shell-hover aria-expanded:bg-shell-hover"
|
|
48
48
|
: "lg:hover:bg-shell-hover lg:aria-expanded:bg-shell-hover lg:rail:w-auto lg:rail:justify-center lg:rail:px-1", className) }), children: [_jsx(OrgMark, { org: active }), _jsxs("span", { className: cn("min-w-0 flex-1", inRail
|
|
49
49
|
? "opacity-0 transition-opacity duration-(--duration-fast) ease-out group-data-expanded/rail:opacity-100 motion-reduce:transition-none"
|
package/dist/page-header.js
CHANGED
|
@@ -3,7 +3,7 @@ import ArrowRight01Icon from "@hugeicons/core-free-icons/ArrowRight01Icon";
|
|
|
3
3
|
import { HugeiconsIcon } from "@hugeicons/react";
|
|
4
4
|
import { useRender } from "@base-ui/react/use-render";
|
|
5
5
|
import { cn } from "./cn.js";
|
|
6
|
-
const crumbLink = "rounded-md text-muted-foreground transition-colors duration-(--duration-fast) ease-out outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50";
|
|
6
|
+
const crumbLink = "touch-target rounded-md text-muted-foreground transition-colors duration-(--duration-fast) ease-out outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50";
|
|
7
7
|
function CrumbLink({ crumb }) {
|
|
8
8
|
return useRender({
|
|
9
9
|
defaultTagName: "a",
|
package/dist/product-rail.js
CHANGED
|
@@ -10,7 +10,7 @@ export const PRODUCT_RAIL_OPEN_DELAY = 150;
|
|
|
10
10
|
function ProductLink({ entry }) {
|
|
11
11
|
// Chaque produit s'ouvre en document complet : la coque montre le H dès le clic.
|
|
12
12
|
const leaveOnClick = useLeaveOnClick();
|
|
13
|
-
return (_jsx("li", { children: _jsxs("a", { href: entry.href, onClick: leaveOnClick, "aria-current": entry.current ? "page" : undefined, className: cn("flex h-10 items-center gap-3 rounded-xl px-2 text-shell-muted outline-none transition-colors duration-(--duration-fast) ease-out", "hover:bg-shell-hover hover:text-shell-foreground focus-visible:ring-2 focus-visible:ring-ring/50", "aria-[current=page]:bg-shell-active aria-[current=page]:text-shell-foreground"), children: [_jsx(ProductBadge, { product: entry.badge, className: "size-6" }), _jsxs("span", { className: "min-w-0 flex-1 truncate opacity-0 transition-opacity duration-(--duration-fast) ease-out group-data-expanded/rail:opacity-100 motion-reduce:transition-none", children: [_jsx("span", { className: "font-heading text-sm font-medium", children: entry.name }), entry.hint ? _jsx("span", { className: "sr-only", children: ", " }) : null, entry.hint ? _jsx("span", { className: "ml-1.5 text-xs text-shell-muted", children: entry.hint }) : null] })] }) }));
|
|
13
|
+
return (_jsx("li", { children: _jsxs("a", { href: entry.href, onClick: leaveOnClick, "aria-current": entry.current ? "page" : undefined, className: cn("flex h-10 items-center gap-3 rounded-xl px-2 text-shell-muted coarse:h-11 outline-none transition-colors duration-(--duration-fast) ease-out", "hover:bg-shell-hover hover:text-shell-foreground focus-visible:ring-2 focus-visible:ring-ring/50", "aria-[current=page]:bg-shell-active aria-[current=page]:text-shell-foreground"), children: [_jsx(ProductBadge, { product: entry.badge, className: "size-6" }), _jsxs("span", { className: "min-w-0 flex-1 truncate opacity-0 transition-opacity duration-(--duration-fast) ease-out group-data-expanded/rail:opacity-100 motion-reduce:transition-none", children: [_jsx("span", { className: "font-heading text-sm font-medium", children: entry.name }), entry.hint ? _jsx("span", { className: "sr-only", children: ", " }) : null, entry.hint ? _jsx("span", { className: "ml-1.5 text-xs text-shell-muted", children: entry.hint }) : null] })] }) }));
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
16
|
* La barre des produits de la famille, tout à gauche de la coque sur ordinateur : organisation en
|
package/dist/resource-list.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export declare function ResourceItem({ icon, title, meta, badge, actions, href,
|
|
|
15
15
|
/** Icône, favicon ou marque, dans une tuile de 40 px. */
|
|
16
16
|
icon?: ReactNode;
|
|
17
17
|
title: ReactNode;
|
|
18
|
-
/** Une ligne de contexte : adresse, date, auteur.
|
|
18
|
+
/** Une ligne de contexte : adresse, date, auteur. Deux lignes au plus sur un écran étroit, puis tronquée. */
|
|
19
19
|
meta?: ReactNode;
|
|
20
20
|
badge?: ReactNode;
|
|
21
21
|
/** Boutons-icônes ou menu « … ». */
|
package/dist/resource-list.js
CHANGED
|
@@ -17,7 +17,7 @@ export function ResourceList({ className, surface = true, ...props }) {
|
|
|
17
17
|
*/
|
|
18
18
|
export function ResourceItem({ icon, title, meta, badge, actions, href, render, className, }) {
|
|
19
19
|
const linked = href !== undefined || render !== undefined;
|
|
20
|
-
return (_jsxs("li", { "data-slot": "resource-item", className: cn("relative flex min-h-16 items-center gap-3 px-4 py-3 transition-colors duration-(--duration-fast) ease-out not-first:border-t not-first:border-foreground/5 sm:gap-4 sm:px-5", linked && "hover:bg-foreground/[0.03] has-focus-visible:bg-foreground/[0.03]", className), children: [icon ? (_jsx("span", { "aria-hidden": true, className: "grid size-10 shrink-0 place-items-center overflow-hidden rounded-xl bg-background text-muted-foreground dark:bg-muted [&_svg:not([class*='size-'])]:size-5", children: icon })) : null, _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: "truncate text-sm font-medium", children: linked ? (_jsx(ResourceLink, { href: href, render: render, children: title })) : (title) }), meta ? _jsx("p", { className: "mt-0.5
|
|
20
|
+
return (_jsxs("li", { "data-slot": "resource-item", className: cn("relative flex min-h-16 items-center gap-3 px-4 py-3 transition-colors duration-(--duration-fast) ease-out not-first:border-t not-first:border-foreground/5 sm:gap-4 sm:px-5", linked && "hover:bg-foreground/[0.03] has-focus-visible:bg-foreground/[0.03]", className), children: [icon ? (_jsx("span", { "aria-hidden": true, className: "grid size-10 shrink-0 place-items-center overflow-hidden rounded-xl bg-background text-muted-foreground dark:bg-muted [&_svg:not([class*='size-'])]:size-5", children: icon })) : null, _jsxs("div", { className: "flex min-w-0 flex-1 flex-wrap items-center gap-x-3 gap-y-2 sm:gap-x-4", children: [_jsxs("div", { className: "min-w-0 flex-[1_1_12rem]", children: [_jsx("p", { className: "truncate text-sm font-medium", children: linked ? (_jsx(ResourceLink, { href: href, render: render, children: title })) : (title) }), meta ? _jsx("p", { className: "mt-0.5 line-clamp-2 text-xs break-words text-muted-foreground", children: meta }) : null] }), badge || actions ? (_jsxs("div", { "data-slot": "resource-item-trailing", className: "flex max-w-full shrink-0 flex-wrap items-center gap-x-3 gap-y-2 sm:gap-x-4", children: [badge ? _jsx("div", { className: "shrink-0", children: badge }) : null, actions ? _jsx("div", { className: "relative z-1 flex shrink-0 items-center gap-1", children: actions }) : null] })) : null] })] }));
|
|
21
21
|
}
|
|
22
22
|
function ResourceLink({ href, render, children, }) {
|
|
23
23
|
return useRender({
|
package/dist/select.js
CHANGED
|
@@ -27,9 +27,9 @@ import { cn } from "./cn.js";
|
|
|
27
27
|
* </SelectContent>
|
|
28
28
|
* </Select>
|
|
29
29
|
*/
|
|
30
|
-
// Même recette que `Input` : filet
|
|
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-
|
|
30
|
+
// Même recette que `Input` : filet `control-off` au repos, 16 px, anneau plein
|
|
31
|
+
// au focus et 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-control-off 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";
|
|
33
33
|
function Select(props) {
|
|
34
34
|
return _jsx(SelectPrimitive.Root, { ...props });
|
|
35
35
|
}
|
|
@@ -59,7 +59,7 @@ function SelectContent({ className, align = "start", alignOffset = 0, side = "bo
|
|
|
59
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
60
|
}
|
|
61
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" }) })] }));
|
|
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 max-lg:min-h-11 coarse:min-h-11 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
63
|
}
|
|
64
64
|
function SelectGroup({ ...props }) {
|
|
65
65
|
return _jsx(SelectPrimitive.Group, { "data-slot": "select-group", ...props });
|
package/dist/switch.js
CHANGED
|
@@ -17,7 +17,7 @@ import { cn } from "./cn.js";
|
|
|
17
17
|
* pouce le trouve sans viser.
|
|
18
18
|
*/
|
|
19
19
|
export function Switch({ checked, onCheckedChange, disabled, className, ...props }) {
|
|
20
|
-
return (_jsx(SwitchPrimitive.Root, { checked: checked, onCheckedChange: onCheckedChange, disabled: disabled, className: cn("relative inline-flex h-8 w-[3.25rem] shrink-0 items-center rounded-full p-0.5", "transition-colors duration-200 ease-ios", "outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2", "focus-visible:ring-offset-background",
|
|
20
|
+
return (_jsx(SwitchPrimitive.Root, { checked: checked, onCheckedChange: onCheckedChange, disabled: disabled, className: cn("touch-target relative inline-flex h-8 w-[3.25rem] shrink-0 items-center rounded-full p-0.5", "transition-colors duration-200 ease-ios", "outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2", "focus-visible:ring-offset-background",
|
|
21
21
|
// Désactivé : tout l'interrupteur s'estompe, la piste ne disparaît pas.
|
|
22
22
|
"disabled:cursor-not-allowed disabled:opacity-50 data-disabled:cursor-not-allowed data-disabled:opacity-50",
|
|
23
23
|
// Éteint : piste grise à 3:1 du fond, de la carte et de `--muted` ; allumé : l'accent d'état.
|
package/dist/table.js
CHANGED
|
@@ -36,7 +36,7 @@ function TableRow({ className, ...props }) {
|
|
|
36
36
|
function TableHead({ className, children, sort, onSort, numeric = false, ...props }) {
|
|
37
37
|
const sortable = onSort !== undefined;
|
|
38
38
|
const direction = sort ?? "none";
|
|
39
|
-
return (_jsx("th", { "data-slot": "table-head", scope: "col", "aria-sort": sortable ? direction : undefined, className: cn("h-11 px-4 text-left align-middle text-xs font-medium whitespace-nowrap text-muted-foreground first:pl-5 last:pr-5", numeric && "text-right", className), ...props, children: sortable ? (_jsxs("button", { type: "button", onClick: onSort, className: cn("-mx-1.5 inline-flex items-center gap-1 rounded-md px-1.5 py-1 outline-none transition-colors duration-(--duration-fast) ease-out hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50", direction !== "none" && "text-foreground"), children: [children, _jsx(HugeiconsIcon, { icon: direction === "ascending" ? ArrowUp01Icon : direction === "descending" ? ArrowDown01Icon : ArrowUpDownIcon, strokeWidth: 2, "aria-hidden": true, className: cn("size-3.5 shrink-0", direction === "none" && "opacity-50") })] })) : (children) }));
|
|
39
|
+
return (_jsx("th", { "data-slot": "table-head", scope: "col", "aria-sort": sortable ? direction : undefined, className: cn("h-11 px-4 text-left align-middle text-xs font-medium whitespace-nowrap text-muted-foreground first:pl-5 last:pr-5", numeric && "text-right", className), ...props, children: sortable ? (_jsxs("button", { type: "button", onClick: onSort, className: cn("touch-target -mx-1.5 inline-flex items-center gap-1 rounded-md px-1.5 py-1 outline-none transition-colors duration-(--duration-fast) ease-out hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50", direction !== "none" && "text-foreground"), children: [children, _jsx(HugeiconsIcon, { icon: direction === "ascending" ? ArrowUp01Icon : direction === "descending" ? ArrowDown01Icon : ArrowUpDownIcon, strokeWidth: 2, "aria-hidden": true, className: cn("size-3.5 shrink-0", direction === "none" && "opacity-50") })] })) : (children) }));
|
|
40
40
|
}
|
|
41
41
|
function TableCell({ className, numeric = false, ...props }) {
|
|
42
42
|
return (_jsx("td", { "data-slot": "table-cell", className: cn("px-4 py-3 align-middle first:pl-5 last:pr-5", numeric && "text-right tabular-nums",
|
package/dist/tabs.d.ts
CHANGED
|
@@ -9,8 +9,12 @@ declare function Tabs({ className, ...props }: TabsPrimitive.Root.Props): React.
|
|
|
9
9
|
/**
|
|
10
10
|
* La rangée d'onglets. L'onglet ouvert est marqué d'une pastille teintée qui glisse de l'un à
|
|
11
11
|
* l'autre, monochrome comme la barre latérale : l'accent reste au focus.
|
|
12
|
+
*
|
|
13
|
+
* Trop d'onglets pour la largeur : la rangée défile, et son bord s'efface en dégradé du côté où il
|
|
14
|
+
* en reste (masque, pas de couleur ajoutée). Sans cet indice, l'onglet coupé au bord passait pour
|
|
15
|
+
* le dernier (dix onglets d'un projet de Portal sur téléphone, 2026-10-18).
|
|
12
16
|
*/
|
|
13
|
-
declare function TabsList({ className, children, ...props }: TabsPrimitive.List.Props): React.JSX.Element;
|
|
17
|
+
declare function TabsList({ className, children, ref, ...props }: TabsPrimitive.List.Props): React.JSX.Element;
|
|
14
18
|
declare function TabsTab({ className, ...props }: TabsPrimitive.Tab.Props): React.JSX.Element;
|
|
15
19
|
declare function TabsPanel({ className, ...props }: TabsPrimitive.Panel.Props): React.JSX.Element;
|
|
16
20
|
export { Tabs, TabsList, TabsTab, TabsPanel };
|
package/dist/tabs.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Tabs as TabsPrimitive } from "@base-ui/react/tabs";
|
|
3
|
+
import { useEffect, useImperativeHandle, useRef } from "react";
|
|
3
4
|
import { cn } from "./cn.js";
|
|
4
5
|
/**
|
|
5
6
|
* Onglets d'une même page (Aperçu, Journal, Réglages). Flèches pour passer d'un onglet à
|
|
@@ -10,12 +11,47 @@ import { cn } from "./cn.js";
|
|
|
10
11
|
function Tabs({ className, ...props }) {
|
|
11
12
|
return _jsx(TabsPrimitive.Root, { "data-slot": "tabs", className: cn("grid gap-6", className), ...props });
|
|
12
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Pose `data-overflow-start` et `data-overflow-end` sur une rangée qui défile horizontalement,
|
|
16
|
+
* selon qu'il reste du contenu caché à gauche ou à droite. Recalculé au défilement et quand la
|
|
17
|
+
* rangée ou ses onglets changent de taille.
|
|
18
|
+
*/
|
|
19
|
+
function useOverflowEdges(forwarded) {
|
|
20
|
+
const local = useRef(null);
|
|
21
|
+
useImperativeHandle(forwarded, () => local.current, []);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const node = local.current;
|
|
24
|
+
if (!node)
|
|
25
|
+
return undefined;
|
|
26
|
+
const update = () => {
|
|
27
|
+
const max = node.scrollWidth - node.clientWidth;
|
|
28
|
+
node.toggleAttribute("data-overflow-start", max > 1 && node.scrollLeft > 1);
|
|
29
|
+
node.toggleAttribute("data-overflow-end", max > 1 && node.scrollLeft < max - 1);
|
|
30
|
+
};
|
|
31
|
+
update();
|
|
32
|
+
node.addEventListener("scroll", update, { passive: true });
|
|
33
|
+
const observer = typeof ResizeObserver === "undefined" ? undefined : new ResizeObserver(update);
|
|
34
|
+
observer?.observe(node);
|
|
35
|
+
for (const child of node.children)
|
|
36
|
+
observer?.observe(child);
|
|
37
|
+
return () => {
|
|
38
|
+
node.removeEventListener("scroll", update);
|
|
39
|
+
observer?.disconnect();
|
|
40
|
+
};
|
|
41
|
+
}, []);
|
|
42
|
+
return local;
|
|
43
|
+
}
|
|
13
44
|
/**
|
|
14
45
|
* La rangée d'onglets. L'onglet ouvert est marqué d'une pastille teintée qui glisse de l'un à
|
|
15
46
|
* l'autre, monochrome comme la barre latérale : l'accent reste au focus.
|
|
47
|
+
*
|
|
48
|
+
* Trop d'onglets pour la largeur : la rangée défile, et son bord s'efface en dégradé du côté où il
|
|
49
|
+
* en reste (masque, pas de couleur ajoutée). Sans cet indice, l'onglet coupé au bord passait pour
|
|
50
|
+
* le dernier (dix onglets d'un projet de Portal sur téléphone, 2026-10-18).
|
|
16
51
|
*/
|
|
17
|
-
function TabsList({ className, children, ...props }) {
|
|
18
|
-
|
|
52
|
+
function TabsList({ className, children, ref, ...props }) {
|
|
53
|
+
const edgesRef = useOverflowEdges(ref);
|
|
54
|
+
return (_jsxs(TabsPrimitive.List, { ref: edgesRef, "data-slot": "tabs-list", className: cn("relative z-0 flex w-fit max-w-full scroll-fade-x items-center gap-1 overflow-x-auto [scrollbar-width:none]", className), ...props, children: [children, _jsx(TabsPrimitive.Indicator, { "data-slot": "tabs-indicator", className: "absolute top-(--active-tab-top) left-0 -z-1 h-(--active-tab-height) w-(--active-tab-width) translate-x-(--active-tab-left) rounded-full bg-foreground/[0.07] transition-[translate,width] duration-(--duration-base) ease-out motion-reduce:transition-none" })] }));
|
|
19
55
|
}
|
|
20
56
|
function TabsTab({ className, ...props }) {
|
|
21
57
|
return (_jsx(TabsPrimitive.Tab, { "data-slot": "tabs-tab", className: cn("touch-target flex h-9 shrink-0 items-center justify-center gap-1.5 rounded-full px-3.5 text-sm font-medium whitespace-nowrap text-muted-foreground", "transition-colors duration-(--duration-fast) ease-out outline-none select-none", "hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 data-active:text-foreground", "data-disabled:pointer-events-none data-disabled:opacity-50", "[&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className), ...props }));
|
package/dist/textarea.js
CHANGED
|
@@ -11,6 +11,6 @@ import { cn } from "./cn.js";
|
|
|
11
11
|
function Textarea({ className, rows, style, ...props }) {
|
|
12
12
|
const fieldId = useFieldId();
|
|
13
13
|
const describedBy = useFieldDescribedBy();
|
|
14
|
-
return (_jsx("textarea", { "data-slot": "textarea", rows: rows, style: rows ? { minHeight: `calc(${rows}lh + 1.5rem)`, ...style } : style, "aria-describedby": props["aria-describedby"] ?? describedBy, className: cn("flex field-sizing-content min-h-16 touch-field w-full resize-none rounded-2xl bg-background px-3.5 py-3 text-base ring-1 ring-
|
|
14
|
+
return (_jsx("textarea", { "data-slot": "textarea", rows: rows, style: rows ? { minHeight: `calc(${rows}lh + 1.5rem)`, ...style } : style, "aria-describedby": props["aria-describedby"] ?? describedBy, className: cn("flex field-sizing-content min-h-16 touch-field w-full resize-none rounded-2xl bg-background px-3.5 py-3 text-base ring-1 ring-control-off 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:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-2 aria-invalid:ring-destructive/60", className), id: props.id ?? fieldId, ...props }));
|
|
15
15
|
}
|
|
16
16
|
export { Textarea };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hgdc/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Interface de la H Collection : jetons de la DA, thèmes, polices et composants.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"homepage": "https://github.com/hugogebel-boop/hgdc-kit/tree/main/packages/ui#readme",
|
package/styles.css
CHANGED
|
@@ -24,6 +24,9 @@
|
|
|
24
24
|
@custom-variant rail (&:where([data-sidebar="rail"] *, [data-rail-peek] *));
|
|
25
25
|
/* Pliée par la personne seulement (pas le temps du survol de la barre des produits). */
|
|
26
26
|
@custom-variant folded (&:where([data-sidebar="rail"] *));
|
|
27
|
+
/* Écran tactile, quelle que soit la largeur : `coarse:min-h-11` rend ses 44 px à une entrée que
|
|
28
|
+
`lg:` avait resserrée pour la souris. Déclarée après les points de rupture, elle les emporte. */
|
|
29
|
+
@custom-variant coarse (@media (pointer: coarse));
|
|
27
30
|
|
|
28
31
|
/* Courbes et durées. Pas d'ease-in : il démarre lentement à l'instant où l'œil regarde. */
|
|
29
32
|
@theme {
|
|
@@ -133,7 +136,7 @@
|
|
|
133
136
|
--destructive: oklch(0.577 0.245 27.325);
|
|
134
137
|
--border: oklch(0.922 0 0);
|
|
135
138
|
--input: oklch(0.922 0 0);
|
|
136
|
-
/* Contrôle éteint (piste d'interrupteur, bord de case
|
|
139
|
+
/* Contrôle éteint (piste d'interrupteur, bord de case, de bouton radio et de champ) : 3:1 au moins contre
|
|
137
140
|
le fond, une carte et `--muted` (WCAG 1.4.11, vérifié par contrast.test.ts). */
|
|
138
141
|
--control-off: oklch(0.62 0 0);
|
|
139
142
|
--ring: var(--accent);
|
|
@@ -375,9 +378,12 @@
|
|
|
375
378
|
}
|
|
376
379
|
}
|
|
377
380
|
|
|
378
|
-
/* Zone tactile de 44 px sur écran tactile, sans changer la taille visible.
|
|
381
|
+
/* Zone tactile de 44 px sur écran tactile, sans changer la taille visible. Le pointeur seul
|
|
382
|
+
décide, jamais la largeur : un iPad en paysage (1024 px) a la coque d'ordinateur et le doigt
|
|
383
|
+
d'un téléphone. Sans `hover: none` non plus : un écran tactile qui annonce aussi un survol
|
|
384
|
+
(iPad avec clavier, portable tactile) gardait des cibles de 36 px. */
|
|
379
385
|
@utility touch-target {
|
|
380
|
-
@media (
|
|
386
|
+
@media (pointer: coarse) {
|
|
381
387
|
position: relative;
|
|
382
388
|
&::after {
|
|
383
389
|
content: "";
|
|
@@ -394,11 +400,32 @@
|
|
|
394
400
|
}
|
|
395
401
|
|
|
396
402
|
@utility touch-field {
|
|
397
|
-
@media (
|
|
403
|
+
@media (pointer: coarse) {
|
|
398
404
|
min-height: 2.75rem;
|
|
399
405
|
}
|
|
400
406
|
}
|
|
401
407
|
|
|
408
|
+
/* Rangée qui défile à l'horizontale (onglets) : le bord s'efface en dégradé du côté où il reste du
|
|
409
|
+
contenu caché, `data-overflow-start` / `data-overflow-end` posés par le composant. Un masque, pas
|
|
410
|
+
une couleur : rien ne s'ajoute à la DA, et le fond de la page reste celui qu'on voit. */
|
|
411
|
+
@utility scroll-fade-x {
|
|
412
|
+
--fade-start: 0px;
|
|
413
|
+
--fade-end: 0px;
|
|
414
|
+
mask-image: linear-gradient(
|
|
415
|
+
to right,
|
|
416
|
+
transparent,
|
|
417
|
+
#000 var(--fade-start),
|
|
418
|
+
#000 calc(100% - var(--fade-end)),
|
|
419
|
+
transparent
|
|
420
|
+
);
|
|
421
|
+
&[data-overflow-start] {
|
|
422
|
+
--fade-start: 2.5rem;
|
|
423
|
+
}
|
|
424
|
+
&[data-overflow-end] {
|
|
425
|
+
--fade-end: 2.5rem;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
402
429
|
@utility date-field {
|
|
403
430
|
appearance: none;
|
|
404
431
|
}
|