@hgdc/ui 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +47 -2
  2. package/dist/avatar.d.ts +11 -0
  3. package/dist/avatar.js +23 -0
  4. package/dist/badge.d.ts +9 -0
  5. package/dist/badge.js +21 -0
  6. package/dist/button.d.ts +19 -0
  7. package/dist/button.js +95 -0
  8. package/dist/cn.d.ts +10 -0
  9. package/dist/cn.js +5 -0
  10. package/dist/dialog.d.ts +14 -0
  11. package/dist/dialog.js +73 -0
  12. package/dist/drawer.d.ts +59 -0
  13. package/dist/drawer.js +77 -0
  14. package/dist/dropdown-menu.d.ts +36 -0
  15. package/dist/dropdown-menu.js +71 -0
  16. package/dist/empty.d.ts +12 -0
  17. package/dist/empty.js +33 -0
  18. package/dist/field.d.ts +34 -0
  19. package/dist/field.js +102 -0
  20. package/dist/floating.d.ts +40 -0
  21. package/dist/floating.js +40 -0
  22. package/dist/input-otp.d.ts +18 -0
  23. package/dist/input-otp.js +26 -0
  24. package/dist/input.d.ts +3 -0
  25. package/dist/input.js +36 -0
  26. package/dist/label.d.ts +3 -0
  27. package/dist/label.js +7 -0
  28. package/dist/segmented-control.d.ts +32 -0
  29. package/dist/segmented-control.js +23 -0
  30. package/dist/select.d.ts +21 -0
  31. package/dist/select.js +73 -0
  32. package/dist/separator.d.ts +3 -0
  33. package/dist/separator.js +7 -0
  34. package/dist/sonner.d.ts +3 -0
  35. package/dist/sonner.js +33 -0
  36. package/dist/spinner.d.ts +20 -0
  37. package/dist/spinner.js +12 -0
  38. package/dist/switch.d.ts +17 -0
  39. package/dist/switch.js +25 -0
  40. package/dist/textarea.d.ts +9 -0
  41. package/dist/textarea.js +16 -0
  42. package/dist/theme.d.ts +21 -0
  43. package/dist/theme.js +68 -0
  44. package/dist/use-media-query.d.ts +23 -0
  45. package/dist/use-media-query.js +51 -0
  46. package/package.json +27 -9
  47. package/styles.css +380 -0
  48. package/dist/index.d.ts +0 -1
  49. package/dist/index.js +0 -2
