@sentropic/design-system-svelte 0.34.46 → 0.34.48
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/AppShell.svelte +167 -0
- package/dist/AppShell.svelte.d.ts +8 -0
- package/dist/AppShell.svelte.d.ts.map +1 -0
- package/dist/Checkbox.svelte +61 -3
- package/dist/Checkbox.svelte.d.ts +5 -0
- package/dist/Checkbox.svelte.d.ts.map +1 -1
- package/dist/IdentityButton.svelte +128 -0
- package/dist/IdentityButton.svelte.d.ts +27 -0
- package/dist/IdentityButton.svelte.d.ts.map +1 -0
- package/dist/NavActionStack.svelte +203 -0
- package/dist/NavActionStack.svelte.d.ts +39 -0
- package/dist/NavActionStack.svelte.d.ts.map +1 -0
- package/dist/Overline.svelte +46 -0
- package/dist/Overline.svelte.d.ts +15 -0
- package/dist/Overline.svelte.d.ts.map +1 -0
- package/dist/Search.svelte +11 -1
- package/dist/Search.svelte.d.ts +2 -0
- package/dist/Search.svelte.d.ts.map +1 -1
- package/dist/StatusDot.svelte +160 -0
- package/dist/StatusDot.svelte.d.ts +19 -0
- package/dist/StatusDot.svelte.d.ts.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/site-config.d.ts +106 -0
- package/dist/site-config.d.ts.map +1 -0
- package/dist/site-config.js +34 -0
- package/package.json +1 -1
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// App-shell SVELTE : COMPOSE les vrais composants du design system — zéro contrôle
|
|
3
|
+
// bricolé. Triggers = DS Button / IconButton ; menus = DS MenuPopover + Menu ;
|
|
4
|
+
// identité = DS IdentityMenu ; barre = DS Header. Le look vient donc des tokens DS
|
|
5
|
+
// (--st-component-control-*) → pixel-cohérent avec le header de référence, et les
|
|
6
|
+
// menus sont ceux du DS (fonctionnels). Piloté par `siteConfig`.
|
|
7
|
+
import Header from "./Header.svelte";
|
|
8
|
+
import Button from "./Button.svelte";
|
|
9
|
+
import IconButton from "./IconButton.svelte";
|
|
10
|
+
import MenuPopover from "./MenuPopover.svelte";
|
|
11
|
+
import Menu from "./Menu.svelte";
|
|
12
|
+
import { Boxes, ChevronDown, Globe, Moon, Palette, Search as SearchIcon, Sun } from "@lucide/svelte";
|
|
13
|
+
import IdentityButton from "./IdentityButton.svelte";
|
|
14
|
+
import type { SiteConfig } from "./site-config";
|
|
15
|
+
|
|
16
|
+
let { config }: { config: SiteConfig } = $props();
|
|
17
|
+
|
|
18
|
+
const brand = $derived(config.brand ?? { name: "Sentropic" });
|
|
19
|
+
const nav = $derived(Array.isArray(config.nav) ? config.nav : []);
|
|
20
|
+
const t = $derived(config.theming ?? { themes: [], theme: "" });
|
|
21
|
+
|
|
22
|
+
const isActive = (item: { href: string; active?: boolean }) =>
|
|
23
|
+
item.active != null
|
|
24
|
+
? item.active
|
|
25
|
+
: config.activePath != null &&
|
|
26
|
+
(item.href === config.activePath || (item.href !== "/" && (config.activePath ?? "").startsWith(item.href)));
|
|
27
|
+
|
|
28
|
+
const themeItems = $derived((t.themes ?? []).map((o) => ({ label: o.label, value: o.id })));
|
|
29
|
+
const fwItems = $derived((config.frameworkSwitcher?.available ?? []).map((o) => ({ label: o.label, value: o.id })));
|
|
30
|
+
const localeItems = $derived((config.locale?.available ?? []).map((o) => ({ label: o.label, value: o.code })));
|
|
31
|
+
const fwLabel = $derived((config.frameworkSwitcher?.available ?? []).find((o) => o.id === config.frameworkSwitcher?.current)?.label ?? "");
|
|
32
|
+
const themeLabel = $derived((t.themes ?? []).find((o) => o.id === t.theme)?.label ?? "");
|
|
33
|
+
|
|
34
|
+
// Ancrage des menus (MenuPopover veut un HTMLElement) : on bind un span tight.
|
|
35
|
+
let themeEl = $state<HTMLElement | null>(null);
|
|
36
|
+
let fwEl = $state<HTMLElement | null>(null);
|
|
37
|
+
let localeEl = $state<HTMLElement | null>(null);
|
|
38
|
+
let themeOpen = $state(false);
|
|
39
|
+
let fwOpen = $state(false);
|
|
40
|
+
let localeOpen = $state(false);
|
|
41
|
+
|
|
42
|
+
function cycleColorMode() {
|
|
43
|
+
const cur = t.colorMode;
|
|
44
|
+
t.onColorModeChange?.(cur === "light" ? "dark" : cur === "dark" ? "auto" : "light");
|
|
45
|
+
}
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
{#snippet logo()}
|
|
49
|
+
<a class="st-shell__brand" href={brand.href ?? "/"} aria-label={brand.label ?? [brand.name, brand.productName].filter(Boolean).join(" ")}>
|
|
50
|
+
{#if brand.logoSrc}<img class="st-shell__brandMark" src={brand.logoSrc} alt="" aria-hidden="true" />{/if}
|
|
51
|
+
<span class="st-shell__brandCopy">
|
|
52
|
+
{#if brand.name}<span class="st-shell__brandName">{brand.name}</span>{/if}
|
|
53
|
+
{#if brand.productName}<span class="st-shell__brandProduct">{brand.productName}</span>{/if}
|
|
54
|
+
</span>
|
|
55
|
+
</a>
|
|
56
|
+
{/snippet}
|
|
57
|
+
|
|
58
|
+
{#snippet navigation()}
|
|
59
|
+
<nav class="st-shell__nav" aria-label={config.navLabel ?? "Navigation"}>
|
|
60
|
+
{#each nav as item (item.href)}
|
|
61
|
+
<a class="st-shell__navLink" href={item.href} aria-current={isActive(item) ? "page" : undefined}>{item.label}</a>
|
|
62
|
+
{/each}
|
|
63
|
+
</nav>
|
|
64
|
+
{/snippet}
|
|
65
|
+
|
|
66
|
+
{#snippet actions()}
|
|
67
|
+
<div class="st-shell__actions">
|
|
68
|
+
{#if config.search?.enabled}
|
|
69
|
+
<Button variant="secondary" size="sm" class="st-shell__search" onclick={() => config.search?.onSearch?.("")}>
|
|
70
|
+
<SearchIcon size={16} strokeWidth={2.1} aria-hidden="true" />
|
|
71
|
+
<span>{config.search.placeholder ?? "Rechercher…"}</span>
|
|
72
|
+
<kbd class="st-shell__kbd">/</kbd>
|
|
73
|
+
</Button>
|
|
74
|
+
{/if}
|
|
75
|
+
|
|
76
|
+
{#if config.frameworkSwitcher?.enabled}
|
|
77
|
+
<span class="st-shell__menuWrap" bind:this={fwEl}>
|
|
78
|
+
<Button variant="secondary" size="sm" class="st-shell__switch" onclick={() => (fwOpen = !fwOpen)} aria-haspopup="menu" aria-expanded={fwOpen}>
|
|
79
|
+
<Boxes size={14} aria-hidden="true" /><span>{fwLabel}</span><ChevronDown size={14} aria-hidden="true" />
|
|
80
|
+
</Button>
|
|
81
|
+
</span>
|
|
82
|
+
<MenuPopover bind:open={fwOpen} trigger={fwEl} placement="bottom-end" label="Framework">
|
|
83
|
+
<Menu label="Framework" items={fwItems} onselect={(v) => { config.frameworkSwitcher?.onChange?.(v); fwOpen = false; }} />
|
|
84
|
+
</MenuPopover>
|
|
85
|
+
{/if}
|
|
86
|
+
|
|
87
|
+
{#if (t.themes ?? []).length}
|
|
88
|
+
<span class="st-shell__menuWrap" bind:this={themeEl}>
|
|
89
|
+
<Button variant="secondary" size="sm" class="st-shell__switch" onclick={() => (themeOpen = !themeOpen)} aria-haspopup="menu" aria-expanded={themeOpen}>
|
|
90
|
+
<Palette size={14} aria-hidden="true" /><span>{themeLabel}</span><ChevronDown size={14} aria-hidden="true" />
|
|
91
|
+
</Button>
|
|
92
|
+
</span>
|
|
93
|
+
<MenuPopover bind:open={themeOpen} trigger={themeEl} placement="bottom-end" label={t.themeLabel ?? "Thème"}>
|
|
94
|
+
<Menu label={t.themeLabel ?? "Thème"} items={themeItems} onselect={(v) => { t.onThemeChange?.(v); themeOpen = false; }} />
|
|
95
|
+
</MenuPopover>
|
|
96
|
+
{/if}
|
|
97
|
+
|
|
98
|
+
{#if t.colorMode}
|
|
99
|
+
<IconButton size="sm" variant="ghost" aria-label="Mode couleur" onclick={cycleColorMode}>
|
|
100
|
+
{#if t.colorMode === "dark"}<Moon size={16} aria-hidden="true" />{:else}<Sun size={16} aria-hidden="true" />{/if}
|
|
101
|
+
</IconButton>
|
|
102
|
+
{/if}
|
|
103
|
+
|
|
104
|
+
{#if config.locale}
|
|
105
|
+
<span class="st-shell__menuWrap" bind:this={localeEl}>
|
|
106
|
+
<Button variant="secondary" size="sm" class="st-shell__switch" onclick={() => (localeOpen = !localeOpen)} aria-haspopup="menu" aria-expanded={localeOpen}>
|
|
107
|
+
<Globe size={14} aria-hidden="true" /><span>{(config.locale.current ?? "").toUpperCase()}</span><ChevronDown size={14} aria-hidden="true" />
|
|
108
|
+
</Button>
|
|
109
|
+
</span>
|
|
110
|
+
<MenuPopover bind:open={localeOpen} trigger={localeEl} placement="bottom-end" label={config.locale.label ?? "Langue"}>
|
|
111
|
+
<Menu label={config.locale.label ?? "Langue"} items={localeItems} onselect={(v) => { config.locale?.onChange?.(v); localeOpen = false; }} />
|
|
112
|
+
</MenuPopover>
|
|
113
|
+
{/if}
|
|
114
|
+
|
|
115
|
+
{#if config.identity}
|
|
116
|
+
<IdentityButton
|
|
117
|
+
mode="icon"
|
|
118
|
+
authState={config.identity.state}
|
|
119
|
+
user={config.identity.user ?? null}
|
|
120
|
+
signInLabel={config.identity.label ?? "Se connecter"}
|
|
121
|
+
onSignIn={() => config.identity?.onSignIn?.()}
|
|
122
|
+
onSignOut={() => config.identity?.onSignOut?.()}
|
|
123
|
+
menu={config.identity.menu ?? []}
|
|
124
|
+
/>
|
|
125
|
+
{/if}
|
|
126
|
+
</div>
|
|
127
|
+
{/snippet}
|
|
128
|
+
|
|
129
|
+
<Header class="st-shell" label={config.brand?.label ?? "Navigation"} logo={logo} navigation={navigation} actions={actions} />
|
|
130
|
+
|
|
131
|
+
<style>
|
|
132
|
+
/* Hauteur de barre alignée sur la référence docs (80px) : le Header DS lit
|
|
133
|
+
`--st-component-header-height` (défaut 3.5rem) ; on le porte à 5rem + padding 1.5rem. */
|
|
134
|
+
:global(.st-shell.st-header) { --st-component-header-height: 5rem; padding-inline: var(--st-spacing-6, 1.5rem); }
|
|
135
|
+
/* Style UNIQUE « bouton de contrôle » token-driven, applique a TOUS les controles
|
|
136
|
+
du header (search, switchers, mode couleur, login) — fini les 3 styles divergents
|
|
137
|
+
(gris secondary / ghost transparent / foncé). Fond blanc (control-background),
|
|
138
|
+
bordure subtle, texte secondaire, 12px/650, 36px ; hover = control-hoverBackground. */
|
|
139
|
+
:global(.st-shell .st-button),
|
|
140
|
+
:global(.st-shell .st-iconButton) {
|
|
141
|
+
background: var(--st-component-control-background, var(--st-semantic-surface-default, #ffffff));
|
|
142
|
+
border: 1px solid var(--st-component-control-border, var(--st-semantic-border-subtle, #e2e8f0));
|
|
143
|
+
color: var(--st-semantic-text-secondary);
|
|
144
|
+
font-weight: 650;
|
|
145
|
+
font-size: 12px;
|
|
146
|
+
height: 2.25rem;
|
|
147
|
+
border-radius: var(--st-radius-md, 0.375rem);
|
|
148
|
+
}
|
|
149
|
+
:global(.st-shell .st-iconButton) { width: 2.25rem; padding: 0; }
|
|
150
|
+
:global(.st-shell .st-button:hover),
|
|
151
|
+
:global(.st-shell .st-iconButton:hover) {
|
|
152
|
+
background: var(--st-component-control-hoverBackground, var(--st-semantic-surface-subtle, #f1f5f9));
|
|
153
|
+
}
|
|
154
|
+
:global(.st-shell .st-button svg),
|
|
155
|
+
:global(.st-shell .st-iconButton svg) { flex-shrink: 0; }
|
|
156
|
+
.st-shell__brand { align-items: center; color: var(--st-semantic-text-primary); display: inline-flex; flex: 0 0 auto; gap: var(--st-spacing-3, 0.75rem); text-decoration: none; }
|
|
157
|
+
.st-shell__brandMark { width: 2rem; height: 2rem; }
|
|
158
|
+
.st-shell__brandCopy { display: flex; flex-direction: column; gap: 0.08rem; line-height: 1; }
|
|
159
|
+
.st-shell__brandName { font-weight: 760; font-size: 1rem; }
|
|
160
|
+
.st-shell__brandProduct { font-weight: 650; font-size: 0.75rem; color: var(--st-semantic-text-secondary); }
|
|
161
|
+
.st-shell__nav { display: flex; align-items: center; gap: var(--st-spacing-1, 0.25rem); }
|
|
162
|
+
.st-shell__navLink { color: var(--st-semantic-text-secondary); text-decoration: none; font-size: 0.875rem; line-height: 1; padding: 0.38rem 0.75rem; border-radius: var(--st-radius-md, 0.375rem); }
|
|
163
|
+
.st-shell__navLink[aria-current="page"] { color: var(--st-semantic-text-primary); font-weight: 650; }
|
|
164
|
+
.st-shell__actions { display: flex; align-items: center; gap: var(--st-spacing-2, 0.5rem); }
|
|
165
|
+
.st-shell__menuWrap { display: inline-flex; }
|
|
166
|
+
.st-shell__kbd { border: 1px solid var(--st-semantic-border-subtle); border-radius: 0.25rem; font-size: 0.6875rem; padding: 0 0.3rem; color: var(--st-semantic-text-secondary); }
|
|
167
|
+
</style>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SiteConfig } from "./site-config";
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
config: SiteConfig;
|
|
4
|
+
};
|
|
5
|
+
declare const AppShell: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
6
|
+
type AppShell = ReturnType<typeof AppShell>;
|
|
7
|
+
export default AppShell;
|
|
8
|
+
//# sourceMappingURL=AppShell.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AppShell.svelte.d.ts","sourceRoot":"","sources":["../src/lib/AppShell.svelte.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAE/C,KAAK,gBAAgB,GAAI;IAAE,MAAM,EAAE,UAAU,CAAA;CAAE,CAAC;AA+HjD,QAAA,MAAM,QAAQ,sDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
|
package/dist/Checkbox.svelte
CHANGED
|
@@ -1,23 +1,56 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
2
3
|
import type { HTMLInputAttributes } from "svelte/elements";
|
|
3
4
|
|
|
4
5
|
type CheckboxProps = Omit<HTMLInputAttributes, "class" | "type"> & {
|
|
5
6
|
label: string;
|
|
6
7
|
helperText?: string;
|
|
8
|
+
/** Secondary muted description line under the label (e.g. a filter hint). */
|
|
9
|
+
description?: string;
|
|
10
|
+
/** Trailing slot pushed to the row end (e.g. a count Badge). */
|
|
11
|
+
trailing?: Snippet;
|
|
7
12
|
invalid?: boolean;
|
|
8
13
|
class?: string;
|
|
9
14
|
};
|
|
10
15
|
|
|
11
|
-
let {
|
|
12
|
-
|
|
16
|
+
let {
|
|
17
|
+
label,
|
|
18
|
+
helperText,
|
|
19
|
+
description,
|
|
20
|
+
trailing,
|
|
21
|
+
invalid = false,
|
|
22
|
+
class: className,
|
|
23
|
+
...rest
|
|
24
|
+
}: CheckboxProps = $props();
|
|
25
|
+
|
|
26
|
+
const uid = $props.id();
|
|
27
|
+
const descriptionId = `${uid}-description`;
|
|
28
|
+
// Merge our description id with any consumer-provided aria-describedby so we
|
|
29
|
+
// never clobber an existing one.
|
|
30
|
+
const describedBy = () => {
|
|
31
|
+
if (!description) return rest["aria-describedby"];
|
|
32
|
+
return [rest["aria-describedby"], descriptionId].filter(Boolean).join(" ");
|
|
33
|
+
};
|
|
34
|
+
const classes = () =>
|
|
35
|
+
["st-choice", "st-choice--checkbox", description ? "st-choice--described" : null, className]
|
|
36
|
+
.filter(Boolean)
|
|
37
|
+
.join(" ");
|
|
13
38
|
</script>
|
|
14
39
|
|
|
15
40
|
<label class={classes()}>
|
|
16
|
-
<input
|
|
41
|
+
<input
|
|
42
|
+
{...rest}
|
|
43
|
+
class="st-choice__input"
|
|
44
|
+
type="checkbox"
|
|
45
|
+
aria-invalid={invalid ? "true" : undefined}
|
|
46
|
+
aria-describedby={describedBy()}
|
|
47
|
+
/>
|
|
17
48
|
<span class="st-choice__content">
|
|
18
49
|
<span class="st-choice__label">{label}</span>
|
|
50
|
+
{#if description}<span class="st-choice__description" id={descriptionId}>{description}</span>{/if}
|
|
19
51
|
{#if helperText}<span class="st-choice__help">{helperText}</span>{/if}
|
|
20
52
|
</span>
|
|
53
|
+
{#if trailing}<span class="st-choice__trailing">{@render trailing()}</span>{/if}
|
|
21
54
|
</label>
|
|
22
55
|
|
|
23
56
|
<style>
|
|
@@ -30,6 +63,24 @@
|
|
|
30
63
|
grid-template-columns: auto 1fr;
|
|
31
64
|
}
|
|
32
65
|
|
|
66
|
+
/* SIGNAL filter row: input + content + trailing count, content fills, trailing
|
|
67
|
+
pushed to the row end. Top-aligned so the box rides the first text line when a
|
|
68
|
+
secondary description wraps below the label. */
|
|
69
|
+
.st-choice:has(.st-choice__trailing) {
|
|
70
|
+
align-items: start;
|
|
71
|
+
grid-template-columns: auto 1fr auto;
|
|
72
|
+
justify-content: space-between;
|
|
73
|
+
width: 100%;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.st-choice__trailing {
|
|
77
|
+
align-items: center;
|
|
78
|
+
align-self: start;
|
|
79
|
+
display: inline-flex;
|
|
80
|
+
flex: 0 0 auto;
|
|
81
|
+
justify-content: flex-end;
|
|
82
|
+
}
|
|
83
|
+
|
|
33
84
|
.st-choice__input {
|
|
34
85
|
/* Natif stylé : couleur de coche thémée + taille + focus par stratégie
|
|
35
86
|
d'anatomie + états. Aucun widget custom, a11y native préservée. */
|
|
@@ -85,4 +136,11 @@
|
|
|
85
136
|
color: var(--st-component-field-helpText, var(--st-semantic-text-secondary));
|
|
86
137
|
font-size: 0.8125rem;
|
|
87
138
|
}
|
|
139
|
+
|
|
140
|
+
/* Secondary muted description under the label (SIGNAL filter hint). */
|
|
141
|
+
.st-choice__description {
|
|
142
|
+
color: var(--st-semantic-text-secondary);
|
|
143
|
+
font-size: 0.8125rem;
|
|
144
|
+
line-height: 1.4;
|
|
145
|
+
}
|
|
88
146
|
</style>
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
1
2
|
import type { HTMLInputAttributes } from "svelte/elements";
|
|
2
3
|
type CheckboxProps = Omit<HTMLInputAttributes, "class" | "type"> & {
|
|
3
4
|
label: string;
|
|
4
5
|
helperText?: string;
|
|
6
|
+
/** Secondary muted description line under the label (e.g. a filter hint). */
|
|
7
|
+
description?: string;
|
|
8
|
+
/** Trailing slot pushed to the row end (e.g. a count Badge). */
|
|
9
|
+
trailing?: Snippet;
|
|
5
10
|
invalid?: boolean;
|
|
6
11
|
class?: string;
|
|
7
12
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Checkbox.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Checkbox.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAGzD,KAAK,aAAa,GAAG,IAAI,CAAC,mBAAmB,EAAE,OAAO,GAAG,MAAM,CAAC,GAAG;IACjE,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;
|
|
1
|
+
{"version":3,"file":"Checkbox.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Checkbox.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAGzD,KAAK,aAAa,GAAG,IAAI,CAAC,mBAAmB,EAAE,OAAO,GAAG,MAAM,CAAC,GAAG;IACjE,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AA4CJ,QAAA,MAAM,QAAQ,mDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
// Types exportes — en Svelte 5 les exports de module vont dans <script module>
|
|
3
|
+
// (le <script> d'instance ne sert qu'aux props/runes).
|
|
4
|
+
export type IdentityState = "anonymous" | "authenticated";
|
|
5
|
+
export type IdentityMode = "icon" | "button" | "menu";
|
|
6
|
+
export type IdentityTone = "default" | "onColor";
|
|
7
|
+
export interface IdentityUser {
|
|
8
|
+
name: string;
|
|
9
|
+
avatarSrc?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface IdentityMenuEntry {
|
|
12
|
+
label: string;
|
|
13
|
+
href?: string;
|
|
14
|
+
onClick?: () => void;
|
|
15
|
+
}
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<script lang="ts">
|
|
19
|
+
// IdentityButton — composant d'identite DS multi-modes (incubation app-shell).
|
|
20
|
+
// Dedup l'ancien split (IconButton+user pour l'anonyme / IdentityMenu pour
|
|
21
|
+
// l'authentifie) en UN composant a modes. Compose les composants DS
|
|
22
|
+
// (IconButton / Button / MenuPopover / Menu) — zero primitive nouvelle.
|
|
23
|
+
import IconButton from "./IconButton.svelte";
|
|
24
|
+
import Button from "./Button.svelte";
|
|
25
|
+
import MenuPopover from "./MenuPopover.svelte";
|
|
26
|
+
import Menu from "./Menu.svelte";
|
|
27
|
+
import { User } from "@lucide/svelte";
|
|
28
|
+
|
|
29
|
+
let {
|
|
30
|
+
authState = "anonymous",
|
|
31
|
+
user = null,
|
|
32
|
+
mode = "icon",
|
|
33
|
+
tone = "default",
|
|
34
|
+
signInLabel = "Se connecter",
|
|
35
|
+
accountLabel = "Compte",
|
|
36
|
+
onSignIn,
|
|
37
|
+
onSignOut,
|
|
38
|
+
menu = [],
|
|
39
|
+
}: {
|
|
40
|
+
authState?: IdentityState;
|
|
41
|
+
user?: IdentityUser | null;
|
|
42
|
+
mode?: IdentityMode;
|
|
43
|
+
tone?: IdentityTone;
|
|
44
|
+
signInLabel?: string;
|
|
45
|
+
accountLabel?: string;
|
|
46
|
+
onSignIn?: () => void;
|
|
47
|
+
onSignOut?: () => void;
|
|
48
|
+
menu?: IdentityMenuEntry[];
|
|
49
|
+
} = $props();
|
|
50
|
+
|
|
51
|
+
let triggerEl = $state<HTMLElement | null>(null);
|
|
52
|
+
let open = $state(false);
|
|
53
|
+
|
|
54
|
+
const initials = $derived(
|
|
55
|
+
user?.name
|
|
56
|
+
? user.name.trim().split(/\s+/).map((w) => w[0]).slice(0, 2).join("").toUpperCase()
|
|
57
|
+
: "",
|
|
58
|
+
);
|
|
59
|
+
const authed = $derived(authState === "authenticated");
|
|
60
|
+
// Items du menu compte (mode "menu" ou clic sur l'avatar authentifie).
|
|
61
|
+
const menuItems = $derived([
|
|
62
|
+
...menu.map((e) => ({ label: e.label, value: e.href ?? e.label })),
|
|
63
|
+
...(onSignOut ? [{ label: "Se déconnecter", value: "__signout" }] : []),
|
|
64
|
+
]);
|
|
65
|
+
function onMenuSelect(value: string) {
|
|
66
|
+
open = false;
|
|
67
|
+
if (value === "__signout") return onSignOut?.();
|
|
68
|
+
const entry = menu.find((e) => (e.href ?? e.label) === value);
|
|
69
|
+
if (entry?.onClick) entry.onClick();
|
|
70
|
+
else if (entry?.href) location.href = entry.href;
|
|
71
|
+
}
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
{#snippet face()}
|
|
75
|
+
{#if authed && user?.avatarSrc}
|
|
76
|
+
<img class="st-identityBtn__avatar" src={user.avatarSrc} alt="" aria-hidden="true" />
|
|
77
|
+
{:else if authed && initials}
|
|
78
|
+
<span class="st-identityBtn__initials" aria-hidden="true">{initials}</span>
|
|
79
|
+
{:else}
|
|
80
|
+
<User size={16} strokeWidth={2.1} aria-hidden="true" />
|
|
81
|
+
{/if}
|
|
82
|
+
{/snippet}
|
|
83
|
+
|
|
84
|
+
<span class="st-identityBtn" class:st-identityBtn--onColor={tone === "onColor"}>
|
|
85
|
+
{#if mode === "button"}
|
|
86
|
+
<Button variant={authed ? "ghost" : "secondary"} size="sm" onclick={() => (authed ? (open = !open) : onSignIn?.())}>
|
|
87
|
+
{@render face()}
|
|
88
|
+
<span>{authed ? (user?.name ?? accountLabel) : signInLabel}</span>
|
|
89
|
+
</Button>
|
|
90
|
+
{:else if mode === "menu"}
|
|
91
|
+
<span class="st-identityBtn__wrap" bind:this={triggerEl}>
|
|
92
|
+
<IconButton size="sm" variant="ghost" aria-label={authed ? accountLabel : signInLabel} onclick={() => (open = !open)}>
|
|
93
|
+
{@render face()}
|
|
94
|
+
</IconButton>
|
|
95
|
+
</span>
|
|
96
|
+
{#if menuItems.length}
|
|
97
|
+
<MenuPopover bind:open trigger={triggerEl} placement="bottom-end" label={accountLabel}>
|
|
98
|
+
<Menu label={accountLabel} items={menuItems} onselect={onMenuSelect} />
|
|
99
|
+
</MenuPopover>
|
|
100
|
+
{/if}
|
|
101
|
+
{:else}
|
|
102
|
+
<!-- mode "icon" (defaut) : icone/avatar seul. -->
|
|
103
|
+
<IconButton
|
|
104
|
+
size="sm"
|
|
105
|
+
variant="ghost"
|
|
106
|
+
aria-label={authed ? accountLabel : signInLabel}
|
|
107
|
+
onclick={() => (authed ? (menuItems.length ? (open = !open) : undefined) : onSignIn?.())}
|
|
108
|
+
>
|
|
109
|
+
{@render face()}
|
|
110
|
+
</IconButton>
|
|
111
|
+
{/if}
|
|
112
|
+
</span>
|
|
113
|
+
|
|
114
|
+
<style>
|
|
115
|
+
.st-identityBtn { display: inline-flex; }
|
|
116
|
+
.st-identityBtn__wrap { display: inline-flex; }
|
|
117
|
+
.st-identityBtn__avatar { width: 1.5rem; height: 1.5rem; border-radius: 50%; object-fit: cover; }
|
|
118
|
+
.st-identityBtn__initials {
|
|
119
|
+
width: 1.5rem; height: 1.5rem; border-radius: 50%;
|
|
120
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
121
|
+
font-size: 0.6875rem; font-weight: 650;
|
|
122
|
+
background: var(--st-semantic-surface-subtle, #f1f5f9);
|
|
123
|
+
color: var(--st-semantic-text-primary, #0f172a);
|
|
124
|
+
}
|
|
125
|
+
/* tone onColor : icone blanche pour chromes colores (Airbus/DSFR/sombres). */
|
|
126
|
+
.st-identityBtn--onColor :global(.st-iconButton),
|
|
127
|
+
.st-identityBtn--onColor :global(.st-button) { color: #ffffff; }
|
|
128
|
+
</style>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type IdentityState = "anonymous" | "authenticated";
|
|
2
|
+
export type IdentityMode = "icon" | "button" | "menu";
|
|
3
|
+
export type IdentityTone = "default" | "onColor";
|
|
4
|
+
export interface IdentityUser {
|
|
5
|
+
name: string;
|
|
6
|
+
avatarSrc?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface IdentityMenuEntry {
|
|
9
|
+
label: string;
|
|
10
|
+
href?: string;
|
|
11
|
+
onClick?: () => void;
|
|
12
|
+
}
|
|
13
|
+
type $$ComponentProps = {
|
|
14
|
+
authState?: IdentityState;
|
|
15
|
+
user?: IdentityUser | null;
|
|
16
|
+
mode?: IdentityMode;
|
|
17
|
+
tone?: IdentityTone;
|
|
18
|
+
signInLabel?: string;
|
|
19
|
+
accountLabel?: string;
|
|
20
|
+
onSignIn?: () => void;
|
|
21
|
+
onSignOut?: () => void;
|
|
22
|
+
menu?: IdentityMenuEntry[];
|
|
23
|
+
};
|
|
24
|
+
declare const IdentityButton: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
25
|
+
type IdentityButton = ReturnType<typeof IdentityButton>;
|
|
26
|
+
export default IdentityButton;
|
|
27
|
+
//# sourceMappingURL=IdentityButton.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"IdentityButton.svelte.d.ts","sourceRoot":"","sources":["../src/lib/IdentityButton.svelte.ts"],"names":[],"mappings":"AAKE,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG,eAAe,CAAC;AAC1D,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;AACtD,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,SAAS,CAAC;AACjD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AACD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB;AAaF,KAAK,gBAAgB,GAAI;IACtB,SAAS,CAAC,EAAE,aAAa,CAAC;IAC1B,IAAI,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;IAC3B,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,IAAI,CAAC,EAAE,iBAAiB,EAAE,CAAC;CAC5B,CAAC;AAwFJ,QAAA,MAAM,cAAc,sDAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
|
|
4
|
+
/** Hiérarchie ENCODÉE DANS LE TYPE : une seule action `primary` est légitime
|
|
5
|
+
* dans une pile. `secondary` = action secondaire ; `ghost` = action discrète.
|
|
6
|
+
* La couleur sémantique (danger) n'est PAS un `kind` — elle vit dans
|
|
7
|
+
* `dangerZone`, rendue à part. Le mauvais chemin (4 « primaires » arc-en-ciel)
|
|
8
|
+
* devient ainsi impossible à exprimer proprement. */
|
|
9
|
+
export type NavActionKind = "primary" | "secondary" | "ghost";
|
|
10
|
+
|
|
11
|
+
export type NavAction = {
|
|
12
|
+
label: string;
|
|
13
|
+
icon?: Snippet;
|
|
14
|
+
onClick?: () => void;
|
|
15
|
+
href?: string;
|
|
16
|
+
kind?: NavActionKind;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Action destructrice, isolée sous un séparateur + un overline « Zone
|
|
21
|
+
* sensible ». Toujours en ton danger, jamais alignée avec les actions
|
|
22
|
+
* normales. Pas de `kind` : c'est une zone, pas une catégorie de couleur. */
|
|
23
|
+
export type NavActionDangerZone = {
|
|
24
|
+
label: string;
|
|
25
|
+
icon?: Snippet;
|
|
26
|
+
onClick?: () => void;
|
|
27
|
+
href?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type NavActionStackOrientation = "vertical" | "horizontal";
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<script lang="ts">
|
|
34
|
+
// NavActionStack — empile des actions en ENCODANT la hiérarchie dans le type.
|
|
35
|
+
// Au plus UN `kind:"primary"` (les suivants sont dégradés en secondary +
|
|
36
|
+
// console.warn). La couleur sémantique « danger » n'est pas détournée en
|
|
37
|
+
// catégorie : la `dangerZone` est rendue séparément, sous un Divider + un
|
|
38
|
+
// overline, en ton danger pleine largeur. Réutilise le Button du DS — aucun
|
|
39
|
+
// bouton n'est réimplémenté. Style token-only scopé, aucun hex en dur.
|
|
40
|
+
import Button from "./Button.svelte";
|
|
41
|
+
import Divider from "./Divider.svelte";
|
|
42
|
+
|
|
43
|
+
type NavActionStackProps = {
|
|
44
|
+
actions?: NavAction[];
|
|
45
|
+
dangerZone?: NavActionDangerZone;
|
|
46
|
+
/** Libellé de l'overline de la zone sensible. Défaut « Zone sensible ». */
|
|
47
|
+
dangerLabel?: string;
|
|
48
|
+
orientation?: NavActionStackOrientation;
|
|
49
|
+
/** Étiquette a11y du groupe d'actions. */
|
|
50
|
+
label?: string;
|
|
51
|
+
class?: string;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
let {
|
|
55
|
+
actions = [],
|
|
56
|
+
dangerZone,
|
|
57
|
+
dangerLabel = "Zone sensible",
|
|
58
|
+
orientation = "vertical",
|
|
59
|
+
label = "Actions",
|
|
60
|
+
class: className
|
|
61
|
+
}: NavActionStackProps = $props();
|
|
62
|
+
|
|
63
|
+
// La règle (un seul primary) appliquée AU RUNTIME en miroir du type : on garde
|
|
64
|
+
// le premier `primary`, on dégrade les suivants en `secondary` et on prévient.
|
|
65
|
+
const normalizedActions = $derived.by(() => {
|
|
66
|
+
let primarySeen = false;
|
|
67
|
+
return actions.map((action) => {
|
|
68
|
+
const kind: NavActionKind = action.kind ?? "secondary";
|
|
69
|
+
if (kind === "primary") {
|
|
70
|
+
if (primarySeen) {
|
|
71
|
+
console.warn(
|
|
72
|
+
`[NavActionStack] Plusieurs actions « primary » fournies — « ${action.label} » dégradée en « secondary ». Une pile n'a qu'une action primaire.`
|
|
73
|
+
);
|
|
74
|
+
return { ...action, kind: "secondary" as NavActionKind };
|
|
75
|
+
}
|
|
76
|
+
primarySeen = true;
|
|
77
|
+
}
|
|
78
|
+
return { ...action, kind };
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// kind → variant Button : primary→primary, secondary→secondary, ghost→ghost.
|
|
83
|
+
const variantFor = (kind: NavActionKind): "primary" | "secondary" | "ghost" =>
|
|
84
|
+
kind;
|
|
85
|
+
|
|
86
|
+
const rootClasses = $derived(
|
|
87
|
+
["st-navActionStack", `st-navActionStack--${orientation}`, className]
|
|
88
|
+
.filter(Boolean)
|
|
89
|
+
.join(" ")
|
|
90
|
+
);
|
|
91
|
+
</script>
|
|
92
|
+
|
|
93
|
+
<div class={rootClasses} role="group" aria-label={label}>
|
|
94
|
+
<div class="st-navActionStack__actions">
|
|
95
|
+
{#each normalizedActions as action (action.label)}
|
|
96
|
+
{#if action.href && !action.disabled}
|
|
97
|
+
<!-- Action-lien : porte les classes Button (réutilisation du style, pas
|
|
98
|
+
de réimplémentation de la logique bouton). -->
|
|
99
|
+
<a
|
|
100
|
+
class="st-button st-button--{variantFor(action.kind)} st-button--md st-navActionStack__item"
|
|
101
|
+
href={action.href}
|
|
102
|
+
onclick={action.onClick}
|
|
103
|
+
>
|
|
104
|
+
{@render action.icon?.()}
|
|
105
|
+
{action.label}
|
|
106
|
+
</a>
|
|
107
|
+
{:else}
|
|
108
|
+
<Button
|
|
109
|
+
variant={variantFor(action.kind)}
|
|
110
|
+
disabled={action.disabled}
|
|
111
|
+
onclick={action.onClick}
|
|
112
|
+
class="st-navActionStack__item"
|
|
113
|
+
>
|
|
114
|
+
{@render action.icon?.()}
|
|
115
|
+
{action.label}
|
|
116
|
+
</Button>
|
|
117
|
+
{/if}
|
|
118
|
+
{/each}
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{#if dangerZone}
|
|
122
|
+
<!-- Zone sensible : SÉPARÉE des actions normales par un Divider, coiffée d'un
|
|
123
|
+
overline token-only, rendue en ton danger pleine largeur. Jamais alignée
|
|
124
|
+
avec la pile au-dessus. -->
|
|
125
|
+
<div class="st-navActionStack__danger" role="group" aria-label={dangerLabel}>
|
|
126
|
+
<Divider />
|
|
127
|
+
<span class="st-navActionStack__dangerLabel">{dangerLabel}</span>
|
|
128
|
+
{#if dangerZone.href}
|
|
129
|
+
<a
|
|
130
|
+
class="st-button st-button--danger st-button--md st-navActionStack__item st-navActionStack__dangerAction"
|
|
131
|
+
href={dangerZone.href}
|
|
132
|
+
onclick={dangerZone.onClick}
|
|
133
|
+
>
|
|
134
|
+
{@render dangerZone.icon?.()}
|
|
135
|
+
{dangerZone.label}
|
|
136
|
+
</a>
|
|
137
|
+
{:else}
|
|
138
|
+
<Button
|
|
139
|
+
variant="danger"
|
|
140
|
+
onclick={dangerZone.onClick}
|
|
141
|
+
class="st-navActionStack__item st-navActionStack__dangerAction"
|
|
142
|
+
>
|
|
143
|
+
{@render dangerZone.icon?.()}
|
|
144
|
+
{dangerZone.label}
|
|
145
|
+
</Button>
|
|
146
|
+
{/if}
|
|
147
|
+
</div>
|
|
148
|
+
{/if}
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<style>
|
|
152
|
+
/* Token-only scopé. Vertical = pile pleine largeur (chaque action prend toute
|
|
153
|
+
la largeur) ; horizontal = rangée (boutons à leur largeur naturelle). */
|
|
154
|
+
.st-navActionStack {
|
|
155
|
+
display: flex;
|
|
156
|
+
flex-direction: column;
|
|
157
|
+
gap: var(--st-spacing-4, 1rem);
|
|
158
|
+
inline-size: 100%;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.st-navActionStack__actions {
|
|
162
|
+
display: flex;
|
|
163
|
+
gap: var(--st-spacing-2, 0.5rem);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.st-navActionStack--vertical .st-navActionStack__actions {
|
|
167
|
+
flex-direction: column;
|
|
168
|
+
align-items: stretch;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.st-navActionStack--horizontal .st-navActionStack__actions {
|
|
172
|
+
flex-direction: row;
|
|
173
|
+
flex-wrap: wrap;
|
|
174
|
+
align-items: center;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* Pleine largeur en pile verticale ; largeur naturelle en rangée. */
|
|
178
|
+
.st-navActionStack--vertical :global(.st-navActionStack__item) {
|
|
179
|
+
inline-size: 100%;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.st-navActionStack__danger {
|
|
183
|
+
display: flex;
|
|
184
|
+
flex-direction: column;
|
|
185
|
+
gap: var(--st-spacing-2, 0.5rem);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.st-navActionStack__dangerLabel {
|
|
189
|
+
color: var(--st-component-overline-color, var(--st-semantic-text-secondary, inherit));
|
|
190
|
+
display: inline-block;
|
|
191
|
+
font-size: var(--st-component-overline-fontSize, 0.6875rem);
|
|
192
|
+
font-weight: var(--st-component-overline-fontWeight, 600);
|
|
193
|
+
letter-spacing: var(--st-component-overline-letterSpacing, 0.04em);
|
|
194
|
+
line-height: var(--st-component-overline-lineHeight, 1.3);
|
|
195
|
+
text-transform: var(--st-component-overline-textTransform, uppercase);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* La zone sensible est pleine largeur quelle que soit l'orientation : c'est une
|
|
199
|
+
zone à part, pas un item de la rangée. */
|
|
200
|
+
.st-navActionStack :global(.st-navActionStack__dangerAction) {
|
|
201
|
+
inline-size: 100%;
|
|
202
|
+
}
|
|
203
|
+
</style>
|