@hgdc/ui 0.54.0 → 0.55.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/dist/avatar.js CHANGED
@@ -2,23 +2,13 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar";
3
3
  import * as React from "react";
4
4
  import { cn } from "./cn.js";
5
- import { avatarTone } from "./initials.js";
5
+ import { AVATAR_TONE_CLASSES, avatarTone } from "./initials.js";
6
6
  function Avatar({ className, size = "default", ...props }) {
7
7
  return (_jsx(AvatarPrimitive.Root, { "data-slot": "avatar", "data-size": size, className: cn("group/avatar relative flex size-8 shrink-0 rounded-full select-none after:absolute after:inset-0 after:rounded-full after:border after:border-border after:mix-blend-darken data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten", className), ...props }));
8
8
  }
9
9
  function AvatarImage({ className, ...props }) {
10
10
  return (_jsx(AvatarPrimitive.Image, { "data-slot": "avatar-image", className: cn("aspect-square size-full rounded-full object-cover", className), ...props }));
11
11
  }
12
- /** Lavis et encre de chaque teinte (`--avatar-<n>`, `styles.css`), à 4,5:1 en clair et en sombre. */
13
- const TONE_CLASSES = {
14
- 1: "bg-avatar-1 text-avatar-1-ink",
15
- 2: "bg-avatar-2 text-avatar-2-ink",
16
- 3: "bg-avatar-3 text-avatar-3-ink",
17
- 4: "bg-avatar-4 text-avatar-4-ink",
18
- 5: "bg-avatar-5 text-avatar-5-ink",
19
- 6: "bg-avatar-6 text-avatar-6-ink",
20
- 7: "bg-avatar-7 text-avatar-7-ink",
21
- };
22
12
  /**
23
13
  * Ce qui s'affiche sans photo (la photo, `AvatarImage`, reste prioritaire). Des initiales en
24
14
  * texte prennent une teinte de la marque, stable : celle de `colorKey` (identifiant ou adresse
@@ -28,7 +18,7 @@ const TONE_CLASSES = {
28
18
  function AvatarFallback({ className, colorKey, children, ...props }) {
29
19
  const key = colorKey === undefined ? (typeof children === "string" ? children : "") : (colorKey ?? "");
30
20
  const tone = key.trim() ? avatarTone(key) : null;
31
- return (_jsx(AvatarPrimitive.Fallback, { "data-slot": "avatar-fallback", "data-tone": tone ?? undefined, className: cn("flex size-full items-center justify-center rounded-full text-sm group-data-[size=sm]/avatar:text-xs", tone ? cn("font-semibold", TONE_CLASSES[tone]) : "bg-muted text-muted-foreground", className), ...props, children: children }));
21
+ return (_jsx(AvatarPrimitive.Fallback, { "data-slot": "avatar-fallback", "data-tone": tone ?? undefined, className: cn("flex size-full items-center justify-center rounded-full text-sm group-data-[size=sm]/avatar:text-xs", tone ? cn("font-semibold", AVATAR_TONE_CLASSES[tone]) : "bg-muted text-muted-foreground", className), ...props, children: children }));
32
22
  }
33
23
  function AvatarBadge({ className, ...props }) {
34
24
  return (_jsx("span", { "data-slot": "avatar-badge", className: cn("absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground bg-blend-color ring-2 ring-background select-none", "group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden", "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2", "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2", className), ...props }));
@@ -15,3 +15,10 @@ export type AvatarTone = 1 | 2 | 3 | 4 | 5 | 6 | 7;
15
15
  * toujours la même teinte, dans tous les produits. FNV-1a 32 bits, puis modulo 7.
16
16
  */
17
17
  export declare function avatarTone(key: string): AvatarTone;
