@agence-zef/astro 0.4.1 → 0.5.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/components/Accordion.astro +61 -0
- package/components/AccordionItem.astro +23 -0
- package/components/Checkbox.astro +20 -0
- package/components/Dialog.astro +72 -0
- package/components/Field.astro +27 -0
- package/components/Image.astro +21 -5
- package/components/Input.astro +49 -0
- package/components/Popover.astro +21 -0
- package/components/Select.astro +55 -0
- package/dist/index.js +6 -0
- package/dist/seo.d.ts +2 -0
- package/dist/seo.js +3 -0
- package/dist/ui.d.ts +7 -0
- package/dist/ui.js +33 -0
- package/package.json +19 -5
- package/styles.css +468 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Accordéon natif (`<details>` / `<summary>`), sans JavaScript ni style imposé : classes
|
|
4
|
+
* `zef-accordion`, `zef-accordion__item`, `__summary`, `__content`, habillées par
|
|
5
|
+
* `@agence-zef/astro/styles.css` (ouverture animée) ou par le CSS du site.
|
|
6
|
+
*
|
|
7
|
+
* Deux usages : `items` (une liste du CMS : `{ summary, html }` ou `{ summary, text }`), ou
|
|
8
|
+
* des `<AccordionItem>` en contenu. `exclusive` : un seul élément ouvert à la fois (attribut
|
|
9
|
+
* `name` natif). `faq` : ajoute les données structurées FAQPage pour Google (avec `items`).
|
|
10
|
+
*/
|
|
11
|
+
import RichText from "./RichText.astro";
|
|
12
|
+
|
|
13
|
+
type Item = { summary: string; html?: string | null; text?: string | null; open?: boolean };
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
items?: Item[];
|
|
17
|
+
exclusive?: boolean;
|
|
18
|
+
faq?: boolean;
|
|
19
|
+
/** Nom du groupe exclusif, à reprendre dans les `<AccordionItem name>` en contenu. */
|
|
20
|
+
name?: string;
|
|
21
|
+
class?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const { items, exclusive = false, faq = false, class: className } = Astro.props;
|
|
25
|
+
const name =
|
|
26
|
+
Astro.props.name ?? (exclusive ? `zef-acc-${Math.random().toString(36).slice(2, 8)}` : undefined);
|
|
27
|
+
const strip = (html: string) =>
|
|
28
|
+
html
|
|
29
|
+
.replace(/<[^>]*>/g, " ")
|
|
30
|
+
.replace(/\s+/g, " ")
|
|
31
|
+
.trim();
|
|
32
|
+
const faqData =
|
|
33
|
+
faq && items?.length
|
|
34
|
+
? {
|
|
35
|
+
"@context": "https://schema.org",
|
|
36
|
+
"@type": "FAQPage",
|
|
37
|
+
mainEntity: items
|
|
38
|
+
.filter((i) => i.summary)
|
|
39
|
+
.map((i) => ({
|
|
40
|
+
"@type": "Question",
|
|
41
|
+
name: i.summary,
|
|
42
|
+
acceptedAnswer: { "@type": "Answer", text: i.text ?? strip(i.html ?? "") },
|
|
43
|
+
})),
|
|
44
|
+
}
|
|
45
|
+
: null;
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
<div class:list={["zef-accordion", className]}>
|
|
49
|
+
{
|
|
50
|
+
items?.map((item) => (
|
|
51
|
+
<details class="zef-accordion__item" name={name} open={item.open}>
|
|
52
|
+
<summary class="zef-accordion__summary">{item.summary}</summary>
|
|
53
|
+
<div class="zef-accordion__content">
|
|
54
|
+
{item.html ? <RichText html={item.html} /> : item.text ? <p>{item.text}</p> : null}
|
|
55
|
+
</div>
|
|
56
|
+
</details>
|
|
57
|
+
))
|
|
58
|
+
}
|
|
59
|
+
<slot />
|
|
60
|
+
</div>
|
|
61
|
+
{faqData && <script type="application/ld+json" is:inline set:html={JSON.stringify(faqData).replace(/</g, "\\u003c")} />}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Élément d'accordéon (`<details>`), à mettre dans `<Accordion>` ou seul. Titre : `summary`, ou
|
|
4
|
+
* le contenu du slot `summary` (icône, mise en forme). `name` : groupe exclusif (un seul ouvert).
|
|
5
|
+
*/
|
|
6
|
+
interface Props {
|
|
7
|
+
summary?: string;
|
|
8
|
+
name?: string;
|
|
9
|
+
open?: boolean;
|
|
10
|
+
class?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { summary, name, open = false, class: className } = Astro.props;
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<details class:list={["zef-accordion__item", className]} name={name} open={open}>
|
|
17
|
+
<summary class="zef-accordion__summary">
|
|
18
|
+
{Astro.slots.has("summary") ? <slot name="summary" /> : summary}
|
|
19
|
+
</summary>
|
|
20
|
+
<div class="zef-accordion__content">
|
|
21
|
+
<slot />
|
|
22
|
+
</div>
|
|
23
|
+
</details>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Case à cocher (ou bouton radio avec `type="radio"`) avec son libellé, classe `zef-check`.
|
|
4
|
+
* Le libellé : `label`, ou le contenu (lien vers les conditions, mise en forme).
|
|
5
|
+
*/
|
|
6
|
+
import type { HTMLAttributes } from "astro/types";
|
|
7
|
+
|
|
8
|
+
type Props = Omit<HTMLAttributes<"input">, "class" | "type"> & {
|
|
9
|
+
label?: string;
|
|
10
|
+
type?: "checkbox" | "radio";
|
|
11
|
+
class?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const { label, type = "checkbox", class: className, ...rest } = Astro.props;
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
<label class:list={["zef-check", className]}>
|
|
18
|
+
<input {...rest} type={type} />
|
|
19
|
+
<span>{Astro.slots.has("default") ? <slot /> : label}</span>
|
|
20
|
+
</label>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Fenêtre modale native (`<dialog>`), sans style imposé : classes `zef-dialog`,
|
|
4
|
+
* `zef-dialog__panel`, `zef-dialog__close`, habillées par `@agence-zef/astro/styles.css`
|
|
5
|
+
* (variables `--zef-*`) ou par le CSS du site.
|
|
6
|
+
*
|
|
7
|
+
* Ouverture : n'importe quel élément avec `data-zef-open="<id>"` (bouton, lien). Fermeture :
|
|
8
|
+
* Échap, clic sur le voile, ou un élément `data-zef-close` dans la fenêtre. La page en dessous
|
|
9
|
+
* ne défile plus pendant l'ouverture ; animations d'ouverture et de fermeture via
|
|
10
|
+
* `[open]` / `[data-closing]`, coupées si le visiteur réduit les animations.
|
|
11
|
+
*/
|
|
12
|
+
interface Props {
|
|
13
|
+
id: string;
|
|
14
|
+
/** Titre lu par les lecteurs d'écran si la fenêtre n'a pas de titre visible. */
|
|
15
|
+
label?: string;
|
|
16
|
+
/** Bouton de fermeture en haut à droite (défaut : oui) et son libellé accessible. */
|
|
17
|
+
closeButton?: boolean;
|
|
18
|
+
closeLabel?: string;
|
|
19
|
+
class?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { id, label, closeButton = true, closeLabel = "Fermer", class: className } = Astro.props;
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
<dialog id={id} class:list={["zef-dialog", className]} aria-label={label} data-zef-dialog>
|
|
26
|
+
<div class="zef-dialog__panel">
|
|
27
|
+
{
|
|
28
|
+
closeButton && (
|
|
29
|
+
<button type="button" class="zef-dialog__close" data-zef-close aria-label={closeLabel}>
|
|
30
|
+
<svg width="20" height="20" viewBox="0 0 24 24" aria-hidden="true">
|
|
31
|
+
<path d="M18 6 6 18M6 6l12 12" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" />
|
|
32
|
+
</svg>
|
|
33
|
+
</button>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
<slot />
|
|
37
|
+
</div>
|
|
38
|
+
</dialog>
|
|
39
|
+
|
|
40
|
+
<style is:global>
|
|
41
|
+
/* Comportement, pas habillage : la page ne défile plus derrière une fenêtre ouverte. */
|
|
42
|
+
html:has(dialog[data-zef-dialog][open]) {
|
|
43
|
+
overflow: hidden;
|
|
44
|
+
}
|
|
45
|
+
</style>
|
|
46
|
+
|
|
47
|
+
<script>
|
|
48
|
+
import { closeDialog } from "@agence-zef/astro/ui";
|
|
49
|
+
|
|
50
|
+
document.addEventListener("click", (event) => {
|
|
51
|
+
const target = event.target as Element;
|
|
52
|
+
const opener = target.closest<HTMLElement>("[data-zef-open]");
|
|
53
|
+
if (opener) {
|
|
54
|
+
const dialog = document.getElementById(opener.dataset.zefOpen ?? "");
|
|
55
|
+
if (dialog instanceof HTMLDialogElement) {
|
|
56
|
+
event.preventDefault();
|
|
57
|
+
dialog.showModal();
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const closer = target.closest("[data-zef-close]");
|
|
62
|
+
const dialog = (closer ?? target).closest("dialog[data-zef-dialog]");
|
|
63
|
+
// Un clic sur le voile atteint le <dialog> lui-même, pas son panneau.
|
|
64
|
+
if (dialog instanceof HTMLDialogElement && (closer || target === dialog)) closeDialog(dialog);
|
|
65
|
+
});
|
|
66
|
+
for (const dialog of document.querySelectorAll<HTMLDialogElement>("dialog[data-zef-dialog]")) {
|
|
67
|
+
dialog.addEventListener("cancel", (event) => {
|
|
68
|
+
event.preventDefault();
|
|
69
|
+
closeDialog(dialog);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
</script>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Champ de formulaire codé dans le site : libellé, contrôle (slot), aide et erreur, avec les
|
|
4
|
+
* classes partagées par tous les formulaires de `@agence-zef/astro` (`zef-field`, `zef-label`,
|
|
5
|
+
* `zef-help`, `zef-error`). `for` : identifiant du contrôle.
|
|
6
|
+
*/
|
|
7
|
+
interface Props {
|
|
8
|
+
label: string;
|
|
9
|
+
for?: string;
|
|
10
|
+
help?: string;
|
|
11
|
+
error?: string;
|
|
12
|
+
required?: boolean;
|
|
13
|
+
class?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { label, for: htmlFor, help, error, required = false, class: className } = Astro.props;
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
<div class:list={["zef-field", className]} data-invalid={error ? "" : undefined}>
|
|
20
|
+
<label class="zef-label" for={htmlFor}>
|
|
21
|
+
{label}
|
|
22
|
+
{required && <span class="zef-required" aria-hidden="true"> *</span>}
|
|
23
|
+
</label>
|
|
24
|
+
<slot />
|
|
25
|
+
{help && <p class="zef-help">{help}</p>}
|
|
26
|
+
<p class="zef-error" role="alert" hidden={!error}>{error}</p>
|
|
27
|
+
</div>
|
package/components/Image.astro
CHANGED
|
@@ -10,18 +10,30 @@ import type { Media } from "@agence-zef/schema";
|
|
|
10
10
|
interface Props {
|
|
11
11
|
media: Media | null;
|
|
12
12
|
class?: string;
|
|
13
|
-
/** `eager` pour l'
|
|
13
|
+
/** `eager` pour une image visible dès l'ouverture de la page. */
|
|
14
14
|
loading?: "lazy" | "eager";
|
|
15
|
+
/**
|
|
16
|
+
* Image principale de la page (LCP, en général celle du hero) : chargée tout de suite et en
|
|
17
|
+
* priorité (`fetchpriority="high"`). Une seule par page.
|
|
18
|
+
*/
|
|
19
|
+
priority?: boolean;
|
|
15
20
|
/** Largeur affichée (`(min-width: 48rem) 33vw, 100vw`) : active le `srcset`. */
|
|
16
21
|
sizes?: string;
|
|
17
22
|
}
|
|
18
23
|
|
|
19
|
-
const { media, class: className,
|
|
24
|
+
const { media, class: className, priority = false, sizes } = Astro.props;
|
|
25
|
+
const loading = priority ? "eager" : (Astro.props.loading ?? "lazy");
|
|
26
|
+
const fetchpriority = priority ? "high" : undefined;
|
|
27
|
+
// Proportions réservées même si le CSS du site force `width: auto` (pas de décalage au chargement).
|
|
28
|
+
const style =
|
|
29
|
+
media?.width && media.height ? `aspect-ratio: ${media.width} / ${media.height}` : undefined;
|
|
20
30
|
const OPTIMIZED = ["image/jpeg", "image/png", "image/webp", "image/avif"];
|
|
21
31
|
const optimize = Boolean(
|
|
22
32
|
media?.width && media.height && media.mime && OPTIMIZED.includes(media.mime),
|
|
23
33
|
);
|
|
24
|
-
|
|
34
|
+
// Largeurs générées pour le `srcset` : assez serrées pour qu'un mobile ne télécharge pas une
|
|
35
|
+
// image deux fois trop grande.
|
|
36
|
+
const STEPS = [320, 480, 640, 800, 960, 1200, 1600, 2000, 2400];
|
|
25
37
|
const widths =
|
|
26
38
|
sizes && media?.width
|
|
27
39
|
? [...STEPS.filter((w) => w < (media.width as number)), media.width]
|
|
@@ -37,10 +49,12 @@ const widths =
|
|
|
37
49
|
width={media.width as number}
|
|
38
50
|
height={media.height as number}
|
|
39
51
|
loading={loading}
|
|
40
|
-
|
|
52
|
+
fetchpriority={fetchpriority}
|
|
53
|
+
decoding={priority ? "sync" : "async"}
|
|
41
54
|
sizes={sizes}
|
|
42
55
|
widths={widths}
|
|
43
56
|
class={className}
|
|
57
|
+
style={style}
|
|
44
58
|
/>
|
|
45
59
|
) : (
|
|
46
60
|
<img
|
|
@@ -49,9 +63,11 @@ const widths =
|
|
|
49
63
|
width={media.width}
|
|
50
64
|
height={media.height}
|
|
51
65
|
loading={loading}
|
|
52
|
-
|
|
66
|
+
fetchpriority={fetchpriority}
|
|
67
|
+
decoding={priority ? "sync" : "async"}
|
|
53
68
|
sizes={sizes}
|
|
54
69
|
class={className}
|
|
70
|
+
style={style}
|
|
55
71
|
/>
|
|
56
72
|
))
|
|
57
73
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Zone de saisie (`<input>` ou `<textarea>` avec `multiline`), classe `zef-input`. Avec `label`,
|
|
4
|
+
* elle est rendue dans un `<Field>` (libellé, aide, erreur). Les autres attributs sont passés
|
|
5
|
+
* tels quels (`name`, `type`, `required`, `placeholder`, `autocomplete`…).
|
|
6
|
+
*/
|
|
7
|
+
import type { HTMLAttributes } from "astro/types";
|
|
8
|
+
import Field from "./Field.astro";
|
|
9
|
+
|
|
10
|
+
type Props = Omit<HTMLAttributes<"input">, "class"> & {
|
|
11
|
+
label?: string;
|
|
12
|
+
help?: string;
|
|
13
|
+
error?: string;
|
|
14
|
+
multiline?: boolean;
|
|
15
|
+
rows?: number;
|
|
16
|
+
class?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const {
|
|
20
|
+
label,
|
|
21
|
+
help,
|
|
22
|
+
error,
|
|
23
|
+
multiline = false,
|
|
24
|
+
rows = 5,
|
|
25
|
+
class: className,
|
|
26
|
+
id: givenId,
|
|
27
|
+
...rest
|
|
28
|
+
} = Astro.props;
|
|
29
|
+
const id =
|
|
30
|
+
givenId ?? (label ? `zef-${rest.name ?? Math.random().toString(36).slice(2, 8)}` : undefined);
|
|
31
|
+
const attrs = {
|
|
32
|
+
...rest,
|
|
33
|
+
id,
|
|
34
|
+
class: `zef-input${className ? ` ${className}` : ""}`,
|
|
35
|
+
"aria-invalid": error ? "true" : undefined,
|
|
36
|
+
};
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
{
|
|
40
|
+
label ? (
|
|
41
|
+
<Field label={label} for={id} help={help} error={error} required={Boolean(rest.required)}>
|
|
42
|
+
{multiline ? <textarea {...(attrs as HTMLAttributes<"textarea">)} rows={rows}>{rest.value ?? ""}</textarea> : <input {...attrs} />}
|
|
43
|
+
</Field>
|
|
44
|
+
) : multiline ? (
|
|
45
|
+
<textarea {...(attrs as HTMLAttributes<"textarea">)} rows={rows}>{rest.value ?? ""}</textarea>
|
|
46
|
+
) : (
|
|
47
|
+
<input {...attrs} />
|
|
48
|
+
)
|
|
49
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Popover natif (attribut `popover`), sans JavaScript ni style imposé : ouvert par un bouton
|
|
4
|
+
* `<button popovertarget="<id>">`, fermé au clic en dehors et par Échap. Placé sous son bouton
|
|
5
|
+
* par le positionnement d'ancre CSS (`styles.css`), au centre de l'écran en repli.
|
|
6
|
+
* Classe `zef-popover` ; `manual` : ne se ferme pas au clic en dehors.
|
|
7
|
+
*/
|
|
8
|
+
interface Props {
|
|
9
|
+
id: string;
|
|
10
|
+
manual?: boolean;
|
|
11
|
+
/** Position par rapport au bouton : en dessous (défaut), au-dessus, à droite, à gauche. */
|
|
12
|
+
placement?: "bottom" | "top" | "right" | "left";
|
|
13
|
+
class?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { id, manual = false, placement = "bottom", class: className } = Astro.props;
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
<div id={id} popover={manual ? "manual" : "auto"} class:list={["zef-popover", className]} data-placement={placement}>
|
|
20
|
+
<slot />
|
|
21
|
+
</div>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Liste déroulante native, classe `zef-select`. Options : `options` (`{ value: label }` ou
|
|
4
|
+
* `[{ value, label }]`) ou `<option>` en contenu. Avec `label`, rendue dans un `<Field>`.
|
|
5
|
+
*/
|
|
6
|
+
import type { HTMLAttributes } from "astro/types";
|
|
7
|
+
import Field from "./Field.astro";
|
|
8
|
+
|
|
9
|
+
type Option = { value: string; label: string };
|
|
10
|
+
type Props = Omit<HTMLAttributes<"select">, "class"> & {
|
|
11
|
+
label?: string;
|
|
12
|
+
help?: string;
|
|
13
|
+
error?: string;
|
|
14
|
+
options?: Record<string, string> | Option[];
|
|
15
|
+
/** Première option vide (« Choisir… »). */
|
|
16
|
+
placeholder?: string;
|
|
17
|
+
value?: string;
|
|
18
|
+
class?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const {
|
|
22
|
+
label,
|
|
23
|
+
help,
|
|
24
|
+
error,
|
|
25
|
+
options,
|
|
26
|
+
placeholder,
|
|
27
|
+
value,
|
|
28
|
+
class: className,
|
|
29
|
+
id: givenId,
|
|
30
|
+
...rest
|
|
31
|
+
} = Astro.props;
|
|
32
|
+
const id =
|
|
33
|
+
givenId ?? (label ? `zef-${rest.name ?? Math.random().toString(36).slice(2, 8)}` : undefined);
|
|
34
|
+
const list: Option[] = Array.isArray(options)
|
|
35
|
+
? options
|
|
36
|
+
: Object.entries(options ?? {}).map(([v, l]) => ({ value: v, label: l }));
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
{
|
|
40
|
+
label ? (
|
|
41
|
+
<Field label={label} for={id} help={help} error={error} required={Boolean(rest.required)}>
|
|
42
|
+
<select {...rest} id={id} class:list={["zef-select", className]} aria-invalid={error ? "true" : undefined}>
|
|
43
|
+
{placeholder !== undefined && <option value="">{placeholder}</option>}
|
|
44
|
+
{list.map((o) => <option value={o.value} selected={o.value === value}>{o.label}</option>)}
|
|
45
|
+
<slot />
|
|
46
|
+
</select>
|
|
47
|
+
</Field>
|
|
48
|
+
) : (
|
|
49
|
+
<select {...rest} id={id} class:list={["zef-select", className]} aria-invalid={error ? "true" : undefined}>
|
|
50
|
+
{placeholder !== undefined && <option value="">{placeholder}</option>}
|
|
51
|
+
{list.map((o) => <option value={o.value} selected={o.value === value}>{o.label}</option>)}
|
|
52
|
+
<slot />
|
|
53
|
+
</select>
|
|
54
|
+
)
|
|
55
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -84,6 +84,12 @@ export default function zef(options = {}) {
|
|
|
84
84
|
]),
|
|
85
85
|
},
|
|
86
86
|
});
|
|
87
|
+
// Performance : le CSS (quelques Ko par page) est écrit dans la page plutôt que chargé à
|
|
88
|
+
// part, ce qui retire une requête bloquante du premier affichage. Un site peut choisir
|
|
89
|
+
// « never » dans son `astro.config`.
|
|
90
|
+
if (config.build.inlineStylesheets === "auto") {
|
|
91
|
+
updateConfig({ build: { inlineStylesheets: "always" } });
|
|
92
|
+
}
|
|
87
93
|
addWatchFile(join(root, ".env"));
|
|
88
94
|
const layout = join(root, options.layout ?? "src/layouts/Page.astro");
|
|
89
95
|
const configFile = join(root, "zef.config.ts");
|
package/dist/seo.d.ts
CHANGED
|
@@ -46,6 +46,8 @@ export type SeoInput = {
|
|
|
46
46
|
type?: string;
|
|
47
47
|
/** Données structurées en plus (article, évènement, FAQ…). */
|
|
48
48
|
jsonLd?: Record<string, unknown> | Record<string, unknown>[];
|
|
49
|
+
/** Icône de l'onglet (`/favicon.svg`) ; à défaut, le logo du client dans le CMS. */
|
|
50
|
+
icon?: string | null;
|
|
49
51
|
};
|
|
50
52
|
export type SeoTags = {
|
|
51
53
|
title: string;
|
package/dist/seo.js
CHANGED
|
@@ -73,6 +73,9 @@ export function seoTags(input) {
|
|
|
73
73
|
if (image)
|
|
74
74
|
add("name", "twitter:image", absolute(origin, image.url));
|
|
75
75
|
const links = [{ rel: "canonical", href: canonical }];
|
|
76
|
+
const icon = input.icon ?? site.logo;
|
|
77
|
+
if (icon)
|
|
78
|
+
links.push({ rel: "icon", href: icon });
|
|
76
79
|
const langs = Object.entries(alternates);
|
|
77
80
|
if (langs.length > 1) {
|
|
78
81
|
for (const [lang, href] of langs)
|
package/dist/ui.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Ouvre une fenêtre `<Dialog>` par son identifiant. */
|
|
2
|
+
export declare function openDialog(id: string): void;
|
|
3
|
+
/**
|
|
4
|
+
* Ferme une fenêtre en laissant jouer son animation de sortie (`[data-closing]`), puis la
|
|
5
|
+
* ferme vraiment ; tout de suite si le visiteur réduit les animations ou sans animation CSS.
|
|
6
|
+
*/
|
|
7
|
+
export declare function closeDialog(target: HTMLDialogElement | string): void;
|
package/dist/ui.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Comportements des composants d'interface (`<Dialog>`, `<Accordion>`) partagés par leurs
|
|
3
|
+
* scripts : exportés pour qu'un site les déclenche lui-même.
|
|
4
|
+
*/
|
|
5
|
+
const reduced = () => window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
6
|
+
/** Ouvre une fenêtre `<Dialog>` par son identifiant. */
|
|
7
|
+
export function openDialog(id) {
|
|
8
|
+
const dialog = document.getElementById(id);
|
|
9
|
+
if (dialog instanceof HTMLDialogElement && !dialog.open)
|
|
10
|
+
dialog.showModal();
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Ferme une fenêtre en laissant jouer son animation de sortie (`[data-closing]`), puis la
|
|
14
|
+
* ferme vraiment ; tout de suite si le visiteur réduit les animations ou sans animation CSS.
|
|
15
|
+
*/
|
|
16
|
+
export function closeDialog(target) {
|
|
17
|
+
const dialog = typeof target === "string" ? document.getElementById(target) : target;
|
|
18
|
+
if (!(dialog instanceof HTMLDialogElement) || !dialog.open || dialog.hasAttribute("data-closing"))
|
|
19
|
+
return;
|
|
20
|
+
const finish = () => {
|
|
21
|
+
dialog.removeAttribute("data-closing");
|
|
22
|
+
dialog.close();
|
|
23
|
+
};
|
|
24
|
+
if (reduced())
|
|
25
|
+
return finish();
|
|
26
|
+
dialog.setAttribute("data-closing", "");
|
|
27
|
+
const animated = getComputedStyle(dialog).animationName !== "none";
|
|
28
|
+
if (!animated)
|
|
29
|
+
return finish();
|
|
30
|
+
dialog.addEventListener("animationend", finish, { once: true });
|
|
31
|
+
// Filet : si l'animation ne se termine pas (onglet en arrière-plan), on ferme quand même.
|
|
32
|
+
setTimeout(() => dialog.hasAttribute("data-closing") && finish(), 400);
|
|
33
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agence-zef/astro",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Intégration Astro du CMS ZEF : routes, rendu des sections, aperçu, composants réutilisables sans style.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -50,11 +50,25 @@
|
|
|
50
50
|
"./VideoPlayer.astro": "./components/VideoPlayer.astro",
|
|
51
51
|
"./Subscriptions.astro": "./components/Subscriptions.astro",
|
|
52
52
|
"./ReservedNotice.astro": "./components/ReservedNotice.astro",
|
|
53
|
-
"./routes/shop.ts": "./components/routes/shop.ts"
|
|
53
|
+
"./routes/shop.ts": "./components/routes/shop.ts",
|
|
54
|
+
"./Dialog.astro": "./components/Dialog.astro",
|
|
55
|
+
"./Accordion.astro": "./components/Accordion.astro",
|
|
56
|
+
"./AccordionItem.astro": "./components/AccordionItem.astro",
|
|
57
|
+
"./Popover.astro": "./components/Popover.astro",
|
|
58
|
+
"./Field.astro": "./components/Field.astro",
|
|
59
|
+
"./Input.astro": "./components/Input.astro",
|
|
60
|
+
"./Select.astro": "./components/Select.astro",
|
|
61
|
+
"./Checkbox.astro": "./components/Checkbox.astro",
|
|
62
|
+
"./styles.css": "./styles.css",
|
|
63
|
+
"./ui": {
|
|
64
|
+
"types": "./dist/ui.d.ts",
|
|
65
|
+
"import": "./dist/ui.js"
|
|
66
|
+
}
|
|
54
67
|
},
|
|
55
68
|
"files": [
|
|
56
69
|
"dist",
|
|
57
|
-
"components"
|
|
70
|
+
"components",
|
|
71
|
+
"styles.css"
|
|
58
72
|
],
|
|
59
73
|
"publishConfig": {
|
|
60
74
|
"access": "public"
|
|
@@ -67,8 +81,8 @@
|
|
|
67
81
|
"@types/node": "^24.0.0"
|
|
68
82
|
},
|
|
69
83
|
"dependencies": {
|
|
70
|
-
"@agence-zef/schema": "0.
|
|
71
|
-
"@agence-zef/sdk": "0.
|
|
84
|
+
"@agence-zef/schema": "0.5.0",
|
|
85
|
+
"@agence-zef/sdk": "0.5.0"
|
|
72
86
|
},
|
|
73
87
|
"homepage": "https://agence-zef.fr",
|
|
74
88
|
"scripts": {
|
package/styles.css
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Habillage facultatif des composants de @agence-zef/astro : champs de formulaire (Form,
|
|
3
|
+
* AccountForm, Checkout, panier, Field / Input / Select / Checkbox), boutons, Dialog,
|
|
4
|
+
* Accordion, Popover, popups et bandeau cookies.
|
|
5
|
+
*
|
|
6
|
+
* - Importé une fois (`import "@agence-zef/astro/styles.css"` dans le gabarit).
|
|
7
|
+
* - Toutes les règles sont en `:where()` : aucune priorité, la moindre règle du site passe devant.
|
|
8
|
+
* - Tout se règle par les variables `--zef-*` ci-dessous, à redéfinir dans `:root` du site
|
|
9
|
+
* (ex. `--zef-accent: var(--color-accent)`).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
:where(:root) {
|
|
13
|
+
--zef-font: inherit;
|
|
14
|
+
--zef-text: currentColor;
|
|
15
|
+
--zef-muted: color-mix(in srgb, currentColor 62%, transparent);
|
|
16
|
+
--zef-surface: #fff;
|
|
17
|
+
--zef-surface-muted: color-mix(in srgb, currentColor 5%, transparent);
|
|
18
|
+
--zef-border: color-mix(in srgb, currentColor 18%, transparent);
|
|
19
|
+
--zef-border-strong: color-mix(in srgb, currentColor 35%, transparent);
|
|
20
|
+
--zef-accent: #18181b;
|
|
21
|
+
--zef-on-accent: #fff;
|
|
22
|
+
--zef-danger: #c62828;
|
|
23
|
+
--zef-success: #2e7d32;
|
|
24
|
+
--zef-focus: 0 0 0 3px color-mix(in srgb, var(--zef-accent) 30%, transparent);
|
|
25
|
+
--zef-radius: 0.5rem;
|
|
26
|
+
--zef-radius-small: 0.25rem;
|
|
27
|
+
--zef-gap: 1rem;
|
|
28
|
+
--zef-input-height: 2.75rem;
|
|
29
|
+
--zef-input-padding: 0.75rem;
|
|
30
|
+
--zef-shadow: 0 12px 40px rgb(0 0 0 / 0.16);
|
|
31
|
+
--zef-backdrop: rgb(0 0 0 / 0.45);
|
|
32
|
+
--zef-dialog-width: 32rem;
|
|
33
|
+
--zef-popover-width: 18rem;
|
|
34
|
+
--zef-duration: 0.2s;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* ---------- Formulaires ---------- */
|
|
38
|
+
|
|
39
|
+
:where(.zef-account, .zef-checkout fieldset, .zef-cart-add) {
|
|
40
|
+
display: flex;
|
|
41
|
+
flex-direction: column;
|
|
42
|
+
gap: var(--zef-gap);
|
|
43
|
+
font-family: var(--zef-font);
|
|
44
|
+
}
|
|
45
|
+
/* Formulaire du CMS : champs en pleine largeur ou en demi-largeur (`data-width="half"`). */
|
|
46
|
+
:where(.zef-form) {
|
|
47
|
+
display: flex;
|
|
48
|
+
flex-wrap: wrap;
|
|
49
|
+
gap: var(--zef-gap);
|
|
50
|
+
font-family: var(--zef-font);
|
|
51
|
+
}
|
|
52
|
+
:where(.zef-form) > :where(*) {
|
|
53
|
+
flex: 1 1 100%;
|
|
54
|
+
}
|
|
55
|
+
/* Champs masqués (logique conditionnelle, erreurs vides) : `hidden` l'emporte sur `display`. */
|
|
56
|
+
:where(.zef-form, .zef-account, .zef-checkout, .zef-cart, .zef-cart-add, .zef-field)
|
|
57
|
+
:where([hidden]) {
|
|
58
|
+
/* biome-ignore lint/complexity/noImportantStyles: `hidden` doit gagner sur tout `display` du site. */
|
|
59
|
+
display: none !important;
|
|
60
|
+
}
|
|
61
|
+
:where(.zef-checkout fieldset) {
|
|
62
|
+
margin: 0;
|
|
63
|
+
padding: 0;
|
|
64
|
+
border: 0;
|
|
65
|
+
}
|
|
66
|
+
:where(.zef-checkout legend, .zef-form legend, .zef-account legend) {
|
|
67
|
+
margin-bottom: 0.5rem;
|
|
68
|
+
padding: 0;
|
|
69
|
+
font-weight: 600;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* Champ : libellé au-dessus du contrôle. */
|
|
73
|
+
:where(.zef-field, .zef-form__field, .zef-account__field, .zef-checkout fieldset > label) {
|
|
74
|
+
display: flex;
|
|
75
|
+
flex-direction: column;
|
|
76
|
+
gap: 0.375rem;
|
|
77
|
+
min-width: 0;
|
|
78
|
+
}
|
|
79
|
+
:where(
|
|
80
|
+
.zef-label,
|
|
81
|
+
.zef-form__label,
|
|
82
|
+
.zef-account__field > label:not(:has(input[type="checkbox"])),
|
|
83
|
+
.zef-checkout fieldset > label
|
|
84
|
+
) {
|
|
85
|
+
font-weight: 600;
|
|
86
|
+
font-size: 0.9375rem;
|
|
87
|
+
}
|
|
88
|
+
:where(.zef-required) {
|
|
89
|
+
color: var(--zef-danger);
|
|
90
|
+
}
|
|
91
|
+
:where(.zef-form__field[data-width="half"]) {
|
|
92
|
+
flex: 1 1 calc(50% - var(--zef-gap) / 2);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* Zones de saisie et listes. */
|
|
96
|
+
:where(
|
|
97
|
+
.zef-input,
|
|
98
|
+
.zef-select,
|
|
99
|
+
:is(.zef-form, .zef-account, .zef-checkout, .zef-cart, .zef-cart-add, .zef-subscriptions)
|
|
100
|
+
:is(
|
|
101
|
+
input:not(
|
|
102
|
+
[type="checkbox"],
|
|
103
|
+
[type="radio"],
|
|
104
|
+
[type="hidden"],
|
|
105
|
+
[type="file"],
|
|
106
|
+
[type="submit"],
|
|
107
|
+
[type="button"]
|
|
108
|
+
),
|
|
109
|
+
select,
|
|
110
|
+
textarea
|
|
111
|
+
)
|
|
112
|
+
) {
|
|
113
|
+
box-sizing: border-box;
|
|
114
|
+
width: 100%;
|
|
115
|
+
min-height: var(--zef-input-height);
|
|
116
|
+
padding: 0 var(--zef-input-padding);
|
|
117
|
+
border: 1px solid var(--zef-border-strong);
|
|
118
|
+
border-radius: var(--zef-radius);
|
|
119
|
+
background: var(--zef-surface);
|
|
120
|
+
color: inherit;
|
|
121
|
+
font: inherit;
|
|
122
|
+
transition:
|
|
123
|
+
border-color var(--zef-duration),
|
|
124
|
+
box-shadow var(--zef-duration);
|
|
125
|
+
}
|
|
126
|
+
:where(textarea.zef-input, :is(.zef-form, .zef-account, .zef-cart-add) textarea) {
|
|
127
|
+
min-height: 7.5rem;
|
|
128
|
+
padding-block: var(--zef-input-padding);
|
|
129
|
+
resize: vertical;
|
|
130
|
+
}
|
|
131
|
+
:where(
|
|
132
|
+
.zef-input,
|
|
133
|
+
.zef-select,
|
|
134
|
+
:is(.zef-form, .zef-account, .zef-checkout, .zef-cart, .zef-cart-add, .zef-subscriptions)
|
|
135
|
+
:is(input, select, textarea)
|
|
136
|
+
):focus-visible {
|
|
137
|
+
border-color: var(--zef-accent);
|
|
138
|
+
outline: none;
|
|
139
|
+
box-shadow: var(--zef-focus);
|
|
140
|
+
}
|
|
141
|
+
:where(
|
|
142
|
+
.zef-input[aria-invalid="true"],
|
|
143
|
+
.zef-select[aria-invalid="true"],
|
|
144
|
+
[data-invalid] :is(input, select, textarea)
|
|
145
|
+
) {
|
|
146
|
+
border-color: var(--zef-danger);
|
|
147
|
+
}
|
|
148
|
+
:where(
|
|
149
|
+
.zef-input,
|
|
150
|
+
.zef-select,
|
|
151
|
+
:is(.zef-form, .zef-account, .zef-checkout) :is(input, select, textarea)
|
|
152
|
+
):disabled {
|
|
153
|
+
opacity: 0.55;
|
|
154
|
+
cursor: not-allowed;
|
|
155
|
+
}
|
|
156
|
+
:where(.zef-form input[type="file"]) {
|
|
157
|
+
font: inherit;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* Cases à cocher et boutons radio. */
|
|
161
|
+
:where(
|
|
162
|
+
.zef-check,
|
|
163
|
+
.zef-form__check,
|
|
164
|
+
.zef-account label:has(> input[type="checkbox"]),
|
|
165
|
+
.zef-checkout label:has(> input:is([type="checkbox"], [type="radio"])),
|
|
166
|
+
.zef-cart-add label:has(> input[type="radio"])
|
|
167
|
+
) {
|
|
168
|
+
display: flex;
|
|
169
|
+
align-items: flex-start;
|
|
170
|
+
gap: 0.625rem;
|
|
171
|
+
font-weight: 400;
|
|
172
|
+
cursor: pointer;
|
|
173
|
+
}
|
|
174
|
+
:where(.zef-check, .zef-form__check, .zef-account, .zef-checkout, .zef-cart-add, .zef-cookies)
|
|
175
|
+
:where(input[type="checkbox"], input[type="radio"]) {
|
|
176
|
+
flex-shrink: 0;
|
|
177
|
+
width: 1.125rem;
|
|
178
|
+
height: 1.125rem;
|
|
179
|
+
margin: 0.125rem 0 0;
|
|
180
|
+
accent-color: var(--zef-accent);
|
|
181
|
+
}
|
|
182
|
+
:where(.zef-form__choices) {
|
|
183
|
+
display: flex;
|
|
184
|
+
flex-direction: column;
|
|
185
|
+
gap: 0.5rem;
|
|
186
|
+
margin: 0;
|
|
187
|
+
padding: 0;
|
|
188
|
+
border: 0;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/* Aide, erreurs, messages. */
|
|
192
|
+
:where(.zef-help, .zef-account__help, .zef-account__optional, .zef-checkout__optional) {
|
|
193
|
+
color: var(--zef-muted);
|
|
194
|
+
font-size: 0.875rem;
|
|
195
|
+
font-weight: 400;
|
|
196
|
+
}
|
|
197
|
+
:where(.zef-error, .zef-form__error, .zef-account__error, .zef-checkout__error, .zef-cart__error) {
|
|
198
|
+
margin: 0;
|
|
199
|
+
color: var(--zef-danger);
|
|
200
|
+
font-size: 0.875rem;
|
|
201
|
+
}
|
|
202
|
+
:where(.zef-form__status, .zef-account__notice, .zef-checkout__notice, .zef-cart__warning) {
|
|
203
|
+
margin: 0;
|
|
204
|
+
padding: 0.75rem 1rem;
|
|
205
|
+
border-radius: var(--zef-radius);
|
|
206
|
+
background: var(--zef-surface-muted);
|
|
207
|
+
}
|
|
208
|
+
:where(.zef-account__success, .zef-cart__success, .zef-subscriptions__success, .zef-form__done) {
|
|
209
|
+
color: var(--zef-success);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* ---------- Boutons ---------- */
|
|
213
|
+
|
|
214
|
+
:where(
|
|
215
|
+
.zef-button,
|
|
216
|
+
.zef-form__submit,
|
|
217
|
+
.zef-account__submit,
|
|
218
|
+
.zef-checkout__submit,
|
|
219
|
+
.zef-cart__checkout,
|
|
220
|
+
.zef-cart-add button[type="submit"],
|
|
221
|
+
.zef-subscriptions__cancel
|
|
222
|
+
) {
|
|
223
|
+
display: inline-flex;
|
|
224
|
+
align-items: center;
|
|
225
|
+
justify-content: center;
|
|
226
|
+
gap: 0.5rem;
|
|
227
|
+
min-height: var(--zef-input-height);
|
|
228
|
+
padding: 0 1.25rem;
|
|
229
|
+
border: 1px solid var(--zef-accent);
|
|
230
|
+
border-radius: var(--zef-radius);
|
|
231
|
+
background: var(--zef-accent);
|
|
232
|
+
color: var(--zef-on-accent);
|
|
233
|
+
font: inherit;
|
|
234
|
+
font-weight: 600;
|
|
235
|
+
text-decoration: none;
|
|
236
|
+
cursor: pointer;
|
|
237
|
+
transition:
|
|
238
|
+
filter var(--zef-duration),
|
|
239
|
+
opacity var(--zef-duration);
|
|
240
|
+
}
|
|
241
|
+
:where(.zef-button[data-variant="secondary"], .zef-subscriptions__cancel) {
|
|
242
|
+
background: transparent;
|
|
243
|
+
color: var(--zef-accent);
|
|
244
|
+
}
|
|
245
|
+
:where(
|
|
246
|
+
.zef-button,
|
|
247
|
+
.zef-form__submit,
|
|
248
|
+
.zef-account__submit,
|
|
249
|
+
.zef-checkout__submit,
|
|
250
|
+
.zef-cart__checkout
|
|
251
|
+
):hover {
|
|
252
|
+
filter: brightness(1.12);
|
|
253
|
+
}
|
|
254
|
+
:where(.zef-button, .zef-form__submit, .zef-account__submit, .zef-checkout__submit):focus-visible {
|
|
255
|
+
outline: none;
|
|
256
|
+
box-shadow: var(--zef-focus);
|
|
257
|
+
}
|
|
258
|
+
:where(.zef-button, .zef-form__submit, .zef-account__submit, .zef-checkout__submit):disabled {
|
|
259
|
+
opacity: 0.55;
|
|
260
|
+
cursor: progress;
|
|
261
|
+
}
|
|
262
|
+
:where(.zef-form__submit, .zef-account__submit) {
|
|
263
|
+
align-self: flex-start;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/* ---------- Dialog et popups ---------- */
|
|
267
|
+
|
|
268
|
+
:where(.zef-dialog, .zef-popup) {
|
|
269
|
+
box-sizing: border-box;
|
|
270
|
+
width: min(var(--zef-dialog-width), calc(100vw - 2rem));
|
|
271
|
+
max-height: calc(100dvh - 2rem);
|
|
272
|
+
padding: 0;
|
|
273
|
+
border: 0;
|
|
274
|
+
border-radius: calc(var(--zef-radius) * 1.5);
|
|
275
|
+
background: var(--zef-surface);
|
|
276
|
+
color: inherit;
|
|
277
|
+
box-shadow: var(--zef-shadow);
|
|
278
|
+
overflow: auto;
|
|
279
|
+
}
|
|
280
|
+
:where(.zef-dialog, .zef-popup)::backdrop {
|
|
281
|
+
background: var(--zef-backdrop);
|
|
282
|
+
}
|
|
283
|
+
:where(.zef-dialog__panel) {
|
|
284
|
+
position: relative;
|
|
285
|
+
padding: 2rem;
|
|
286
|
+
}
|
|
287
|
+
:where(.zef-dialog__close, .zef-popup__close) {
|
|
288
|
+
position: absolute;
|
|
289
|
+
top: 0.75rem;
|
|
290
|
+
right: 0.75rem;
|
|
291
|
+
display: grid;
|
|
292
|
+
place-items: center;
|
|
293
|
+
width: 2.25rem;
|
|
294
|
+
height: 2.25rem;
|
|
295
|
+
padding: 0;
|
|
296
|
+
border: 0;
|
|
297
|
+
border-radius: 999px;
|
|
298
|
+
background: transparent;
|
|
299
|
+
color: inherit;
|
|
300
|
+
cursor: pointer;
|
|
301
|
+
}
|
|
302
|
+
:where(.zef-dialog__close, .zef-popup__close):hover {
|
|
303
|
+
background: var(--zef-surface-muted);
|
|
304
|
+
}
|
|
305
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
306
|
+
:where(.zef-dialog[open], .zef-popup[open]) {
|
|
307
|
+
animation: zef-dialog-in var(--zef-duration) cubic-bezier(0.16, 1, 0.3, 1);
|
|
308
|
+
}
|
|
309
|
+
:where(.zef-dialog[open])::backdrop {
|
|
310
|
+
animation: zef-fade-in var(--zef-duration) ease-out;
|
|
311
|
+
}
|
|
312
|
+
:where(.zef-dialog[data-closing]) {
|
|
313
|
+
animation: zef-dialog-out calc(var(--zef-duration) * 0.75) ease-in forwards;
|
|
314
|
+
}
|
|
315
|
+
:where(.zef-dialog[data-closing])::backdrop {
|
|
316
|
+
animation: zef-fade-out calc(var(--zef-duration) * 0.75) ease-in forwards;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
@keyframes zef-dialog-in {
|
|
320
|
+
from {
|
|
321
|
+
opacity: 0;
|
|
322
|
+
transform: translateY(0.5rem) scale(0.98);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
@keyframes zef-dialog-out {
|
|
326
|
+
to {
|
|
327
|
+
opacity: 0;
|
|
328
|
+
transform: translateY(0.5rem) scale(0.98);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
@keyframes zef-fade-in {
|
|
332
|
+
from {
|
|
333
|
+
opacity: 0;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
@keyframes zef-fade-out {
|
|
337
|
+
to {
|
|
338
|
+
opacity: 0;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/* ---------- Accordéon ---------- */
|
|
343
|
+
|
|
344
|
+
:where(.zef-accordion) {
|
|
345
|
+
display: flex;
|
|
346
|
+
flex-direction: column;
|
|
347
|
+
border-top: 1px solid var(--zef-border);
|
|
348
|
+
}
|
|
349
|
+
:where(.zef-accordion__item) {
|
|
350
|
+
border-bottom: 1px solid var(--zef-border);
|
|
351
|
+
/* Hauteur animée (navigateurs récents) ; sinon ouverture instantanée. */
|
|
352
|
+
interpolate-size: allow-keywords;
|
|
353
|
+
}
|
|
354
|
+
:where(.zef-accordion__summary) {
|
|
355
|
+
display: flex;
|
|
356
|
+
align-items: center;
|
|
357
|
+
justify-content: space-between;
|
|
358
|
+
gap: 1rem;
|
|
359
|
+
padding: 1.125rem 0;
|
|
360
|
+
font-weight: 600;
|
|
361
|
+
list-style: none;
|
|
362
|
+
cursor: pointer;
|
|
363
|
+
}
|
|
364
|
+
:where(.zef-accordion__summary)::-webkit-details-marker {
|
|
365
|
+
display: none;
|
|
366
|
+
}
|
|
367
|
+
:where(.zef-accordion__summary)::after {
|
|
368
|
+
content: "";
|
|
369
|
+
flex-shrink: 0;
|
|
370
|
+
width: 0.5rem;
|
|
371
|
+
height: 0.5rem;
|
|
372
|
+
margin-right: 0.25rem;
|
|
373
|
+
border-right: 1.5px solid currentColor;
|
|
374
|
+
border-bottom: 1.5px solid currentColor;
|
|
375
|
+
transform: translateY(-25%) rotate(45deg);
|
|
376
|
+
transition: transform var(--zef-duration);
|
|
377
|
+
}
|
|
378
|
+
:where(.zef-accordion__item[open] > .zef-accordion__summary)::after {
|
|
379
|
+
transform: translateY(25%) rotate(-135deg);
|
|
380
|
+
}
|
|
381
|
+
:where(.zef-accordion__summary):focus-visible {
|
|
382
|
+
outline: none;
|
|
383
|
+
box-shadow: var(--zef-focus);
|
|
384
|
+
border-radius: var(--zef-radius-small);
|
|
385
|
+
}
|
|
386
|
+
:where(.zef-accordion__content) {
|
|
387
|
+
padding-bottom: 1.25rem;
|
|
388
|
+
color: var(--zef-muted);
|
|
389
|
+
}
|
|
390
|
+
:where(.zef-accordion__content) > :first-child {
|
|
391
|
+
margin-top: 0;
|
|
392
|
+
}
|
|
393
|
+
:where(.zef-accordion__content) > :last-child {
|
|
394
|
+
margin-bottom: 0;
|
|
395
|
+
}
|
|
396
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
397
|
+
:where(.zef-accordion__item)::details-content {
|
|
398
|
+
block-size: 0;
|
|
399
|
+
overflow: hidden;
|
|
400
|
+
transition:
|
|
401
|
+
block-size var(--zef-duration) ease,
|
|
402
|
+
content-visibility var(--zef-duration) allow-discrete;
|
|
403
|
+
}
|
|
404
|
+
:where(.zef-accordion__item[open])::details-content {
|
|
405
|
+
block-size: auto;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/* ---------- Popover ---------- */
|
|
410
|
+
|
|
411
|
+
:where(.zef-popover) {
|
|
412
|
+
box-sizing: border-box;
|
|
413
|
+
width: min(var(--zef-popover-width), calc(100vw - 2rem));
|
|
414
|
+
margin: 0;
|
|
415
|
+
padding: 1rem;
|
|
416
|
+
border: 1px solid var(--zef-border);
|
|
417
|
+
border-radius: var(--zef-radius);
|
|
418
|
+
background: var(--zef-surface);
|
|
419
|
+
color: inherit;
|
|
420
|
+
box-shadow: var(--zef-shadow);
|
|
421
|
+
/* Positionnement d'ancre : sous le bouton qui l'ouvre, retourné s'il manque de place. */
|
|
422
|
+
position-area: block-end span-inline-end;
|
|
423
|
+
position-try-fallbacks: flip-block, flip-inline;
|
|
424
|
+
margin-block: 0.375rem;
|
|
425
|
+
}
|
|
426
|
+
:where(.zef-popover[data-placement="top"]) {
|
|
427
|
+
position-area: block-start span-inline-end;
|
|
428
|
+
}
|
|
429
|
+
:where(.zef-popover[data-placement="right"]) {
|
|
430
|
+
position-area: inline-end span-block-end;
|
|
431
|
+
margin: 0 0.375rem;
|
|
432
|
+
}
|
|
433
|
+
:where(.zef-popover[data-placement="left"]) {
|
|
434
|
+
position-area: inline-start span-block-end;
|
|
435
|
+
margin: 0 0.375rem;
|
|
436
|
+
}
|
|
437
|
+
@supports not (position-area: block-end) {
|
|
438
|
+
:where(.zef-popover) {
|
|
439
|
+
margin: auto;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
443
|
+
:where(.zef-popover) {
|
|
444
|
+
transition:
|
|
445
|
+
opacity var(--zef-duration),
|
|
446
|
+
transform var(--zef-duration),
|
|
447
|
+
overlay var(--zef-duration) allow-discrete,
|
|
448
|
+
display var(--zef-duration) allow-discrete;
|
|
449
|
+
opacity: 0;
|
|
450
|
+
transform: translateY(-0.25rem);
|
|
451
|
+
}
|
|
452
|
+
:where(.zef-popover:popover-open) {
|
|
453
|
+
opacity: 1;
|
|
454
|
+
transform: none;
|
|
455
|
+
}
|
|
456
|
+
@starting-style {
|
|
457
|
+
:where(.zef-popover:popover-open) {
|
|
458
|
+
opacity: 0;
|
|
459
|
+
transform: translateY(-0.25rem);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
@media (max-width: 40rem) {
|
|
465
|
+
:where(.zef-form__field[data-width="half"]) {
|
|
466
|
+
flex-basis: 100%;
|
|
467
|
+
}
|
|
468
|
+
}
|