@hgdc/ui 0.67.0 → 0.69.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/image-file.d.ts +35 -0
- package/dist/image-file.js +110 -0
- package/dist/org-switcher.js +5 -3
- package/dist/shell-memory.d.ts +6 -0
- package/dist/shell-memory.js +20 -0
- package/package.json +1 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fichier image envoyé ou téléchargé, lu dans ses octets et sans I/O : le type annoncé ne prouve rien.
|
|
3
|
+
* Un SVG n'y figure jamais : ouvert seul, il exécuterait ses scripts sur l'origine qui le sert.
|
|
4
|
+
*/
|
|
5
|
+
/** Type réel d'une image, d'après ses premiers octets ; `null` si ce n'est ni PNG, ni JPEG, ni WebP. */
|
|
6
|
+
export declare function imageKind(head: Uint8Array): "png" | "jpeg" | "webp" | null;
|
|
7
|
+
export type ImageKind = NonNullable<ReturnType<typeof imageKind>>;
|
|
8
|
+
export declare const IMAGE_MIME: Record<ImageKind, string>;
|
|
9
|
+
/** Dimensions d'un PNG, lues dans son en-tête `IHDR` ; `null` si l'en-tête manque. */
|
|
10
|
+
export declare function pngSize(bytes: Uint8Array): {
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
} | null;
|
|
14
|
+
/** Dimensions d'un JPEG, lues dans son premier segment SOF ; `null` si aucun n'est trouvé. */
|
|
15
|
+
export declare function jpegSize(bytes: Uint8Array): {
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
} | null;
|
|
19
|
+
/** Dimensions d'un WebP (VP8, VP8L ou VP8X) ; `null` si l'en-tête manque. */
|
|
20
|
+
export declare function webpSize(bytes: Uint8Array): {
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
} | null;
|
|
24
|
+
/** Côté le plus long accepté pour une image téléversée : au-delà, rien ne sert et le décodage coûte. */
|
|
25
|
+
export declare const MAX_IMAGE_SIDE = 4096;
|
|
26
|
+
export declare function imageSize(kind: ImageKind, bytes: Uint8Array): {
|
|
27
|
+
width: number;
|
|
28
|
+
height: number;
|
|
29
|
+
} | null;
|
|
30
|
+
/**
|
|
31
|
+
* JPEG sans ses métadonnées : EXIF et XMP (APP1, dont la position GPS), IPTC (APP13), commentaires
|
|
32
|
+
* et autres segments d'application sont retirés, l'image elle-même n'est pas touchée (pas de
|
|
33
|
+
* réencodage). Un fichier mal formé est rendu tel quel : `imageProblem` l'a déjà contrôlé.
|
|
34
|
+
*/
|
|
35
|
+
export declare function stripJpegMetadata(bytes: Uint8Array): Uint8Array;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fichier image envoyé ou téléchargé, lu dans ses octets et sans I/O : le type annoncé ne prouve rien.
|
|
3
|
+
* Un SVG n'y figure jamais : ouvert seul, il exécuterait ses scripts sur l'origine qui le sert.
|
|
4
|
+
*/
|
|
5
|
+
/** Type réel d'une image, d'après ses premiers octets ; `null` si ce n'est ni PNG, ni JPEG, ni WebP. */
|
|
6
|
+
export function imageKind(head) {
|
|
7
|
+
const at = (i) => head[i] ?? -1;
|
|
8
|
+
if ([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a].every((b, i) => at(i) === b))
|
|
9
|
+
return "png";
|
|
10
|
+
if (at(0) === 0xff && at(1) === 0xd8 && at(2) === 0xff)
|
|
11
|
+
return "jpeg";
|
|
12
|
+
const ascii = (from, to) => String.fromCharCode(...head.slice(from, to));
|
|
13
|
+
if (ascii(0, 4) === "RIFF" && ascii(8, 12) === "WEBP")
|
|
14
|
+
return "webp";
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
export const IMAGE_MIME = { png: "image/png", jpeg: "image/jpeg", webp: "image/webp" };
|
|
18
|
+
/** Dimensions d'un PNG, lues dans son en-tête `IHDR` ; `null` si l'en-tête manque. */
|
|
19
|
+
export function pngSize(bytes) {
|
|
20
|
+
if (bytes.length < 24 || String.fromCharCode(...bytes.slice(12, 16)) !== "IHDR")
|
|
21
|
+
return null;
|
|
22
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
23
|
+
return { width: view.getUint32(16), height: view.getUint32(20) };
|
|
24
|
+
}
|
|
25
|
+
/** Dimensions d'un JPEG, lues dans son premier segment SOF ; `null` si aucun n'est trouvé. */
|
|
26
|
+
export function jpegSize(bytes) {
|
|
27
|
+
let i = 2;
|
|
28
|
+
while (i + 9 < bytes.length) {
|
|
29
|
+
if (bytes[i] !== 0xff)
|
|
30
|
+
return null;
|
|
31
|
+
const marker = bytes[i + 1] ?? 0;
|
|
32
|
+
if (marker === 0xff) {
|
|
33
|
+
i++;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
// Marqueurs sans longueur : début d'image, redémarrages.
|
|
37
|
+
if (marker === 0xd8 || marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) {
|
|
38
|
+
i += 2;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const sof = marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc;
|
|
42
|
+
if (sof)
|
|
43
|
+
return {
|
|
44
|
+
height: ((bytes[i + 5] ?? 0) << 8) | (bytes[i + 6] ?? 0),
|
|
45
|
+
width: ((bytes[i + 7] ?? 0) << 8) | (bytes[i + 8] ?? 0),
|
|
46
|
+
};
|
|
47
|
+
i += 2 + (((bytes[i + 2] ?? 0) << 8) | (bytes[i + 3] ?? 0));
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
/** Dimensions d'un WebP (VP8, VP8L ou VP8X) ; `null` si l'en-tête manque. */
|
|
52
|
+
export function webpSize(bytes) {
|
|
53
|
+
if (bytes.length < 30)
|
|
54
|
+
return null;
|
|
55
|
+
const b = (i) => bytes[i] ?? 0;
|
|
56
|
+
const chunk = String.fromCharCode(...bytes.slice(12, 16));
|
|
57
|
+
if (chunk === "VP8 ")
|
|
58
|
+
return { width: (b(26) | (b(27) << 8)) & 0x3fff, height: (b(28) | (b(29) << 8)) & 0x3fff };
|
|
59
|
+
if (chunk === "VP8L")
|
|
60
|
+
return {
|
|
61
|
+
width: 1 + (((b(22) & 0x3f) << 8) | b(21)),
|
|
62
|
+
height: 1 + (((b(24) & 0x0f) << 10) | (b(23) << 2) | ((b(22) & 0xc0) >> 6)),
|
|
63
|
+
};
|
|
64
|
+
if (chunk === "VP8X")
|
|
65
|
+
return { width: 1 + (b(24) | (b(25) << 8) | (b(26) << 16)), height: 1 + (b(27) | (b(28) << 8) | (b(29) << 16)) };
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
/** Côté le plus long accepté pour une image téléversée : au-delà, rien ne sert et le décodage coûte. */
|
|
69
|
+
export const MAX_IMAGE_SIDE = 4096;
|
|
70
|
+
export function imageSize(kind, bytes) {
|
|
71
|
+
return kind === "png" ? pngSize(bytes) : kind === "jpeg" ? jpegSize(bytes) : webpSize(bytes);
|
|
72
|
+
}
|
|
73
|
+
/** Segments JPEG gardés : JFIF (APP0), profil de couleur (APP2), Adobe (APP14, conversion des couleurs). */
|
|
74
|
+
const KEPT_APP = new Set([0xe0, 0xe2, 0xee]);
|
|
75
|
+
/**
|
|
76
|
+
* JPEG sans ses métadonnées : EXIF et XMP (APP1, dont la position GPS), IPTC (APP13), commentaires
|
|
77
|
+
* et autres segments d'application sont retirés, l'image elle-même n'est pas touchée (pas de
|
|
78
|
+
* réencodage). Un fichier mal formé est rendu tel quel : `imageProblem` l'a déjà contrôlé.
|
|
79
|
+
*/
|
|
80
|
+
export function stripJpegMetadata(bytes) {
|
|
81
|
+
if (bytes[0] !== 0xff || bytes[1] !== 0xd8)
|
|
82
|
+
return bytes;
|
|
83
|
+
const parts = [bytes.slice(0, 2)];
|
|
84
|
+
let i = 2;
|
|
85
|
+
while (i + 4 <= bytes.length) {
|
|
86
|
+
if (bytes[i] !== 0xff)
|
|
87
|
+
return bytes;
|
|
88
|
+
const marker = bytes[i + 1] ?? 0;
|
|
89
|
+
// Début des données compressées : tout le reste est l'image.
|
|
90
|
+
if (marker === 0xda) {
|
|
91
|
+
parts.push(bytes.slice(i));
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
const length = ((bytes[i + 2] ?? 0) << 8) | (bytes[i + 3] ?? 0);
|
|
95
|
+
const end = i + 2 + length;
|
|
96
|
+
if (length < 2 || end > bytes.length)
|
|
97
|
+
return bytes;
|
|
98
|
+
const metadata = (marker >= 0xe1 && marker <= 0xef && !KEPT_APP.has(marker)) || marker === 0xfe;
|
|
99
|
+
if (!metadata)
|
|
100
|
+
parts.push(bytes.slice(i, end));
|
|
101
|
+
i = end;
|
|
102
|
+
}
|
|
103
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
104
|
+
let at = 0;
|
|
105
|
+
for (const part of parts) {
|
|
106
|
+
out.set(part, at);
|
|
107
|
+
at += part.length;
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
package/dist/org-switcher.js
CHANGED
|
@@ -4,7 +4,7 @@ import Tick02Icon from "@hugeicons/core-free-icons/Tick02Icon";
|
|
|
4
4
|
import UnfoldMoreIcon from "@hugeicons/core-free-icons/UnfoldMoreIcon";
|
|
5
5
|
import { HugeiconsIcon } from "@hugeicons/react";
|
|
6
6
|
import { Menu as MenuPrimitive } from "@base-ui/react/menu";
|
|
7
|
-
import { useEffect } from "react";
|
|
7
|
+
import { useEffect, useState } from "react";
|
|
8
8
|
import { Badge } from "./badge.js";
|
|
9
9
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "./dropdown-menu.js";
|
|
10
10
|
import { AVATAR_TONE_CLASSES, initials, orgTone } from "./initials.js";
|
|
@@ -30,8 +30,10 @@ export const MENU_LINK_ITEM = "relative flex cursor-default items-center gap-2.5
|
|
|
30
30
|
* teinte du nom.
|
|
31
31
|
*/
|
|
32
32
|
export function OrgMark({ org, className, }) {
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
// Image retirée ou serveur muet : les initiales, plutôt qu'une image cassée.
|
|
34
|
+
const [broken, setBroken] = useState(null);
|
|
35
|
+
if (org.logo && org.logo !== broken)
|
|
36
|
+
return (_jsx("img", { src: org.logo, alt: "", onError: () => setBroken(org.logo ?? null), className: cn("size-8 shrink-0 rounded-lg bg-muted object-contain", className) }));
|
|
35
37
|
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) }));
|
|
36
38
|
}
|
|
37
39
|
/**
|
package/dist/shell-memory.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export declare const SHELL_ORG_KEY = "hgdc-shell:org";
|
|
|
4
4
|
export declare const SHELL_NAV_KEY_PREFIX = "hgdc-shell:nav:";
|
|
5
5
|
/** Dernier produit ouvert (son mot), en `localStorage` : l'accueil d'`app.hgdc.ch` y renvoie. */
|
|
6
6
|
export declare const SHELL_LAST_PRODUCT_KEY = "hgdc-shell:last";
|
|
7
|
+
/** Slug de la dernière organisation ouverte, le même pour tous les modules : Basalte est un seul produit (ADR 0019 de la famille). */
|
|
8
|
+
export declare const SHELL_LAST_ORG_KEY = "hgdc-shell:last-org";
|
|
7
9
|
export type ShellOrganization = {
|
|
8
10
|
initials: string;
|
|
9
11
|
logo?: string;
|
|
@@ -59,6 +61,10 @@ export declare function rememberShellOrganization(org: {
|
|
|
59
61
|
}): void;
|
|
60
62
|
/** Oublie l'organisation (déconnexion) : la page suivante montre un squelette. */
|
|
61
63
|
export declare function forgetShellOrganization(): void;
|
|
64
|
+
/** Retient l'organisation ouverte : à appeler sur la page d'une organisation, jamais pour la démo. */
|
|
65
|
+
export declare function rememberLastOrganization(slug: string): void;
|
|
66
|
+
/** Slug de la dernière organisation ouverte dans un module. À comparer aux organisations de la personne. */
|
|
67
|
+
export declare function lastOrganization(): string | null;
|
|
62
68
|
/** Retient la hauteur de la navigation d'un produit (`AppShell` le fait après l'affichage). */
|
|
63
69
|
export declare function rememberShellNavHeight(product: string, height: number): void;
|
|
64
70
|
/** Retient le produit affiché (`AppShell` le fait), pour que l'accueil d'`app.hgdc.ch` y renvoie. */
|
package/dist/shell-memory.js
CHANGED
|
@@ -18,6 +18,8 @@ export const SHELL_ORG_KEY = "hgdc-shell:org";
|
|
|
18
18
|
export const SHELL_NAV_KEY_PREFIX = "hgdc-shell:nav:";
|
|
19
19
|
/** Dernier produit ouvert (son mot), en `localStorage` : l'accueil d'`app.hgdc.ch` y renvoie. */
|
|
20
20
|
export const SHELL_LAST_PRODUCT_KEY = "hgdc-shell:last";
|
|
21
|
+
/** Slug de la dernière organisation ouverte, le même pour tous les modules : Basalte est un seul produit (ADR 0019 de la famille). */
|
|
22
|
+
export const SHELL_LAST_ORG_KEY = "hgdc-shell:last-org";
|
|
21
23
|
/**
|
|
22
24
|
* Applique la mémoire de la coque sur `<html>` : `data-sidebar="rail"`, `data-hgdc-product`
|
|
23
25
|
* (le produit de l'adresse), `data-hgdc-org` et les variables `--hgdc-org-initials`,
|
|
@@ -150,8 +152,26 @@ export function forgetShellOrganization() {
|
|
|
150
152
|
if (!win)
|
|
151
153
|
return;
|
|
152
154
|
storageSet(SHELL_ORG_KEY, null);
|
|
155
|
+
storageSet(SHELL_LAST_ORG_KEY, null);
|
|
153
156
|
shellBoot(SHELL_BOOT_CONFIG, win);
|
|
154
157
|
}
|
|
158
|
+
/** Retient l'organisation ouverte : à appeler sur la page d'une organisation, jamais pour la démo. */
|
|
159
|
+
export function rememberLastOrganization(slug) {
|
|
160
|
+
if (browser())
|
|
161
|
+
storageSet(SHELL_LAST_ORG_KEY, slug);
|
|
162
|
+
}
|
|
163
|
+
/** Slug de la dernière organisation ouverte dans un module. À comparer aux organisations de la personne. */
|
|
164
|
+
export function lastOrganization() {
|
|
165
|
+
const win = browser();
|
|
166
|
+
if (!win)
|
|
167
|
+
return null;
|
|
168
|
+
try {
|
|
169
|
+
return win.localStorage.getItem(SHELL_LAST_ORG_KEY);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
155
175
|
/** Retient la hauteur de la navigation d'un produit (`AppShell` le fait après l'affichage). */
|
|
156
176
|
export function rememberShellNavHeight(product, height) {
|
|
157
177
|
if (!browser() || !(height > 0))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hgdc/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.69.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",
|