18
+ /** Lavis et encre de chaque teinte (`--avatar-<n>`, `styles.css`), à 4,5:1 en clair et en sombre. */
19
+ export declare const AVATAR_TONE_CLASSES: Readonly<Record<AvatarTone, string>>;
20
+ /** Teinte de la marque d'une organisation (`OrgMark`, mémoire de la coque) : celle de son identifiant, sinon de son nom. */
21
+ export declare function orgTone(org: {
22
+ id?: string | undefined;
23
+ name: string;
24
+ }): AvatarTone;
package/dist/initials.js CHANGED
@@ -36,3 +36,17 @@ export function avatarTone(key) {
36
36
  }
37
37
  return ((hash % AVATAR_TONES) + 1);
38
38
  }
39
+ /** Lavis et encre de chaque teinte (`--avatar-<n>`, `styles.css`), à 4,5:1 en clair et en sombre. */
40
+ export const AVATAR_TONE_CLASSES = {
41
+ 1: "bg-avatar-1 text-avatar-1-ink",
42
+ 2: "bg-avatar-2 text-avatar-2-ink",
43
+ 3: "bg-avatar-3 text-avatar-3-ink",
44
+ 4: "bg-avatar-4 text-avatar-4-ink",
45
+ 5: "bg-avatar-5 text-avatar-5-ink",
46
+ 6: "bg-avatar-6 text-avatar-6-ink",
47
+ 7: "bg-avatar-7 text-avatar-7-ink",
48
+ };
49
+ /** Teinte de la marque d'une organisation (`OrgMark`, mémoire de la coque) : celle de son identifiant, sinon de son nom. */
50
+ export function orgTone(org) {
51
+ return avatarTone(org.id?.trim() ? org.id : org.name);
52
+ }
@@ -19,9 +19,15 @@ export type Organization = {
19
19
  };
20
20
  /** Un lien dans un menu, à l'allure d'un `DropdownMenuItem`. */
21
21
  export declare const MENU_LINK_ITEM: string;
22
- /** Marque d'une organisation : son logo, ou ses initiales (`initials`) sur un carré arrondi. */
22
+ /**
23
+ * Marque d'une organisation : son logo, ou ses initiales (`initials`) sur un carré arrondi, dans la teinte de
24
+ * son identifiant (`avatarTone`) : deux organisations aux mêmes initiales ne se confondent pas. Sans `id`, la
25
+ * teinte du nom.
26
+ */
23
27
  export declare function OrgMark({ org, className, }: {
24
- org: Pick<Organization, "name" | "logo">;
28
+ org: Pick<Organization, "name" | "logo"> & {
29
+ id?: string;
30
+ };
25
31
  className?: string;
26
32
  }): React.JSX.Element;
27
33
  /**
@@ -7,7 +7,7 @@ import { Menu as MenuPrimitive } from "@base-ui/react/menu";
7
7
  import { useEffect } from "react";
8
8
  import { Badge } from "./badge.js";
9
9
  import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "./dropdown-menu.js";
10
- import { initials } from "./initials.js";
10
+ import { AVATAR_TONE_CLASSES, initials, orgTone } from "./initials.js";
11
11
  import { rememberShellOrganization } from "./shell-memory.js";
12
12
  import { useShellPlacement } from "./shell-placement.js";
13
13
  import { cn } from "./cn.js";
@@ -25,11 +25,15 @@ export function RoleBadge({ orgRole: role, title, className, }) {
25
25
  }
26
26
  /** Un lien dans un menu, à l'allure d'un `DropdownMenuItem`. */
27
27
  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";