@@ -0,0 +1,17 @@
1
+ import { Switch as SwitchPrimitive } from "@base-ui/react/switch";
2
+ /**
3
+ * Interrupteur — la forme qu'a un réglage qu'on allume et qu'on éteint.
4
+ *
5
+ * Il remplace le couple « phrase qui décrit l'état + bouton qui le change ».
6
+ * Ces deux-là disaient la même chose deux fois — « Notifications activées »
7
+ * au-dessus de « Désactiver » — et l'un des deux mentait toujours au premier
8
+ * coup d'œil, puisqu'un bouton porte l'action et la phrase porte l'état.
9
+ *
10
+ * Une pastille de texte avait d'abord pris leur place ; elle disait bien l'état
11
+ * mais ne se lisait pas comme un contrôle. L'interrupteur, lui, n'a pas besoin
12
+ * d'être expliqué : sa forme est son mode d'emploi.
13
+ *
14
+ * Dimensions calées sur iOS — 51 × 31 points, soit 3,25 × 2 rem — pour que le
15
+ * pouce le trouve sans viser.
16
+ */
17
+ export declare function Switch({ checked, onCheckedChange, disabled, className, ...props }: SwitchPrimitive.Root.Props): React.JSX.Element;
package/dist/switch.js ADDED
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Switch as SwitchPrimitive } from "@base-ui/react/switch";
3
+ import { cn } from "./cn.js";
4
+ /**
5
+ * Interrupteur — la forme qu'a un réglage qu'on allume et qu'on éteint.
6
+ *
7
+ * Il remplace le couple « phrase qui décrit l'état + bouton qui le change ».
8
+ * Ces deux-là disaient la même chose deux fois — « Notifications activées »
9
+ * au-dessus de « Désactiver » — et l'un des deux mentait toujours au premier
10
+ * coup d'œil, puisqu'un bouton porte l'action et la phrase porte l'état.
11
+ *
12
+ * Une pastille de texte avait d'abord pris leur place ; elle disait bien l'état
13
+ * mais ne se lisait pas comme un contrôle. L'interrupteur, lui, n'a pas besoin
14
+ * d'être expliqué : sa forme est son mode d'emploi.
15
+ *
16
+ * Dimensions calées sur iOS — 51 × 31 points, soit 3,25 × 2 rem — pour que le
17
+ * pouce le trouve sans viser.
18
+ */
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", "disabled:cursor-not-allowed disabled:opacity-50", "bg-muted data-checked:bg-status-active", className), ...props, children: _jsx(SwitchPrimitive.Thumb, { className: cn("size-7 rounded-full bg-background shadow-sm ring-1 ring-foreground/5",
21
+ // `translate` et non `left` : la position part sur le compositeur,
22
+ // le pouce ne voit donc pas la bascule sautiller quand la page est
23
+ // occupée ailleurs.
24
+ "transition-transform duration-200 ease-ios", "translate-x-0 data-checked:translate-x-5") }) }));
25
+ }
@@ -0,0 +1,9 @@
1
+ import * as React from "react";
2
+ /**
3
+ * `field-sizing: content` fait grandir la zone avec le texte, mais ignore
4
+ * `rows` : vide, elle retombait à `min-h-16`, et un exemple de plusieurs lignes
5
+ * en placeholder était coupé (le « Message » d'une annonce, 2026-09-27).
6
+ * `rows` devient donc une hauteur minimale, en lignes (`lh`) plus le padding.
7
+ */
8
+ declare function Textarea({ className, rows, style, ...props }: React.ComponentProps<"textarea">): React.JSX.Element;
9
+ export { Textarea };
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { useFieldDescribedBy, useFieldId } from "./field.js";
4
+ import { cn } from "./cn.js";
5
+ /**
6
+ * `field-sizing: content` fait grandir la zone avec le texte, mais ignore
7
+ * `rows` : vide, elle retombait à `min-h-16`, et un exemple de plusieurs lignes
8
+ * en placeholder était coupé (le « Message » d'une annonce, 2026-09-27).
9
+ * `rows` devient donc une hauteur minimale, en lignes (`lh`) plus le padding.
10
+ */
11
+ function Textarea({ className, rows, style, ...props }) {
12
+ const fieldId = useFieldId();
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-border/70 transition-[color,box-shadow,background-color] duration-(--duration-fast) outline-none ring-inset placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-2 aria-invalid:ring-destructive/60 md:text-sm", className), id: props.id ?? fieldId, ...props }));
15
+ }
16
+ export { Textarea };
@@ -0,0 +1,21 @@
1
+ export type Theme = "dark" | "light" | "system";
2
+ type ThemeProviderProps = {
3
+ children: React.ReactNode;
4
+ defaultTheme?: Theme;
5
+ storageKey?: string;
6
+ };
7
+ export type ThemeProviderState = {
8
+ theme: Theme;
9
+ setTheme: (theme: Theme) => void;
10
+ };
11
+ /**
12
+ * Script anti-flash de thème. DOIT être placé dans le <head>, avant la feuille
13
+ * de styles render-blocking : il pose la classe `.dark`/`.light` sur <html>
14
+ * pendant le parsing du head, donc avant que le CSS ne peigne. Sans ça, le CSS
15
+ * s'applique avec le thème clair par défaut (`:root --background` blanc) le
16
+ * temps que le script tourne → flash blanc au lancement de la PWA.
17
+ */
18
+ export declare const themeInitScript: string;
19
+ export declare function ThemeProvider({ children, defaultTheme, storageKey, }: ThemeProviderProps): React.JSX.Element;
20
+ export declare function useTheme(): ThemeProviderState;
21
+ export {};
package/dist/theme.js ADDED
@@ -0,0 +1,68 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /* eslint-disable react-refresh/only-export-components */
3
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
4
+ const THEME_STORAGE_KEY = "theme";
5
+ // Clair par défaut, quel que soit le réglage du système : le sombre se choisit dans le menu.
6
+ const THEME_DEFAULT = "light";
7
+ function buildThemeScript(storageKey, defaultTheme) {
8
+ return `(function(){try{var k=${JSON.stringify(storageKey)};var d=${JSON.stringify(defaultTheme)};var t=localStorage.getItem(k);if(t!=='light'&&t!=='dark'&&t!=='system'){t=d}var m=matchMedia('(prefers-color-scheme: dark)').matches;var r=t==='system'?(m?'dark':'light'):t;var e=document.documentElement;e.classList.add(r);e.style.colorScheme=r}catch(e){}})();`;
9
+ }
10
+ /**
11
+ * Script anti-flash de thème. DOIT être placé dans le <head>, avant la feuille
12
+ * de styles render-blocking : il pose la classe `.dark`/`.light` sur <html>
13
+ * pendant le parsing du head, donc avant que le CSS ne peigne. Sans ça, le CSS
14
+ * s'applique avec le thème clair par défaut (`:root --background` blanc) le
15
+ * temps que le script tourne → flash blanc au lancement de la PWA.
16
+ */
17
+ export const themeInitScript = buildThemeScript(THEME_STORAGE_KEY, THEME_DEFAULT);
18
+ const ThemeProviderContext = createContext({
19
+ theme: "light",
20
+ setTheme: () => { },
21
+ });
22
+ function resolveTheme(theme) {
23
+ if (theme !== "system")
24
+ return theme;
25
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
26
+ }
27
+ function applyTheme(resolved) {
28
+ const root = document.documentElement;
29
+ root.classList.remove("light", "dark");
30
+ root.classList.add(resolved);
31
+ root.style.colorScheme = resolved;
32
+ }
33
+ function isTheme(value) {
34
+ return value === "light" || value === "dark" || value === "system";
35
+ }
36
+ export function ThemeProvider({ children, defaultTheme = THEME_DEFAULT, storageKey = THEME_STORAGE_KEY, }) {
37
+ const [theme, setThemeState] = useState(() => {
38
+ if (typeof window === "undefined")
39
+ return defaultTheme;
40
+ const stored = localStorage.getItem(storageKey);
41
+ return isTheme(stored) ? stored : defaultTheme;
42
+ });
43
+ const mounted = useRef(false);
44
+ useEffect(() => {
45
+ if (!mounted.current) {
46
+ mounted.current = true;
47
+ return;
48
+ }
49
+ applyTheme(resolveTheme(theme));
50
+ }, [theme]);
51
+ useEffect(() => {
52
+ if (theme !== "system")
53
+ return undefined;
54
+ const media = window.matchMedia("(prefers-color-scheme: dark)");
55
+ const onChange = () => applyTheme(media.matches ? "dark" : "light");
56
+ media.addEventListener("change", onChange);
57
+ return () => media.removeEventListener("change", onChange);
58
+ }, [theme]);
59
+ const setTheme = useCallback((next) => {
60
+ localStorage.setItem(storageKey, next);
61
+ setThemeState(next);
62
+ }, [storageKey]);
63
+ const value = useMemo(() => ({ theme, setTheme }), [theme, setTheme]);
64
+ return _jsx(ThemeProviderContext, { value: value, children: children });
65
+ }
66
+ export function useTheme() {
67
+ return useContext(ThemeProviderContext);
68
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Lecture d'une media query, sûre au rendu serveur.
3
+ *
4
+ * `useSyncExternalStore` rend un `false` stable au serveur (pas de
5
+ * clignotement), puis s'abonne à `matchMedia` une fois hydraté.
6
+ *
7
+ * Un couple `subscribe`/`getSnapshot` par requête, mémorisé au niveau du
8
+ * module. React compare l'IDENTITÉ de `subscribe` : une fonction fabriquée
9
+ * pendant le rendu fait retirer puis reposer l'écouteur `matchMedia` après
10
+ * CHAQUE rendu. Sur le board « Suivi », trente cartes qui se rendent à chaque
11
+ * image d'un glisser, cela faisait des milliers de poses et de retraits
12
+ * d'écouteurs par seconde pour un booléen qui ne bouge pas.
13
+ *
14
+ * `window.matchMedia` est en revanche rappelé à chaque lecture, et non gardé :
15
+ * un test qui remplace la fonction doit voir sa valeur prise en compte.
16
+ *
17
+ * @see https://react.dev/reference/react/useSyncExternalStore — « If you pass a
18
+ * different subscribe function between re-renders, React will re-subscribe to
19
+ * the store. »
20
+ */
21
+ export declare function useMediaQuery(query: string): boolean;
22
+ /** Mobile = viewport < 1024px (matches lg: Tailwind breakpoint). */
23
+ export declare function useIsMobile(): boolean;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Lecture d'une media query, sûre au rendu serveur.
3
+ *
4
+ * `useSyncExternalStore` rend un `false` stable au serveur (pas de
5
+ * clignotement), puis s'abonne à `matchMedia` une fois hydraté.
6
+ *
7
+ * Un couple `subscribe`/`getSnapshot` par requête, mémorisé au niveau du
8
+ * module. React compare l'IDENTITÉ de `subscribe` : une fonction fabriquée
9
+ * pendant le rendu fait retirer puis reposer l'écouteur `matchMedia` après
10
+ * CHAQUE rendu. Sur le board « Suivi », trente cartes qui se rendent à chaque
11
+ * image d'un glisser, cela faisait des milliers de poses et de retraits
12
+ * d'écouteurs par seconde pour un booléen qui ne bouge pas.
13
+ *
14
+ * `window.matchMedia` est en revanche rappelé à chaque lecture, et non gardé :
15
+ * un test qui remplace la fonction doit voir sa valeur prise en compte.
16
+ *
17
+ * @see https://react.dev/reference/react/useSyncExternalStore — « If you pass a
18
+ * different subscribe function between re-renders, React will re-subscribe to
19
+ * the store. »
20
+ */
21
+ import { useSyncExternalStore } from "react";
22
+ const stores = new Map();
23
+ function storeFor(query) {
24
+ const cached = stores.get(query);
25
+ if (cached)
26
+ return cached;
27
+ const store = {
28
+ subscribe: (callback) => {
29
+ if (typeof window === "undefined")
30
+ return noop;
31
+ const list = window.matchMedia(query);
32
+ list.addEventListener("change", callback);
33
+ return () => list.removeEventListener("change", callback);
34
+ },
35
+ getSnapshot: () => typeof window !== "undefined" && window.matchMedia(query).matches,
36
+ };
37
+ stores.set(query, store);
38
+ return store;
39
+ }
40
+ function noop() { }
41
+ function serverSnapshot() {
42
+ return false;
43
+ }
44
+ export function useMediaQuery(query) {
45
+ const store = storeFor(query);
46
+ return useSyncExternalStore(store.subscribe, store.getSnapshot, serverSnapshot);
47
+ }
48
+ /** Mobile = viewport < 1024px (matches lg: Tailwind breakpoint). */
49
+ export function useIsMobile() {
50
+ return useMediaQuery("(max-width: 1023px)");
51
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hgdc/ui",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Interface de la H family : 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",
@@ -15,21 +15,39 @@
15
15
  "access": "public"
16
16
  },
17
17
  "files": [
18
- "dist"
18
+ "dist",
19
+ "styles.css"
20
+ ],
21
+ "sideEffects": [
22
+ "*.css"
19
23
  ],
20
- "sideEffects": false,
21
24
  "exports": {
22
- ".": {
23
- "types": "./dist/index.d.ts",
24
- "default": "./dist/index.js"
25
- },
26
- "./package.json": "./package.json"
25
+ "./styles.css": "./styles.css",
26
+ "./package.json": "./package.json",
27
+ "./*": {
28
+ "types": "./dist/*.d.ts",
29
+ "default": "./dist/*.js"
30
+ }
27
31
  },
28
32
  "scripts": {
29
33
  "build": "tsc -p tsconfig.build.json"
30
34
  },
35
+ "dependencies": {
36
+ "@base-ui/react": "^1.9.0",
37
+ "@fontsource-variable/geist": "^5.3.0",
38
+ "@fontsource-variable/inter": "^5.3.0",
39
+ "@hugeicons/core-free-icons": "^4.3.5",
40
+ "@hugeicons/react": "^1.1.10",
41
+ "class-variance-authority": "^0.7.1",
42
+ "clsx": "^2.1.1",
43
+ "input-otp": "^1.5.0",
44
+ "shadcn": "^4.21.4",
45
+ "sonner": "^2.0.8",
46
+ "tailwind-merge": "^3.6.0"
47
+ },
31
48
  "peerDependencies": {
32
49
  "react": "^19.3.0",
33
- "react-dom": "^19.3.0"
50
+ "react-dom": "^19.3.0",
51
+ "tailwindcss": "^4.3.3"
34
52
  }
35
53
  }
