@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.
@@ -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"}
@@ -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 { label, helperText, invalid = false, class: className, ...rest }: CheckboxProps = $props();
12
- const classes = () => ["st-choice", "st-choice--checkbox", className].filter(Boolean).join(" ");
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 {...rest} class="st-choice__input" type="checkbox" aria-invalid={invalid ? "true" : undefined} />
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;AAqBJ,QAAA,MAAM,QAAQ,mDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,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>