28
- /** Marque d'une organisation : son logo, ou ses initiales (`initials`) sur un carré arrondi. */
28
+ /**
29
+ * Marque d'une organisation : son logo, ou ses initiales (`initials`) sur un carré arrondi, dans la teinte de
30
+ * son identifiant (`avatarTone`) : deux organisations aux mêmes initiales ne se confondent pas. Sans `id`, la
31
+ * teinte du nom.
32
+ */
29
33
  export function OrgMark({ org, className, }) {
30
34
  if (org.logo)
31
35
  return _jsx("img", { src: org.logo, alt: "", className: cn("size-8 shrink-0 rounded-lg object-cover", className) });
32
- return (_jsx("span", { "aria-hidden": true, className: cn("grid size-8 shrink-0 place-items-center rounded-lg bg-foreground text-xs font-semibold text-background uppercase", className), children: initials(org.name) }));
36
+ return (_jsx("span", { "aria-hidden": true, className: cn("grid size-8 shrink-0 place-items-center rounded-lg text-xs font-semibold uppercase", AVATAR_TONE_CLASSES[orgTone(org)], className), children: initials(org.name) }));
33
37
  }
34
38
  /**
35
39
  * L'organisation active, et le menu pour en changer ou en créer une. En haut de la barre des
@@ -46,17 +50,17 @@ export function OrgSwitcher({ organizations, activeId, onSelect, createHref, onC
46
50
  const activeLogo = active?.logo;
47
51
  useEffect(() => {
48
52
  if (activeName !== undefined)
49
- rememberShellOrganization({ name: activeName, logo: activeLogo });
50
- }, [activeName, activeLogo]);
53
+ rememberShellOrganization({ id: activeId, name: activeName, logo: activeLogo });
54
+ }, [activeId, activeName, activeLogo]);
51
55
  if (!active)
52
56
  return null;
53
57
  // En haut de la barre des produits : la marque seule dans 40 px, le nom quand la barre s'élargit.
54
58
  const inRail = placement === "product-rail";
55
- 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
56
- ? "shrink-0 gap-3 px-1 hover:bg-shell-hover aria-expanded:bg-shell-hover"
59
+ return (_jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { render: _jsx("button", { type: "button", "aria-label": `Organisation ${active.name}, changer d'organisation`, title: active.name, className: cn("flex min-h-10 w-full min-w-0 items-center gap-2.5 rounded-xl px-1.5 py-1 text-left coarse:min-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
60
+ ? "h-10 shrink-0 gap-3 px-1 py-0 hover:bg-shell-hover aria-expanded:bg-shell-hover coarse:h-11"
57
61
  : "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
58
62
  ? "opacity-0 transition-opacity duration-(--duration-fast) ease-out group-data-expanded/rail:opacity-100 motion-reduce:transition-none"
59
- : "lg:rail:hidden"), children: [_jsx("span", { className: "block truncate text-sm font-medium", children: active.name }), active.role ? (_jsx("span", { className: "block truncate text-xs text-muted-foreground lg:text-shell-muted", children: ORG_ROLE_LABEL[active.role] })) : null] }), _jsx(HugeiconsIcon, { icon: UnfoldMoreIcon, "aria-hidden": true, className: cn("size-4 shrink-0 text-muted-foreground lg:text-shell-muted", inRail
63
+ : "lg:rail:hidden"), children: [_jsx("span", { className: cn("block text-sm font-medium", inRail ? "truncate" : "line-clamp-2 break-words"), children: active.name }), active.role ? (_jsx("span", { className: "block truncate text-xs text-muted-foreground lg:text-shell-muted", children: ORG_ROLE_LABEL[active.role] })) : null] }), _jsx(HugeiconsIcon, { icon: UnfoldMoreIcon, "aria-hidden": true, className: cn("size-4 shrink-0 text-muted-foreground lg:text-shell-muted", inRail
60
64
  ? "opacity-0 transition-opacity duration-(--duration-fast) group-data-expanded/rail:opacity-100"
61
- : "lg:rail:hidden") })] }), _jsxs(DropdownMenuContent, { align: "start", className: "w-64", children: [_jsxs(DropdownMenuGroup, { children: [_jsx(DropdownMenuLabel, { children: "Organisations" }), organizations.map((org) => (_jsxs(DropdownMenuItem, { onClick: () => onSelect(org.id), children: [_jsx(OrgMark, { org: org, className: "size-6 rounded-md bg-muted text-[10px] text-foreground" }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: org.name }), org.id === activeId ? (_jsx(HugeiconsIcon, { icon: Tick02Icon, "aria-label": "Organisation active", className: "size-4" })) : org.role ? (_jsx("span", { className: "text-xs font-normal text-muted-foreground", children: ORG_ROLE_LABEL[org.role] })) : null] }, org.id)))] }), createHref !== undefined ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsxs(MenuPrimitive.LinkItem, { href: createHref, className: MENU_LINK_ITEM, children: [_jsx(HugeiconsIcon, { icon: PlusSignIcon, "aria-hidden": true }), "Cr\u00E9er une organisation"] })] })) : onCreate !== undefined ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onClick: onCreate, children: [_jsx(HugeiconsIcon, { icon: PlusSignIcon, "aria-hidden": true }), "Cr\u00E9er une organisation"] })] })) : null] })] }));
65
+ : "lg:rail:hidden") })] }), _jsxs(DropdownMenuContent, { align: "start", className: "w-64", children: [_jsxs(DropdownMenuGroup, { children: [_jsx(DropdownMenuLabel, { children: "Organisations" }), organizations.map((org) => (_jsxs(DropdownMenuItem, { onClick: () => onSelect(org.id), children: [_jsx(OrgMark, { org: org, className: "size-6 rounded-md text-[10px]" }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: org.name }), org.id === activeId ? (_jsx(HugeiconsIcon, { icon: Tick02Icon, "aria-label": "Organisation active", className: "size-4" })) : org.role ? (_jsx("span", { className: "text-xs font-normal text-muted-foreground", children: ORG_ROLE_LABEL[org.role] })) : null] }, org.id)))] }), createHref !== undefined ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsxs(MenuPrimitive.LinkItem, { href: createHref, className: MENU_LINK_ITEM, children: [_jsx(HugeiconsIcon, { icon: PlusSignIcon, "aria-hidden": true }), "Cr\u00E9er une organisation"] })] })) : onCreate !== undefined ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onClick: onCreate, children: [_jsx(HugeiconsIcon, { icon: PlusSignIcon, "aria-hidden": true }), "Cr\u00E9er une organisation"] })] })) : null] })] }));
62
66
  }
@@ -9,6 +9,7 @@ export declare const SHELL_SWITCH_MAX_AGE_MS = 10000;
9
9
  export type ShellOrganization = {
10
10
  initials: string;
11
11
  logo?: string;
12
+ tone?: number;
12
13
  };
13
14
  export type ShellBootConfig = {
14
15
  /** Clé du rail de la barre latérale (`sidebar`), valeur `rail`. */