package/styles.css ADDED
@@ -0,0 +1,380 @@
1
+ /*
2
+ * @hgdc/ui : jetons, thèmes clair et sombre, polices et utilitaires de la DA HGDC.
3
+ *
4
+ * À importer juste après Tailwind, avant les styles propres au produit :
5
+ *
6
+ * @import "tailwindcss";
7
+ * @import "@hgdc/ui/styles.css";
8
+ *
9
+ * Un produit redéfinit un jeton (`--accent`, `--chart-1`…) dans son propre `:root` ou `.dark`,
10
+ * après cet import.
11
+ */
12
+
13
+ @import "shadcn/tailwind.css";
14
+ @import "@fontsource-variable/geist";
15
+ @import "@fontsource-variable/inter";
16
+
17
+ /* Tailwind ignore node_modules : les classes des composants du kit sont déclarées ici. */
18
+ @source "./dist";
19
+
20
+ @custom-variant dark (&:is(.dark *));
21
+
22
+ /* Courbes et durées. Pas d'ease-in : il démarre lentement à l'instant où l'œil regarde. */
23
+ @theme {
24
+ --ease-ios: cubic-bezier(0.2, 0, 0, 1);
25
+ --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
26
+ --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
27
+ --ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
28
+ --duration-instant: 100ms;
29
+ --duration-fast: 150ms;
30
+ --duration-base: 200ms;
31
+ --duration-slow: 300ms;
32
+ --duration-drawer: 350ms;
33
+ }
34
+
35
+ @theme inline {
36
+ --font-sans: "Geist Variable", ui-sans-serif, system-ui, sans-serif;
37
+ --font-heading: "Inter Variable", ui-sans-serif, system-ui, sans-serif;
38
+ --font-mono: ui-monospace, "SFMono-Regular", "Cascadia Code", Consolas, monospace;
39
+ --color-background: var(--background);
40
+ --color-foreground: var(--foreground);
41
+ --color-card: var(--card);
42
+ --color-card-foreground: var(--card-foreground);
43
+ --color-popover: var(--popover);
44
+ --color-popover-foreground: var(--popover-foreground);
45
+ --color-primary: var(--primary);
46
+ --color-primary-foreground: var(--primary-foreground);
47
+ --color-secondary: var(--secondary);
48
+ --color-secondary-foreground: var(--secondary-foreground);
49
+ --color-muted: var(--muted);
50
+ --color-muted-foreground: var(--muted-foreground);
51
+ --color-accent: var(--accent);
52
+ --color-accent-foreground: var(--accent-foreground);
53
+ --color-accent-soft: var(--accent-soft);
54
+ --color-destructive: var(--destructive);
55
+ --color-border: var(--border);
56
+ --color-input: var(--input);
57
+ --color-ring: var(--ring);
58
+ --color-shell: var(--shell);
59
+ --color-shell-foreground: var(--shell-foreground);
60
+ --color-shell-muted: var(--shell-muted-foreground);
61
+ --color-shell-hover: var(--shell-hover);
62
+ --color-shell-active: var(--shell-active);
63
+ --color-success: var(--success);
64
+ --color-success-foreground: var(--success-foreground);
65
+ --color-status-active: var(--status-active);
66
+ --color-status-paused: var(--status-paused);
67
+ --color-status-done: var(--status-done);
68
+ --color-status-active-ink: var(--status-active-ink);
69
+ --color-status-paused-ink: var(--status-paused-ink);
70
+ --color-info: var(--info);
71
+ --color-info-ink: var(--info-ink);
72
+ --color-chart-1: var(--chart-1);
73
+ --color-inverse: var(--inverse);
74
+ --color-inverse-foreground: var(--inverse-foreground);
75
+ --color-inverse-muted: var(--inverse-muted);
76
+ --color-inverse-code: var(--inverse-code);
77
+ --radius-sm: calc(var(--radius) * 0.6);
78
+ --radius-md: calc(var(--radius) * 0.8);
79
+ --radius-lg: var(--radius);
80
+ --radius-xl: calc(var(--radius) * 1.4);
81
+ --radius-2xl: calc(var(--radius) * 1.8);
82
+ --radius-3xl: calc(var(--radius) * 2.2);
83
+ --radius-4xl: calc(var(--radius) * 2.6);
84
+ }
85
+
86
+ /* Chrome monochrome OKLCH, un seul accent violet, rare : focus, lien, élément actif,
87
+ un geste principal par écran. Les couleurs vives restent aux graphiques et aux statuts. */
88
+ :root {
89
+ --background: oklch(1 0 0);
90
+ --foreground: oklch(0.145 0 0);
91
+ --card: oklch(1 0 0);
92
+ --card-foreground: oklch(0.145 0 0);
93
+ /* Synonymes gardés pour les primitives shadcn : ils suivent leur jeton de rôle. */
94
+ --popover: var(--card);
95
+ --popover-foreground: var(--card-foreground);
96
+ /* Menus et sélecteurs (`floating.ts`) : fond opaque, un cran au-dessus de la carte en sombre. */
97
+ --floating: oklch(1 0 0);
98
+ --floating-muted: oklch(0.97 0 0);
99
+ --primary: oklch(0.205 0 0);
100
+ --primary-foreground: oklch(0.985 0 0);
101
+ --secondary: var(--muted);
102
+ --secondary-foreground: oklch(0.205 0 0);
103
+ --muted: oklch(0.97 0 0);
104
+ /* L=0.52 : 4,5:1 sur `muted`. */
105
+ --muted-foreground: oklch(0.52 0 0);
106
+ /* `--brand-accent*` : couleur d’un client posée par le produit, sans toucher au kit. */
107
+ --accent: var(--brand-accent, oklch(0.55 0.25 296));
108
+ --accent-foreground: var(--brand-accent-foreground, oklch(0.985 0 0));
109
+ --accent-soft: color-mix(in oklch, var(--accent) 10%, transparent);
110
+ --destructive: oklch(0.577 0.245 27.325);
111
+ --border: oklch(0.922 0 0);
112
+ --input: oklch(0.922 0 0);
113
+ --ring: var(--accent);
114
+ /* Coque de l'application : barre latérale et en-tête. */
115
+ --shell: oklch(0.955 0 0);
116
+ --shell-foreground: oklch(0.145 0 0);
117
+ --shell-muted-foreground: oklch(0.45 0 0);
118
+ --shell-hover: oklch(0 0 0 / 5%);
119
+ --shell-active: oklch(0 0 0 / 8%);
120
+ --chart-1: oklch(0.62 0.19 256);
121
+ --status-active: oklch(0.55 0.15 156);
122
+ --status-paused: oklch(0.62 0.14 75);
123
+ --status-done: oklch(0.52 0.2 293);
124
+ /* Encres : le texte d'un statut sur son lavis (4,5:1), la couleur vive restant aux pastilles. */
125
+ --status-active-ink: oklch(0.45 0.12 155);
126
+ --status-paused-ink: oklch(0.5 0.12 70);
127
+ /* Information : bleu, pour que le violet reste l'accent. */
128
+ --info: oklch(0.62 0.19 256);
129
+ --info-ink: oklch(0.48 0.17 256);
130
+ --success: oklch(0.58 0.15 152);
131
+ --success-foreground: oklch(0.985 0 0);
132
+ /* Panneau inversé de la vitrine : encre sombre dans les deux thèmes. */
133
+ --inverse: oklch(0.205 0 0);
134
+ --inverse-foreground: oklch(0.985 0 0);
135
+ --inverse-muted: oklch(0.708 0 0);
136
+ --inverse-code: oklch(0.82 0.11 296);
137
+ --radius: 0.85rem;
138
+ --scrollbar-thumb: color-mix(in oklch, var(--foreground) 18%, transparent);
139
+ }
140
+
141
+ .dark {
142
+ --background: oklch(0.145 0 0);
143
+ --foreground: oklch(0.985 0 0);
144
+ --card: oklch(0.205 0 0);
145
+ --card-foreground: oklch(0.985 0 0);
146
+ --floating: oklch(0.255 0 0);
147
+ --floating-muted: oklch(0.315 0 0);
148
+ --primary: oklch(0.922 0 0);
149
+ --primary-foreground: oklch(0.205 0 0);
150
+ --secondary-foreground: oklch(0.985 0 0);
151
+ --muted: oklch(0.269 0 0);
152
+ --muted-foreground: oklch(0.708 0 0);
153
+ --accent: var(--brand-accent-dark, oklch(0.74 0.19 296));
154
+ --accent-foreground: var(--brand-accent-dark-foreground, oklch(0.205 0 0));
155
+ --destructive: oklch(0.704 0.191 22.216);
156
+ --border: oklch(1 0 0 / 10%);
157
+ --input: oklch(1 0 0 / 15%);
158
+ --shell: oklch(0.08 0 0);
159
+ --shell-foreground: oklch(0.985 0 0);
160
+ --shell-muted-foreground: oklch(0.708 0 0);
161
+ --shell-hover: oklch(1 0 0 / 6%);
162
+ --shell-active: oklch(1 0 0 / 12%);
163
+ --chart-1: oklch(0.68 0.16 256);
164
+ --status-active: oklch(0.74 0.14 156);
165
+ --status-paused: oklch(0.82 0.15 86);
166
+ --status-done: oklch(0.68 0.17 293);
167
+ --status-active-ink: var(--status-active);
168
+ --status-paused-ink: var(--status-paused);
169
+ --info: oklch(0.7 0.16 256);
170
+ --info-ink: var(--info);
171
+ --success: oklch(0.7 0.15 152);
172
+ --success-foreground: oklch(0.16 0.02 152);
173
+ --inverse: oklch(0.1 0 0);
174
+ }
175
+
176
+ @layer base {
177
+ /* Une grille sans colonnes déclarées a une seule piste qui ne s'élargit jamais pour un
178
+ texte long : sans ça, `min-width: auto` pousse la page en défilement horizontal sur
179
+ téléphone. Les `grid-cols-*` (couche utilities) passent devant. */
180
+ .grid {
181
+ grid-template-columns: minmax(0, 1fr);
182
+ }
183
+ * {
184
+ @apply border-border outline-ring/50;
185
+ }
186
+ ::-webkit-scrollbar {
187
+ width: 6px;
188
+ height: 6px;
189
+ }
190
+ ::-webkit-scrollbar-track {
191
+ background: transparent;
192
+ }
193
+ ::-webkit-scrollbar-thumb {
194
+ background: var(--scrollbar-thumb);
195
+ border-radius: 9999px;
196
+ }
197
+ html {
198
+ @apply font-sans;
199
+ -webkit-font-smoothing: antialiased;
200
+ -moz-osx-font-smoothing: grayscale;
201
+ overflow-x: clip;
202
+ }
203
+ body {
204
+ @apply bg-background text-foreground;
205
+ min-height: 100dvh;
206
+ }
207
+ h1,
208
+ h2,
209
+ h3 {
210
+ @apply font-heading;
211
+ text-wrap: balance;
212
+ letter-spacing: -0.02em;
213
+ }
214
+ ::view-transition-group(root) {
215
+ animation-duration: var(--duration-slow);
216
+ animation-timing-function: var(--ease-ios);
217
+ }
218
+ }
219
+
220
+ /* ── Surfaces ──────────────────────────────────────────────────────────── */
221
+
222
+ /* Niveau 1 de la DA : une teinte, ni ombre ni bordure. `muted` en clair, `card` en sombre
223
+ (un cran plus clair que la page). */
224
+ @utility surface {
225
+ background-color: var(--muted);
226
+ :where(.dark) & {
227
+ background-color: var(--card);
228
+ }
229
+ }
230
+
231
+ /* Surface cliquable : le survol fonce la teinte, sans ombre. */
232
+ @utility surface-link {
233
+ background-color: var(--muted);
234
+ transition: background-color var(--duration-fast) var(--ease-out);
235
+ &:hover {
236
+ background-color: oklch(0.94 0 0);
237
+ }
238
+ :where(.dark) & {
239
+ background-color: var(--card);
240
+ }
241
+ :where(.dark) &:hover {
242
+ background-color: var(--muted);
243
+ }
244
+ }
245
+
246
+ /* ── Mouvement ─────────────────────────────────────────────────────────── */
247
+
248
+ @utility pressable {
249
+ transition:
250
+ transform var(--duration-slow) var(--ease-bounce),
251
+ background-color var(--duration-fast) var(--ease-out);
252
+ &:active {
253
+ transform: scale(0.96);
254
+ transition-duration: var(--duration-instant);
255
+ transition-timing-function: var(--ease-out);
256
+ }
257
+ }
258
+
259
+ @utility pressable-card {
260
+ transition:
261
+ transform var(--duration-slow) var(--ease-bounce),
262
+ background-color var(--duration-fast) var(--ease-out);
263
+ &:active {
264
+ transform: scale(0.985);
265
+ transition-duration: var(--duration-instant);
266
+ transition-timing-function: var(--ease-out);
267
+ }
268
+ }
269
+
270
+ /* Zone tactile de 44 px sur écran tactile, sans changer la taille visible. */
271
+ @utility touch-target {
272
+ @media (hover: none) and (pointer: coarse) {
273
+ position: relative;
274
+ &::after {
275
+ content: "";
276
+ position: absolute;
277
+ top: 50%;
278
+ left: 50%;
279
+ translate: -50% -50%;
280
+ min-width: 44px;
281
+ min-height: 44px;
282
+ width: 100%;
283
+ height: 100%;
284
+ }
285
+ }
286
+ }
287
+
288
+ @utility touch-field {
289
+ @media (hover: none) and (pointer: coarse) {
290
+ min-height: 2.75rem;
291
+ }
292
+ }
293
+
294
+ @utility date-field {
295
+ appearance: none;
296
+ }
297
+
298
+ @keyframes soft-in {
299
+ from {
300
+ opacity: 0;
301
+ transform: translateY(4px);
302
+ }
303
+ }
304
+ @utility soft-in {
305
+ animation: soft-in var(--duration-base) var(--ease-out) both;
306
+ }
307
+
308
+ @keyframes pop-in {
309
+ from {
310
+ opacity: 0;
311
+ scale: 0.6;
312
+ }
313
+ }
314
+ @utility pop-in {
315
+ animation: pop-in var(--duration-base) var(--ease-ios) both;
316
+ }
317
+
318
+ /* Une ligne qui arrive dans un flux en direct : elle se pose, une seule fois. */
319
+ @utility card-in {
320
+ transition:
321
+ opacity var(--duration-base) var(--ease-out),
322
+ translate var(--duration-base) var(--ease-out),
323
+ background-color var(--duration-slow) var(--ease-out);
324
+ @starting-style {
325
+ opacity: 0;
326
+ translate: 0 -6px;
327
+ }
328
+ }
329
+
330
+ @keyframes skeleton-pulse {
331
+ 50% {
332
+ opacity: 0.6;
333
+ }
334
+ }
335
+ @utility skeleton-pulse {
336
+ animation: skeleton-pulse 1.8s ease-in-out infinite;
337
+ }
338
+
339
+ /* Le point « en direct » : un anneau qui s'élargit et s'efface. */
340
+ @keyframes live-ping {
341
+ from {
342
+ transform: scale(0.8);
343
+ opacity: 0.6;
344
+ }
345
+ to {
346
+ transform: scale(2.2);
347
+ opacity: 0;
348
+ }
349
+ }
350
+ @utility live-ping {
351
+ animation: live-ping 1.6s var(--ease-out) infinite;
352
+ }
353
+
354
+ /* Des étapes qui montent l'une après l'autre au premier affichage. */
355
+ @keyframes rise {
356
+ from {
357
+ opacity: 0;
358
+ transform: translateY(10px);
359
+ }
360
+ }
361
+ @utility rise {
362
+ animation: rise 420ms var(--ease-ios) both;
363
+ }
364
+
365
+ @media (prefers-reduced-motion: reduce) {
366
+ .soft-in,
367
+ .pop-in,
368
+ .rise,
369
+ .live-ping,
370
+ .skeleton-pulse {
371
+ animation: none;
372
+ }
373
+ .pressable:active,
374
+ .pressable-card:active {
375
+ transform: none;
376
+ }
377
+ .card-in {
378
+ transition: none;
379
+ }
380
+ }
package/dist/index.d.ts DELETED
@@ -1 +0,0 @@
1
- export {};
package/dist/index.js DELETED
@@ -1,2 +0,0 @@
1
- export {};
2
- // Paquet réservé : contenu livré par le chantier A de la vague 1 (HANDOFF.md).