@hgdc/ui 0.2.1 → 0.3.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 +62 -21
- package/dist/account-menu.d.ts +32 -0
- package/dist/account-menu.js +39 -0
- package/dist/alert.d.ts +19 -0
- package/dist/alert.js +25 -0
- package/dist/app-shell.d.ts +56 -0
- package/dist/app-shell.js +107 -0
- package/dist/card.d.ts +24 -0
- package/dist/card.js +42 -0
- package/dist/checkbox.d.ts +12 -0
- package/dist/checkbox.js +19 -0
- package/dist/command-menu.d.ts +56 -0
- package/dist/command-menu.js +79 -0
- package/dist/copy-button.d.ts +26 -0
- package/dist/copy-button.js +46 -0
- package/dist/kbd.d.ts +9 -0
- package/dist/kbd.js +19 -0
- package/dist/org-switcher.d.ts +32 -0
- package/dist/org-switcher.js +32 -0
- package/dist/page-header.d.ts +27 -0
- package/dist/page-header.js +31 -0
- package/dist/popover.d.ts +13 -0
- package/dist/popover.js +22 -0
- package/dist/radio-group.d.ts +16 -0
- package/dist/radio-group.js +22 -0
- package/dist/relative-time.d.ts +16 -0
- package/dist/relative-time.js +54 -0
- package/dist/resource-list.d.ts +31 -0
- package/dist/resource-list.js +35 -0
- package/dist/secret-reveal.d.ts +13 -0
- package/dist/secret-reveal.js +26 -0
- package/dist/skeleton.d.ts +28 -0
- package/dist/skeleton.js +47 -0
- package/dist/stat.d.ts +30 -0
- package/dist/stat.js +26 -0
- package/dist/status.d.ts +22 -0
- package/dist/status.js +37 -0
- package/dist/table.d.ts +50 -0
- package/dist/table.js +68 -0
- package/dist/tabs.d.ts +16 -0
- package/dist/tabs.js +26 -0
- package/dist/time-chart.d.ts +40 -0
- package/dist/time-chart.js +127 -0
- package/dist/timeline.d.ts +21 -0
- package/dist/timeline.js +11 -0
- package/dist/tooltip.d.ts +15 -0
- package/dist/tooltip.js +25 -0
- package/dist/uptime-bar.d.ts +24 -0
- package/dist/uptime-bar.js +25 -0
- package/package.json +1 -1
- package/styles.css +45 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @hgdc/ui
|
|
2
2
|
|
|
3
|
-
Interface de la H family : jetons de la DA, thèmes clair et sombre, polices
|
|
3
|
+
Interface de la H family : jetons de la DA, thèmes clair et sombre, polices, composants de base et coque des consoles, pour une application React 19 + Tailwind v4.
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
6
|
bun add @hgdc/ui
|
|
@@ -19,34 +19,75 @@ bun add @hgdc/ui
|
|
|
19
19
|
|
|
20
20
|
```tsx
|
|
21
21
|
import { Button } from "@hgdc/ui/button"
|
|
22
|
-
import { ThemeProvider, themeInitScript
|
|
22
|
+
import { ThemeProvider, themeInitScript } from "@hgdc/ui/theme"
|
|
23
|
+
import { sidebarInitScript } from "@hgdc/ui/app-shell"
|
|
23
24
|
```
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
Un sous-chemin par composant, sans fichier d'index. Les primitives viennent toutes de Base UI ; les graphiques sont en SVG, sans bibliothèque.
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
| ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
29
|
-
| `@hgdc/ui/styles.css` | Jetons OKLCH, thèmes (`.dark` sur `<html>`), Inter et Geist, utilitaires `surface`, `pressable`, `soft-in`… |
|
|
30
|
-
| `@hgdc/ui/theme` | `ThemeProvider`, `useTheme`, `themeInitScript` (à poser dans le `<head>` contre le flash de thème) |
|
|
31
|
-
| `@hgdc/ui/cn` | `cn()` (clsx + tailwind-merge) |
|
|
32
|
-
| `@hgdc/ui/use-media-query` | `useMediaQuery`, `useIsMobile` (sûrs au rendu serveur) |
|
|
33
|
-
| `@hgdc/ui/floating` | Classes des surfaces flottantes (menus, sélecteurs) |
|
|
34
|
-
| `avatar` `badge` `button` `dialog` `drawer` `dropdown-menu` `empty` `field` `input` `input-otp` | Primitives sur Base UI |
|
|
35
|
-
| `label` `segmented-control` `select` `separator` `sonner` `spinner` `switch` `textarea` | Primitives sur Base UI, toasts sonner, icônes Hugeicons |
|
|
28
|
+
## Fondations
|
|
36
29
|
|
|
37
|
-
|
|
30
|
+
| Import | Rôle |
|
|
31
|
+
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
32
|
+
| `@hgdc/ui/styles.css` | Jetons OKLCH, thèmes (`.dark` sur `<html>`), variante `rail:`, polices, utilitaires `surface`, `pressable`, `skeleton-pulse`… |
|
|
33
|
+
| `@hgdc/ui/theme` | `ThemeProvider`, `useTheme`, `themeInitScript` (dans le `<head>`, contre le flash de thème) |
|
|
34
|
+
| `@hgdc/ui/cn` | `cn()` (clsx + tailwind-merge) |
|
|
35
|
+
| `@hgdc/ui/use-media-query` | `useMediaQuery`, `useIsMobile` |
|
|
36
|
+
| `@hgdc/ui/floating` | Classes des surfaces flottantes |
|
|
37
|
+
|
|
38
|
+
Primitives déjà là : `avatar` `badge` `button` `dialog` `drawer` `dropdown-menu` `empty` `field` `input` `input-otp` `label` `segmented-control` `select` `separator` `sonner` `spinner` `switch` `textarea`.
|
|
39
|
+
|
|
40
|
+
## Coque des consoles
|
|
41
|
+
|
|
42
|
+
| Import | Exemple |
|
|
43
|
+
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
44
|
+
| `app-shell` | `<AppShell sidebarHeader={<OrgSwitcher …/>} nav={<SidebarNav label="Projet">…</SidebarNav>} sidebarFooter={<AccountMenu …/>}>` |
|
|
45
|
+
| | `<SidebarNavItem icon={Rocket01Icon} label="Déploiements" render={<Link to="/deployments" />} />` |
|
|
46
|
+
| `org-switcher` | `<OrgSwitcher organizations={orgs} activeId={org.id} onSelect={go} createHref="/organisations/nouvelle" />` |
|
|
47
|
+
| `account-menu` | `<AccountMenu user={user} onProfile={…} onSecurity={…} onSignOut={signOut} products={HGDC_PRODUCTS} />` |
|
|
48
|
+
| `command-menu` | `<CommandMenu groups={[{ label: "Aller à", items }, { label: "Créer", items }, { label: "Changer", items }]} />` (`Ctrl+K`) |
|
|
49
|
+
| `kbd` | `<KbdGroup><Kbd>Ctrl</Kbd><Kbd>K</Kbd></KbdGroup>` |
|
|
50
|
+
| `page-header` | `<PageHeader title="Domaines" breadcrumbs={[{ label: "Acme", render: <Link to="/o/acme" /> }, { label: "Domaines" }]} actions={<Button>Ajouter</Button>} />` |
|
|
51
|
+
|
|
52
|
+
La barre se réduit en rail (`Ctrl+B`, mémorisé) : poser `sidebarInitScript` dans le `<head>`. Le kit ne connaît ni routeur ni authentification : liens par `render={<Link />}`, actions par callbacks. Rôles d'organisation : `owner`, `admin`, `member` (Propriétaire, Admin, Membre).
|
|
53
|
+
|
|
54
|
+
## Contenu et données
|
|
55
|
+
|
|
56
|
+
| Import | Exemple |
|
|
57
|
+
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
58
|
+
| `card` | `<Card><CardHeader><CardTitle>Domaines</CardTitle><CardAction>…</CardAction></CardHeader><CardContent>…</CardContent><CardFooter>…</CardFooter></Card>` |
|
|
59
|
+
| `alert` | `<Alert tone="warning" title="Quota presque atteint" action={<Button size="sm">Voir</Button>}>90 % utilisés.</Alert>` |
|
|
60
|
+
| `table` | `<TableHead sort="descending" onSort={…}>Date</TableHead>`, `<TableRowLink render={<Link … />}>Production</TableRowLink>`, `<TableEmpty colSpan={4}>…</TableEmpty>`, `<TableSkeleton columns={4} />` |
|
|
61
|
+
| `resource-list` | `<ResourceList><ResourceItem icon={…} title="Site vitrine" meta="acme.ch" badge={…} actions={…} href="/p/site" /></ResourceList>` |
|
|
62
|
+
| `stat` | `<StatGrid><StatTile label="Erreurs" value="12" delta={{ value: "8 %", direction: "up", good: "down" }} /></StatGrid>` |
|
|
63
|
+
| `time-chart` | `<TimeChart label="Envois par jour" timestamps={days} series={[{ key: "sent", label: "Envoyés", values }]} kind="bar" />` |
|
|
64
|
+
| `status` | `<StatusBadge status="degraded" />` (Opérationnel, Dégradé, Panne, Maintenance, Inconnu) |
|
|
65
|
+
| `uptime-bar` | `<UptimeBar days={[{ date, status: "operational", uptime: "100 %" }]} summary="99,95 % sur 90 jours" />` |
|
|
66
|
+
| `timeline` | `<Timeline entries={[{ id, title: "Résolu", time, status: "operational" }]} />` |
|
|
67
|
+
| `relative-time` | `<RelativeTime date={deployment._creationTime} />` (« il y a 3 min », date exacte au survol) |
|
|
68
|
+
| `copy-button` | `<Snippet value="bun add @hgdc/ui" prompt />`, `<CopyButton value={url} />` |
|
|
69
|
+
| `secret-reveal` | `<SecretReveal label="Clé d'API" value={key} onDismiss={forget} />` : affichée une fois, puis retirée de la page |
|
|
70
|
+
| `skeleton` | `<Pending pending={isLoading} fallback={<TableSkeleton columns={3} />}>…</Pending>` : après 200 ms, tenu 400 ms au moins |
|
|
71
|
+
|
|
72
|
+
## Contrôles et surfaces flottantes
|
|
73
|
+
|
|
74
|
+
| Import | Exemple |
|
|
75
|
+
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
76
|
+
| `checkbox` | `<label className="flex items-center gap-2.5"><Checkbox defaultChecked /> Résumé hebdomadaire</label>` |
|
|
77
|
+
| `radio-group` | `<RadioGroup aria-labelledby="role" defaultValue="member"><RadioGroupItem value="admin" />…</RadioGroup>` |
|
|
78
|
+
| `tabs` | `<Tabs defaultValue="apercu"><TabsList><TabsTab value="apercu">Aperçu</TabsTab></TabsList><TabsPanel value="apercu">…</TabsPanel></Tabs>` |
|
|
79
|
+
| `tooltip` | `<TooltipProvider>` autour de l'app, puis `<Tooltip><TooltipTrigger aria-label="Copier">…</TooltipTrigger><TooltipContent>Copier</TooltipContent></Tooltip>` |
|
|
80
|
+
| `popover` | `<Popover><PopoverTrigger>Aide</PopoverTrigger><PopoverContent><PopoverTitle>…</PopoverTitle></PopoverContent></Popover>` |
|
|
38
81
|
|
|
39
82
|
## Règles de la DA
|
|
40
83
|
|
|
41
|
-
- Chrome monochrome, un seul accent violet : focus, lien,
|
|
42
|
-
- Profondeur par la teinte :
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
- Chargement : l'anneau de la DA, `<Spinner />` dans un bouton, `<Spinner size={8} tone="accent" />` seul sur une page.
|
|
47
|
-
- Un `Button` rendu en lien (`render={<Link />}`) reste un lien pour les lecteurs d'écran.
|
|
84
|
+
- Chrome monochrome, un seul accent violet : focus, lien, un geste principal par écran. « Vous êtes ici » (barre, onglets) reste en teinte.
|
|
85
|
+
- Profondeur par la teinte : cartes `surface` sans ombre ni contour, ombre réservée à ce qui flotte, aucun flou.
|
|
86
|
+
- Au survol, seule la couleur change. Le rebond est réservé à l'appui. La palette de commandes s'ouvre sans animation.
|
|
87
|
+
- Un statut s'écrit toujours (point et libellé), jamais par la couleur seule. Chiffres en `tabular-nums`.
|
|
88
|
+
- Graphiques : `--chart-1` à `--chart-6` dans cet ordre, validées pour le daltonisme dans les deux thèmes ; au-delà de six séries, regrouper en « Autres ».
|
|
48
89
|
- Référence de l'interface : Host. Couleur d'un client : `--brand-accent`.
|
|
49
90
|
|
|
50
|
-
Mode d'emploi complet : [Adopter le kit](https://github.com/hugogebel-boop/hgdc-kit/blob/main/docs/guides/adopter-le-kit.md#7-interface). Décisions : [ADR 0005](https://github.com/hugogebel-boop/hgdc-kit/blob/main/docs/adr/0005-ui-css-brute-et-sous-chemins.md).
|
|
91
|
+
Mode d'emploi complet : [Adopter le kit](https://github.com/hugogebel-boop/hgdc-kit/blob/main/docs/guides/adopter-le-kit.md#7-interface). Décisions : [ADR 0005](https://github.com/hugogebel-boop/hgdc-kit/blob/main/docs/adr/0005-ui-css-brute-et-sous-chemins.md), [ADR 0006](https://github.com/hugogebel-boop/hgdc-kit/blob/main/docs/adr/0006-ui-composants-des-consoles.md).
|
|
51
92
|
|
|
52
93
|
Tous droits réservés, Hugo Gebel Développement Conseil.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export type HgdcProduct = {
|
|
3
|
+
/** « HGDC Host ». */
|
|
4
|
+
name: string;
|
|
5
|
+
href: string;
|
|
6
|
+
/** Le produit ouvert : coché, sans lien. */
|
|
7
|
+
current?: boolean;
|
|
8
|
+
};
|
|
9
|
+
export type AccountUser = {
|
|
10
|
+
name: string;
|
|
11
|
+
email: string;
|
|
12
|
+
image?: string | null;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Le menu du compte, en bas de la barre latérale : profil, sécurité, thème, raccourcis, autres
|
|
16
|
+
* produits HGDC, déconnexion. `row` : avatar, nom et adresse (l'avatar seul en rail) ; `avatar` :
|
|
17
|
+
* l'avatar seul, pour une barre du haut. Le produit passe les actions ; le thème passe par
|
|
18
|
+
* `ThemeProvider`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function AccountMenu({ user, onProfile, onSecurity, onShortcuts, onSignOut, products, children, variant, }: {
|
|
21
|
+
user: AccountUser;
|
|
22
|
+
onProfile?: () => void;
|
|
23
|
+
onSecurity?: () => void;
|
|
24
|
+
/** Ouvre l'aide des raccourcis clavier. */
|
|
25
|
+
onShortcuts?: () => void;
|
|
26
|
+
onSignOut: () => void;
|
|
27
|
+
/** Les produits de la famille, pour passer de l'un à l'autre. */
|
|
28
|
+
products?: Array<HgdcProduct>;
|
|
29
|
+
/** Entrées propres au produit (« Administration »), après le profil. */
|
|
30
|
+
children?: ReactNode;
|
|
31
|
+
variant?: "row" | "avatar";
|
|
32
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import ComputerIcon from "@hugeicons/core-free-icons/ComputerIcon";
|
|
3
|
+
import DashboardSquare01Icon from "@hugeicons/core-free-icons/DashboardSquare01Icon";
|
|
4
|
+
import KeyboardIcon from "@hugeicons/core-free-icons/KeyboardIcon";
|
|
5
|
+
import Logout01Icon from "@hugeicons/core-free-icons/Logout01Icon";
|
|
6
|
+
import Moon02Icon from "@hugeicons/core-free-icons/Moon02Icon";
|
|
7
|
+
import SecurityLockIcon from "@hugeicons/core-free-icons/SecurityLockIcon";
|
|
8
|
+
import Sun03Icon from "@hugeicons/core-free-icons/Sun03Icon";
|
|
9
|
+
import Tick02Icon from "@hugeicons/core-free-icons/Tick02Icon";
|
|
10
|
+
import UserIcon from "@hugeicons/core-free-icons/UserIcon";
|
|
11
|
+
import { HugeiconsIcon } from "@hugeicons/react";
|
|
12
|
+
import { Menu as MenuPrimitive } from "@base-ui/react/menu";
|
|
13
|
+
import { Avatar, AvatarFallback, AvatarImage } from "./avatar.js";
|
|
14
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from "./dropdown-menu.js";
|
|
15
|
+
import { MENU_LINK_ITEM } from "./org-switcher.js";
|
|
16
|
+
import { useTheme } from "./theme.js";
|
|
17
|
+
const THEMES = ["light", "dark", "system"];
|
|
18
|
+
function initials(name) {
|
|
19
|
+
return name
|
|
20
|
+
.split(/\s+/)
|
|
21
|
+
.filter(Boolean)
|
|
22
|
+
.slice(0, 2)
|
|
23
|
+
.map((part) => part[0])
|
|
24
|
+
.join("");
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Le menu du compte, en bas de la barre latérale : profil, sécurité, thème, raccourcis, autres
|
|
28
|
+
* produits HGDC, déconnexion. `row` : avatar, nom et adresse (l'avatar seul en rail) ; `avatar` :
|
|
29
|
+
* l'avatar seul, pour une barre du haut. Le produit passe les actions ; le thème passe par
|
|
30
|
+
* `ThemeProvider`.
|
|
31
|
+
*/
|
|
32
|
+
export function AccountMenu({ user, onProfile, onSecurity, onShortcuts, onSignOut, products, children, variant = "row", }) {
|
|
33
|
+
const { theme, setTheme } = useTheme();
|
|
34
|
+
return (_jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { render: variant === "row" ? (_jsx("button", { type: "button", "aria-label": `Compte de ${user.name}`, className: "flex w-full min-w-0 items-center gap-2.5 rounded-xl p-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 lg:hover:bg-shell-hover lg:aria-expanded:bg-shell-hover lg:rail:justify-center" })) : (_jsx("button", { type: "button", "aria-label": `Compte de ${user.name}`, className: "touch-target grid size-9 place-items-center rounded-full outline-none focus-visible:ring-2 focus-visible:ring-ring/50" })), children: [_jsxs(Avatar, { children: [user.image ? _jsx(AvatarImage, { src: user.image, alt: "" }) : null, _jsx(AvatarFallback, { className: "text-xs font-semibold uppercase", children: initials(user.name) })] }), variant === "row" ? (_jsxs("span", { className: "min-w-0 flex-1 lg:rail:hidden", children: [_jsx("span", { className: "block truncate text-sm font-medium", children: user.name }), _jsx("span", { className: "block truncate text-xs text-muted-foreground lg:text-shell-muted", children: user.email })] })) : null] }), _jsxs(DropdownMenuContent, { align: variant === "row" ? "start" : "end", side: variant === "row" ? "top" : "bottom", className: "w-64", children: [_jsx(DropdownMenuGroup, { children: _jsxs(DropdownMenuLabel, { children: [_jsx("span", { className: "block truncate text-foreground", children: user.name }), _jsx("span", { className: "block truncate", children: user.email })] }) }), _jsx(DropdownMenuSeparator, {}), onProfile ? (_jsxs(DropdownMenuItem, { onClick: onProfile, children: [_jsx(HugeiconsIcon, { icon: UserIcon, "aria-hidden": true }), "Profil"] })) : null, onSecurity ? (_jsxs(DropdownMenuItem, { onClick: onSecurity, children: [_jsx(HugeiconsIcon, { icon: SecurityLockIcon, "aria-hidden": true }), "S\u00E9curit\u00E9"] })) : null, children, _jsxs(DropdownMenuSub, { children: [_jsxs(DropdownMenuSubTrigger, { children: [_jsx(HugeiconsIcon, { icon: theme === "dark" ? Moon02Icon : theme === "light" ? Sun03Icon : ComputerIcon, "aria-hidden": true }), "Th\u00E8me"] }), _jsx(DropdownMenuSubContent, { children: _jsxs(DropdownMenuRadioGroup, { value: theme, onValueChange: (value) => {
|
|
35
|
+
const next = THEMES.find((t) => t === value);
|
|
36
|
+
if (next)
|
|
37
|
+
setTheme(next);
|
|
38
|
+
}, children: [_jsx(DropdownMenuRadioItem, { value: "light", children: "Clair" }), _jsx(DropdownMenuRadioItem, { value: "dark", children: "Sombre" }), _jsx(DropdownMenuRadioItem, { value: "system", children: "Syst\u00E8me" })] }) })] }), onShortcuts ? (_jsxs(DropdownMenuItem, { onClick: onShortcuts, children: [_jsx(HugeiconsIcon, { icon: KeyboardIcon, "aria-hidden": true }), "Raccourcis clavier", _jsx(DropdownMenuShortcut, { children: "?" })] })) : null, products?.length ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuGroup, { children: [_jsx(DropdownMenuLabel, { children: "Produits HGDC" }), products.map((product) => product.current ? (_jsxs(DropdownMenuItem, { disabled: true, className: "data-disabled:opacity-100", children: [_jsx(HugeiconsIcon, { icon: DashboardSquare01Icon, "aria-hidden": true }), _jsx("span", { className: "flex-1", children: product.name }), _jsx(HugeiconsIcon, { icon: Tick02Icon, "aria-label": "Produit ouvert" })] }, product.name)) : (_jsxs(MenuPrimitive.LinkItem, { href: product.href, className: MENU_LINK_ITEM, children: [_jsx(HugeiconsIcon, { icon: DashboardSquare01Icon, "aria-hidden": true }), product.name] }, product.name)))] })] })) : null, _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onClick: onSignOut, children: [_jsx(HugeiconsIcon, { icon: Logout01Icon, "aria-hidden": true }), "Se d\u00E9connecter"] })] })] }));
|
|
39
|
+
}
|
package/dist/alert.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type IconSvgElement } from "@hugeicons/react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
export type AlertTone = "info" | "success" | "warning" | "destructive";
|
|
4
|
+
/**
|
|
5
|
+
* Message posé dans la page, non bloquant : une information, un succès durable, un avertissement,
|
|
6
|
+
* une erreur. Lavis de la couleur, icône dans son encre, texte en couleur de texte : la couleur
|
|
7
|
+
* ne porte jamais seule le sens, l'icône et le titre le disent aussi.
|
|
8
|
+
*
|
|
9
|
+
* Une erreur s'annonce tout de suite (`role="alert"`), le reste poliment (`role="status"`).
|
|
10
|
+
* Pour un choix qui bloque, c'est un `Dialog`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function Alert({ tone, title, children, action, icon, className, ...props }: Omit<React.ComponentProps<"div">, "title"> & {
|
|
13
|
+
tone?: AlertTone;
|
|
14
|
+
title?: ReactNode;
|
|
15
|
+
/** Un bouton ou un lien à droite (« Réessayer », « Voir la doc »). */
|
|
16
|
+
action?: ReactNode;
|
|
17
|
+
/** Remplace l'icône du ton. */
|
|
18
|
+
icon?: IconSvgElement;
|
|
19
|
+
}): React.JSX.Element;
|
package/dist/alert.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import Alert02Icon from "@hugeicons/core-free-icons/Alert02Icon";
|
|
3
|
+
import AlertCircleIcon from "@hugeicons/core-free-icons/AlertCircleIcon";
|
|
4
|
+
import CheckmarkCircle02Icon from "@hugeicons/core-free-icons/CheckmarkCircle02Icon";
|
|
5
|
+
import InformationCircleIcon from "@hugeicons/core-free-icons/InformationCircleIcon";
|
|
6
|
+
import { HugeiconsIcon } from "@hugeicons/react";
|
|
7
|
+
import { cn } from "./cn.js";
|
|
8
|
+
const TONES = {
|
|
9
|
+
info: { box: "bg-info/10", icon: "text-info-ink", glyph: InformationCircleIcon },
|
|
10
|
+
success: { box: "bg-status-active/12", icon: "text-status-active-ink", glyph: CheckmarkCircle02Icon },
|
|
11
|
+
warning: { box: "bg-status-paused/14", icon: "text-status-paused-ink", glyph: Alert02Icon },
|
|
12
|
+
destructive: { box: "bg-destructive/10", icon: "text-destructive", glyph: AlertCircleIcon },
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Message posé dans la page, non bloquant : une information, un succès durable, un avertissement,
|
|
16
|
+
* une erreur. Lavis de la couleur, icône dans son encre, texte en couleur de texte : la couleur
|
|
17
|
+
* ne porte jamais seule le sens, l'icône et le titre le disent aussi.
|
|
18
|
+
*
|
|
19
|
+
* Une erreur s'annonce tout de suite (`role="alert"`), le reste poliment (`role="status"`).
|
|
20
|
+
* Pour un choix qui bloque, c'est un `Dialog`.
|
|
21
|
+
*/
|
|
22
|
+
export function Alert({ tone = "info", title, children, action, icon, className, ...props }) {
|
|
23
|
+
const t = TONES[tone];
|
|
24
|
+
return (_jsxs("div", { role: tone === "destructive" ? "alert" : "status", "data-slot": "alert", "data-tone": tone, className: cn("flex flex-wrap items-start gap-x-3 gap-y-2 rounded-2xl px-4 py-3 text-sm sm:flex-nowrap", t.box, className), ...props, children: [_jsx(HugeiconsIcon, { icon: icon ?? t.glyph, strokeWidth: 2, "aria-hidden": true, className: cn("mt-0.5 size-4.5 shrink-0", t.icon) }), _jsxs("div", { className: "min-w-0 flex-1", children: [title ? _jsx("p", { className: "font-medium text-foreground", children: title }) : null, children ? _jsx("div", { className: cn("text-foreground/80", title ? "mt-0.5" : undefined), children: children }) : null] }), action ? _jsx("div", { className: "flex shrink-0 items-center gap-2 max-sm:w-full max-sm:pl-7.5", children: action }) : null] }));
|
|
25
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type IconSvgElement } from "@hugeicons/react";
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
/**
|
|
4
|
+
* À poser dans le `<head>`, avec `themeInitScript` : il remet le rail avant la première
|
|
5
|
+
* peinture, et la variante CSS `rail:` le lit, donc rien ne saute au chargement.
|
|
6
|
+
*/
|
|
7
|
+
export declare const sidebarInitScript: string;
|
|
8
|
+
/** La barre latérale est-elle réduite en rail ? L'état vit sur `<html data-sidebar="rail">`. */
|
|
9
|
+
export declare function useSidebarRail(): boolean;
|
|
10
|
+
export declare function toggleSidebar(): void;
|
|
11
|
+
/** Pour fermer le tiroir mobile depuis un élément de la barre (un sélecteur, un bouton). */
|
|
12
|
+
export declare function useAppShell(): {
|
|
13
|
+
closeMobileNav: () => void;
|
|
14
|
+
};
|
|
15
|
+
/** La navigation du produit : 4 à 7 entrées, à plat. */
|
|
16
|
+
export declare function SidebarNav({ label, className, children, }: {
|
|
17
|
+
/** Nom de la navigation pour les lecteurs d'écran : « Projet », « Organisation ». */
|
|
18
|
+
label: string;
|
|
19
|
+
className?: string;
|
|
20
|
+
children: ReactNode;
|
|
21
|
+
}): React.JSX.Element;
|
|
22
|
+
/**
|
|
23
|
+
* Une entrée de la barre. « Vous êtes ici » par `active` (ou `data-status="active"`, que pose le
|
|
24
|
+
* `Link` de TanStack Router) : teinte de la coque, jamais l'accent. En rail, l'icône seule, et le
|
|
25
|
+
* nom en infobulle.
|
|
26
|
+
*/
|
|
27
|
+
export declare function SidebarNavItem({ icon, label, active, href, render, }: {
|
|
28
|
+
icon: IconSvgElement;
|
|
29
|
+
label: string;
|
|
30
|
+
active?: boolean;
|
|
31
|
+
href?: string;
|
|
32
|
+
/** Lien du routeur : `render={<Link to="…" />}`. */
|
|
33
|
+
render?: React.ReactElement;
|
|
34
|
+
}): React.JSX.Element;
|
|
35
|
+
/**
|
|
36
|
+
* La coque commune des consoles HGDC, celle de Host. Sur ordinateur : colonne teintée à gauche
|
|
37
|
+
* (organisation en haut, navigation, compte en bas), réductible en rail (`Ctrl+B`, mémorisé), et
|
|
38
|
+
* la page sur une carte à côté. Sur téléphone : une barre en haut, le menu dans un tiroir qui
|
|
39
|
+
* monte du bas et se referme quand on choisit une page.
|
|
40
|
+
*
|
|
41
|
+
* Le kit ne connaît ni le routeur ni l'authentification : le produit passe ses éléments
|
|
42
|
+
* (`OrgSwitcher`, `SidebarNav`, `AccountMenu`) et ses liens.
|
|
43
|
+
*/
|
|
44
|
+
export declare function AppShell({ sidebarHeader, nav, sidebarFooter, header, children, mainClassName, }: {
|
|
45
|
+
/** Haut de la barre : `OrgSwitcher`. */
|
|
46
|
+
sidebarHeader?: ReactNode;
|
|
47
|
+
/** `SidebarNav` et ses `SidebarNavItem`. */
|
|
48
|
+
nav: ReactNode;
|
|
49
|
+
/** Bas de la barre : liens secondaires (Docs, Paramètres) et `AccountMenu`. */
|
|
50
|
+
sidebarFooter?: ReactNode;
|
|
51
|
+
/** Barre du haut de la page : sélecteur de projet, recherche (`CommandMenuButton`). */
|
|
52
|
+
header?: ReactNode;
|
|
53
|
+
children: ReactNode;
|
|
54
|
+
/** Largeur et marges du contenu : `max-w-6xl` par défaut. */
|
|
55
|
+
mainClassName?: string;
|
|
56
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import Menu01Icon from "@hugeicons/core-free-icons/Menu01Icon";
|
|
3
|
+
import PanelLeftCloseIcon from "@hugeicons/core-free-icons/PanelLeftCloseIcon";
|
|
4
|
+
import PanelLeftOpenIcon from "@hugeicons/core-free-icons/PanelLeftOpenIcon";
|
|
5
|
+
import { HugeiconsIcon } from "@hugeicons/react";
|
|
6
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
7
|
+
import { createContext, useContext, useEffect, useState, useSyncExternalStore } from "react";
|
|
8
|
+
import { Drawer, DrawerContent, DrawerTitle } from "./drawer.js";
|
|
9
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip.js";
|
|
10
|
+
import { cn } from "./cn.js";
|
|
11
|
+
// ── Rail : la barre latérale réduite aux icônes, mémorisée par navigateur ──────────────
|
|
12
|
+
const RAIL_KEY = "sidebar";
|
|
13
|
+
const RAIL_EVENT = "sidebarchange";
|
|
14
|
+
/**
|
|
15
|
+
* À poser dans le `<head>`, avec `themeInitScript` : il remet le rail avant la première
|
|
16
|
+
* peinture, et la variante CSS `rail:` le lit, donc rien ne saute au chargement.
|
|
17
|
+
*/
|
|
18
|
+
export const sidebarInitScript = `try{if(localStorage.getItem(${JSON.stringify(RAIL_KEY)})==="rail")document.documentElement.dataset.sidebar="rail"}catch(e){}`;
|
|
19
|
+
function subscribeRail(onChange) {
|
|
20
|
+
window.addEventListener(RAIL_EVENT, onChange);
|
|
21
|
+
return () => window.removeEventListener(RAIL_EVENT, onChange);
|
|
22
|
+
}
|
|
23
|
+
/** La barre latérale est-elle réduite en rail ? L'état vit sur `<html data-sidebar="rail">`. */
|
|
24
|
+
export function useSidebarRail() {
|
|
25
|
+
return useSyncExternalStore(subscribeRail, () => document.documentElement.dataset.sidebar === "rail", () => false);
|
|
26
|
+
}
|
|
27
|
+
export function toggleSidebar() {
|
|
28
|
+
const root = document.documentElement;
|
|
29
|
+
const rail = root.dataset.sidebar !== "rail";
|
|
30
|
+
if (rail)
|
|
31
|
+
root.dataset.sidebar = "rail";
|
|
32
|
+
else
|
|
33
|
+
delete root.dataset.sidebar;
|
|
34
|
+
try {
|
|
35
|
+
if (rail)
|
|
36
|
+
localStorage.setItem(RAIL_KEY, "rail");
|
|
37
|
+
else
|
|
38
|
+
localStorage.removeItem(RAIL_KEY);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Stockage bloqué : le choix vaut pour cette page seulement.
|
|
42
|
+
}
|
|
43
|
+
window.dispatchEvent(new Event(RAIL_EVENT));
|
|
44
|
+
}
|
|
45
|
+
/** `Ctrl+B` (`⌘B`) réduit ou déplie la barre latérale, hors des champs de saisie. */
|
|
46
|
+
function useToggleShortcut() {
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
const onKey = (event) => {
|
|
49
|
+
if (event.key.toLowerCase() !== "b" || !(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey)
|
|
50
|
+
return;
|
|
51
|
+
const target = event.target;
|
|
52
|
+
if (target instanceof Element && target.closest("input, textarea, select, [contenteditable=true]"))
|
|
53
|
+
return;
|
|
54
|
+
event.preventDefault();
|
|
55
|
+
toggleSidebar();
|
|
56
|
+
};
|
|
57
|
+
document.addEventListener("keydown", onKey);
|
|
58
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
59
|
+
}, []);
|
|
60
|
+
}
|
|
61
|
+
// ── Navigation ──────────────────────────────────────────────────────────────────────
|
|
62
|
+
const ShellContext = createContext({ closeMobileNav: () => { } });
|
|
63
|
+
/** Pour fermer le tiroir mobile depuis un élément de la barre (un sélecteur, un bouton). */
|
|
64
|
+
export function useAppShell() {
|
|
65
|
+
return useContext(ShellContext);
|
|
66
|
+
}
|
|
67
|
+
/** La navigation du produit : 4 à 7 entrées, à plat. */
|
|
68
|
+
export function SidebarNav({ label, className, children, }) {
|
|
69
|
+
return (_jsx("nav", { "aria-label": label, className: cn("min-h-0 flex-1 overflow-y-auto px-3 py-2", className), children: _jsx("ul", { className: "grid gap-0.5", children: children }) }));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Une entrée de la barre. « Vous êtes ici » par `active` (ou `data-status="active"`, que pose le
|
|
73
|
+
* `Link` de TanStack Router) : teinte de la coque, jamais l'accent. En rail, l'icône seule, et le
|
|
74
|
+
* nom en infobulle.
|
|
75
|
+
*/
|
|
76
|
+
export function SidebarNavItem({ icon, label, active, href, render, }) {
|
|
77
|
+
const rail = useSidebarRail();
|
|
78
|
+
const { closeMobileNav } = useAppShell();
|
|
79
|
+
const link = useRender({
|
|
80
|
+
defaultTagName: "a",
|
|
81
|
+
render,
|
|
82
|
+
props: {
|
|
83
|
+
href,
|
|
84
|
+
"aria-current": active ? "page" : undefined,
|
|
85
|
+
onClick: closeMobileNav,
|
|
86
|
+
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"),
|
|
87
|
+
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 })] })),
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
return (_jsx("li", { children: _jsxs(Tooltip, { disabled: !rail, children: [_jsx(TooltipTrigger, { render: link }), _jsx(TooltipContent, { side: "right", children: label })] }) }));
|
|
91
|
+
}
|
|
92
|
+
// ── Coque ─────────────────────────────────────────────────────────────────────────────
|
|
93
|
+
/**
|
|
94
|
+
* La coque commune des consoles HGDC, celle de Host. Sur ordinateur : colonne teintée à gauche
|
|
95
|
+
* (organisation en haut, navigation, compte en bas), réductible en rail (`Ctrl+B`, mémorisé), et
|
|
96
|
+
* la page sur une carte à côté. Sur téléphone : une barre en haut, le menu dans un tiroir qui
|
|
97
|
+
* monte du bas et se referme quand on choisit une page.
|
|
98
|
+
*
|
|
99
|
+
* Le kit ne connaît ni le routeur ni l'authentification : le produit passe ses éléments
|
|
100
|
+
* (`OrgSwitcher`, `SidebarNav`, `AccountMenu`) et ses liens.
|
|
101
|
+
*/
|
|
102
|
+
export function AppShell({ sidebarHeader, nav, sidebarFooter, header, children, mainClassName, }) {
|
|
103
|
+
const rail = useSidebarRail();
|
|
104
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
105
|
+
useToggleShortcut();
|
|
106
|
+
return (_jsx(ShellContext.Provider, { value: { closeMobileNav: () => setMenuOpen(false) }, children: _jsxs("div", { "data-slot": "app-shell", className: "min-h-dvh lg:grid lg:h-dvh lg:grid-cols-[auto_minmax(0,1fr)] lg:bg-shell", 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" }), _jsxs("aside", { "aria-label": "Barre lat\u00E9rale", className: "hidden overflow-hidden text-shell-foreground transition-[width] duration-(--duration-drawer) ease-ios motion-reduce:transition-none lg:flex lg:w-64 lg:flex-col lg:rail:w-16", children: [_jsxs("div", { className: "mt-2 flex min-h-14 shrink-0 items-center gap-1 px-3 rail:flex-col rail:justify-center rail:px-0 rail:py-1", children: [_jsx("div", { className: "min-w-0 flex-1 rail:flex-none", children: sidebarHeader }), _jsx("button", { type: "button", onClick: toggleSidebar, "aria-label": rail ? "Déplier la barre latérale" : "Réduire la barre latérale", "aria-keyshortcuts": "Control+B Meta+B", title: rail ? "Déplier (Ctrl+B)" : "Réduire (Ctrl+B)", className: "grid size-8 shrink-0 place-items-center rounded-lg 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 active:scale-[0.96]", children: _jsx(HugeiconsIcon, { icon: rail ? PanelLeftCloseIcon : PanelLeftOpenIcon, strokeWidth: 1.8, "aria-hidden": true, className: "size-[1.125rem]" }) })] }), 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", { className: "flex flex-1 flex-col lg:min-h-0 lg:overflow-y-auto lg:rounded-xl lg:bg-background", 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" }) }), _jsx("div", { className: "flex min-w-0 flex-1 items-center gap-2", children: header })] }), _jsx("main", { id: "contenu", tabIndex: -1, 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: children })] }) }), _jsx(Drawer, { side: "bottom", open: menuOpen, onOpenChange: setMenuOpen, children: _jsxs(DrawerContent, { className: "lg:hidden", children: [_jsx(DrawerTitle, { className: "sr-only", children: "Menu" }), sidebarHeader ? _jsx("div", { className: "px-3 pt-1", children: sidebarHeader }) : 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] }) })] }) }));
|
|
107
|
+
}
|
package/dist/card.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
2
|
+
type CardProps = useRender.ComponentProps<"section"> & {
|
|
3
|
+
/** `danger` : la zone des actions irréversibles (supprimer un projet), cerclée de rouge. */
|
|
4
|
+
tone?: "default" | "danger";
|
|
5
|
+
/**
|
|
6
|
+
* Toute la carte est cliquable (`render={<Link to="…" />}`) : le survol fonce la teinte,
|
|
7
|
+
* l'appui l'enfonce à peine. Jamais d'ombre ni de zoom.
|
|
8
|
+
*/
|
|
9
|
+
interactive?: boolean;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Carte de la DA : une teinte (`surface`), ni contour ni ombre, `rounded-3xl`. Un seul niveau
|
|
13
|
+
* de carte par écran ; ce qui est dedans reste à plat.
|
|
14
|
+
*/
|
|
15
|
+
declare function Card({ className, tone, interactive, render, ...props }: CardProps): React.JSX.Element;
|
|
16
|
+
declare function CardHeader({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
|
17
|
+
declare function CardTitle({ className, children, ...props }: React.ComponentProps<"h2">): React.JSX.Element;
|
|
18
|
+
declare function CardDescription({ className, ...props }: React.ComponentProps<"p">): React.JSX.Element;
|
|
19
|
+
/** Action en haut à droite de l'en-tête (un bouton « … », un lien « Tout voir »). */
|
|
20
|
+
declare function CardAction({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
|
21
|
+
declare function CardContent({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
|
22
|
+
/** Pied d'action, comme celui des réglages de Host : un filet, puis les boutons alignés à droite. */
|
|
23
|
+
declare function CardFooter({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
|
24
|
+
export { Card, CardHeader, CardTitle, CardDescription, CardAction, CardContent, CardFooter, type CardProps };
|
package/dist/card.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
3
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
4
|
+
import { cn } from "./cn.js";
|
|
5
|
+
/**
|
|
6
|
+
* Carte de la DA : une teinte (`surface`), ni contour ni ombre, `rounded-3xl`. Un seul niveau
|
|
7
|
+
* de carte par écran ; ce qui est dedans reste à plat.
|
|
8
|
+
*/
|
|
9
|
+
function Card({ className, tone = "default", interactive = false, render, ...props }) {
|
|
10
|
+
return useRender({
|
|
11
|
+
defaultTagName: "section",
|
|
12
|
+
render,
|
|
13
|
+
props: mergeProps({
|
|
14
|
+
"data-slot": "card",
|
|
15
|
+
"data-tone": tone,
|
|
16
|
+
className: cn("flex flex-col overflow-hidden rounded-3xl text-card-foreground", interactive
|
|
17
|
+
? "surface-link pressable-card outline-none focus-visible:ring-3 focus-visible:ring-ring/50"
|
|
18
|
+
: "surface", tone === "danger" && "ring-1 ring-destructive/30 ring-inset", className),
|
|
19
|
+
}, props),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function CardHeader({ className, ...props }) {
|
|
23
|
+
return (_jsx("div", { "data-slot": "card-header", className: cn("grid auto-rows-min items-start gap-1 px-5 pt-5 sm:px-6 sm:pt-6 has-data-[slot=card-action]:grid-cols-[minmax(0,1fr)_auto]", className), ...props }));
|
|
24
|
+
}
|
|
25
|
+
function CardTitle({ className, children, ...props }) {
|
|
26
|
+
return (_jsx("h2", { "data-slot": "card-title", className: cn("text-base font-semibold", className), ...props, children: children }));
|
|
27
|
+
}
|
|
28
|
+
function CardDescription({ className, ...props }) {
|
|
29
|
+
return (_jsx("p", { "data-slot": "card-description", className: cn("max-w-prose text-sm text-muted-foreground", className), ...props }));
|
|
30
|
+
}
|
|
31
|
+
/** Action en haut à droite de l'en-tête (un bouton « … », un lien « Tout voir »). */
|
|
32
|
+
function CardAction({ className, ...props }) {
|
|
33
|
+
return (_jsx("div", { "data-slot": "card-action", className: cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className), ...props }));
|
|
34
|
+
}
|
|
35
|
+
function CardContent({ className, ...props }) {
|
|
36
|
+
return (_jsx("div", { "data-slot": "card-content", className: cn("grid gap-4 px-5 py-5 sm:px-6 sm:py-6 [[data-slot=card-header]+&]:pt-4", className), ...props }));
|
|
37
|
+
}
|
|
38
|
+
/** Pied d'action, comme celui des réglages de Host : un filet, puis les boutons alignés à droite. */
|
|
39
|
+
function CardFooter({ className, ...props }) {
|
|
40
|
+
return (_jsx("div", { "data-slot": "card-footer", className: cn("mt-auto flex flex-wrap items-center justify-end gap-3 border-t border-foreground/5 bg-foreground/[0.02] px-5 py-3 sm:px-6", className), ...props }));
|
|
41
|
+
}
|
|
42
|
+
export { Card, CardHeader, CardTitle, CardDescription, CardAction, CardContent, CardFooter };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox";
|
|
2
|
+
/**
|
|
3
|
+
* Case à cocher. Monochrome : cochée, elle prend l'encre du texte (`primary`), l'accent violet
|
|
4
|
+
* reste au focus. Le nom accessible vient d'un `<label>` qui l'entoure, ou d'un `Field`.
|
|
5
|
+
*
|
|
6
|
+
* ```tsx
|
|
7
|
+
* <label className="flex items-center gap-2.5 text-sm">
|
|
8
|
+
* <Checkbox defaultChecked /> Recevoir le résumé hebdomadaire
|
|
9
|
+
* </label>
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
export declare function Checkbox({ className, indeterminate, ...props }: CheckboxPrimitive.Root.Props): React.JSX.Element;
|
package/dist/checkbox.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox";
|
|
3
|
+
import MinusSignIcon from "@hugeicons/core-free-icons/MinusSignIcon";
|
|
4
|
+
import Tick02Icon from "@hugeicons/core-free-icons/Tick02Icon";
|
|
5
|
+
import { HugeiconsIcon } from "@hugeicons/react";
|
|
6
|
+
import { cn } from "./cn.js";
|
|
7
|
+
/**
|
|
8
|
+
* Case à cocher. Monochrome : cochée, elle prend l'encre du texte (`primary`), l'accent violet
|
|
9
|
+
* reste au focus. Le nom accessible vient d'un `<label>` qui l'entoure, ou d'un `Field`.
|
|
10
|
+
*
|
|
11
|
+
* ```tsx
|
|
12
|
+
* <label className="flex items-center gap-2.5 text-sm">
|
|
13
|
+
* <Checkbox defaultChecked /> Recevoir le résumé hebdomadaire
|
|
14
|
+
* </label>
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export function Checkbox({ className, indeterminate, ...props }) {
|
|
18
|
+
return (_jsx(CheckboxPrimitive.Root, { "data-slot": "checkbox", indeterminate: indeterminate, className: cn("touch-target peer grid size-5 shrink-0 place-items-center rounded-[0.4rem] border border-foreground/25 bg-background text-primary-foreground", "transition-[background-color,border-color,box-shadow] duration-(--duration-fast) ease-out outline-none", "hover:border-foreground/40 focus-visible:ring-3 focus-visible:ring-ring/50", "data-checked:border-primary data-checked:bg-primary data-indeterminate:border-primary data-indeterminate:bg-primary", "data-disabled:cursor-not-allowed data-disabled:opacity-50", "aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-invalid:border-destructive", "dark:bg-white/[0.04]", className), ...props, children: _jsx(CheckboxPrimitive.Indicator, { className: "grid place-items-center data-unchecked:hidden", children: _jsx(HugeiconsIcon, { icon: indeterminate ? MinusSignIcon : Tick02Icon, strokeWidth: 2.5, className: "size-3.5" }) }) }));
|
|
19
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type IconSvgElement } from "@hugeicons/react";
|
|
2
|
+
export type Command = {
|
|
3
|
+
/** Unique dans toute la palette. */
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
icon?: IconSvgElement;
|
|
7
|
+
/** Autres mots qui trouvent la commande : « dns » pour « Ajouter un domaine ». */
|
|
8
|
+
keywords?: Array<string>;
|
|
9
|
+
/** Texte discret à droite : l'organisation d'un projet, « Page ». */
|
|
10
|
+
hint?: string;
|
|
11
|
+
/** Touches affichées à droite, par exemple `["G", "P"]`. */
|
|
12
|
+
shortcut?: Array<string>;
|
|
13
|
+
onSelect: () => void;
|
|
14
|
+
};
|
|
15
|
+
/** Les groupes de la famille, dans cet ordre : « Aller à », « Créer », « Changer ». */
|
|
16
|
+
export type CommandGroup = {
|
|
17
|
+
label: string;
|
|
18
|
+
items: Array<Command>;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Une commande répond à la recherche si chaque mot tapé figure dans son libellé ou ses mots-clés,
|
|
22
|
+
* sans tenir compte des accents ni de la casse : « creer dom » trouve « Créer un domaine ».
|
|
23
|
+
*/
|
|
24
|
+
export declare function matchCommand(command: Pick<Command, "label" | "keywords">, query: string): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* `Ctrl+K` (`⌘K` sur Mac) partout, même dans un champ : c'est le geste qui ouvre tout. Le produit
|
|
27
|
+
* le pose une fois, autour de sa coque.
|
|
28
|
+
*/
|
|
29
|
+
export declare function useCommandMenuShortcut(onToggle: () => void, key?: string): void;
|
|
30
|
+
/**
|
|
31
|
+
* Palette de commandes : chercher une page, créer une ressource, changer d'organisation ou de
|
|
32
|
+
* thème, sans la souris. Bâtie sur l'`Autocomplete` de Base UI rendu dans un `Dialog`, comme
|
|
33
|
+
* l'exemple « Command palette » de sa documentation : flèches pour choisir, Entrée pour lancer,
|
|
34
|
+
* Échap pour fermer, focus rendu à l'élément d'origine.
|
|
35
|
+
*
|
|
36
|
+
* Sans animation, ni à l'ouverture ni à la fermeture : elle s'ouvre des dizaines de fois par
|
|
37
|
+
* jour, au clavier, et doit être là à l'instant où la touche est pressée.
|
|
38
|
+
*/
|
|
39
|
+
export declare function CommandMenu({ groups, open: openProp, onOpenChange, placeholder, emptyLabel, shortcut, }: {
|
|
40
|
+
groups: Array<CommandGroup>;
|
|
41
|
+
open?: boolean;
|
|
42
|
+
onOpenChange?: (open: boolean) => void;
|
|
43
|
+
placeholder?: string;
|
|
44
|
+
emptyLabel?: string;
|
|
45
|
+
/** Ouvrir et fermer par `Ctrl+K`. `false` si le produit gère le raccourci lui-même. */
|
|
46
|
+
shortcut?: boolean;
|
|
47
|
+
}): React.JSX.Element;
|
|
48
|
+
/**
|
|
49
|
+
* Le bouton qui ouvre la palette, pour la souris et le doigt : « Rechercher » et le raccourci.
|
|
50
|
+
* Dans la barre latérale ou l'en-tête.
|
|
51
|
+
*/
|
|
52
|
+
export declare function CommandMenuButton({ onClick, label, className, }: {
|
|
53
|
+
onClick: () => void;
|
|
54
|
+
label?: string;
|
|
55
|
+
className?: string;
|
|
56
|
+
}): React.JSX.Element;
|