@@ -21,6 +22,8 @@ export type ShellBootConfig = {
21
22
  origins: ReadonlyArray<readonly [string, string, string]>;
22
23
  /** Domaine des produits sur leur sous-domaine (`hgdc.ch` : `mail.hgdc.ch` est Mail). */
23
24
  domain: string;
25
+ /** Nombre de teintes d'avatar (`AVATAR_TONES`) : la teinte retenue de l'organisation est un entier de 1 à `tones`. */
26
+ tones: number;
24
27
  };
25
28
  type BootWindow = {
26
29
  document: {
@@ -46,11 +49,13 @@ export declare const SHELL_BOOT_CONFIG: ShellBootConfig;
46
49
  export declare function shellBootScript(config?: ShellBootConfig): string;
47
50
  /** Ce que la coque retient d'une organisation : ses initiales et son logo (`https:` seulement). */
48
51
  export declare function shellOrganization(org: {
52
+ id?: string | undefined;
49
53
  name: string;
50
54
  logo?: string | undefined;
51
55
  }): ShellOrganization;
52
56
  /** Retient l'organisation affichée (`OrgSwitcher` le fait) et l'applique à la page. */
53
57
  export declare function rememberShellOrganization(org: {
58
+ id?: string | undefined;
54
59
  name: string;
55
60
  logo?: string | undefined;
56
61
  }): void;
@@ -1,4 +1,4 @@
1
- import { initials } from "./initials.js";
1
+ import { AVATAR_TONES, initials, orgTone } from "./initials.js";
2
2
  import { FAMILY_PRODUCTS } from "./family-svg.js";
3
3
  import { PRODUCT_ORIGINS } from "./products.js";
4
4
  /*
@@ -77,11 +77,26 @@ export function shellBoot(config, win) {
77
77
  root.style.setProperty("--hgdc-org-logo", `url("${logo}")`);
78
78
  else
79
79
  root.style.removeProperty("--hgdc-org-logo");
80
+ // Teinte d'OrgMark (`--avatar-<n>`), un entier de 1 à 7 seulement.
81
+ const tone = org && Number.isInteger(org.tone) && Number(org.tone) >= 1 && Number(org.tone) <= config.tones
82
+ ? Number(org.tone)
83
+ : 0;
84
+ if (tone) {
85
+ const token = `--avatar-${tone}`;
86
+ root.style.setProperty("--hgdc-org-bg", `var(${token})`);
87
+ root.style.setProperty("--hgdc-org-ink", `var(${token}-ink)`);
88
+ }
89
+ else {
90
+ root.style.removeProperty("--hgdc-org-bg");
91
+ root.style.removeProperty("--hgdc-org-ink");
92
+ }
80
93
  }
81
94
  else {
82
95
  delete root.dataset.hgdcOrg;
83
96
  root.style.removeProperty("--hgdc-org-initials");
84
97
  root.style.removeProperty("--hgdc-org-logo");
98
+ root.style.removeProperty("--hgdc-org-bg");
99
+ root.style.removeProperty("--hgdc-org-ink");
85
100
  }
86
101
  let height = 0;
87
102
  try {
@@ -101,6 +116,7 @@ export const SHELL_BOOT_CONFIG = {
101
116
  products: FAMILY_PRODUCTS,
102
117
  origins: Object.entries(PRODUCT_ORIGINS).map(([word, { origin, basePath }]) => [word, origin, basePath]),
103
118
  domain: "hgdc.ch",
119
+ tones: AVATAR_TONES,
104
120
  };
105
121
  /** Le script en ligne du `<head>` : `shellBoot` et sa configuration, sans dépendance. */
106
122
  export function shellBootScript(config = SHELL_BOOT_CONFIG) {
@@ -122,7 +138,7 @@ function storageSet(storage, key, value) {
122
138
  }
123
139
  /** Ce que la coque retient d'une organisation : ses initiales et son logo (`https:` seulement). */
124
140
  export function shellOrganization(org) {
125
- return { initials: initials(org.name), ...(org.logo ? { logo: org.logo } : {}) };
141
+ return { initials: initials(org.name), ...(org.logo ? { logo: org.logo } : { tone: orgTone(org) }) };
126
142
  }
127
143
  /** Retient l'organisation affichée (`OrgSwitcher` le fait) et l'applique à la page. */
128
144
  export function rememberShellOrganization(org) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hgdc/ui",
3
- "version": "0.54.0",
3
+ "version": "0.55.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
@@ -896,8 +896,8 @@
896
896
  background: var(--shell-hover);
897
897
  }
898
898
  :root[data-hgdc-org] [data-slot="org-placeholder-mark"] {
899
- background: var(--hgdc-org-logo, var(--foreground)) center / cover no-repeat;
900
- color: var(--background);
899
+ background: var(--hgdc-org-logo, var(--hgdc-org-bg, var(--foreground))) center / cover no-repeat;
900
+ color: var(--hgdc-org-ink, var(--background));
901
901
  }
902
902
  :root[data-hgdc-org] [data-slot="org-placeholder-mark"]::before {
903
903
  content: var(--hgdc-org-initials, "");