@sentropic/design-system-svelte 0.34.48 → 0.34.50

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,266 @@
1
+ <script lang="ts" module>
2
+ import type { Snippet } from "svelte";
3
+
4
+ /** Profondeur dans l'arbre de nav → échelle typographique DÉCROISSANTE.
5
+ * L0 = racine (base/600), chaque palier descend en taille ET en graisse pour
6
+ * que la hiérarchie se LISE sans indentation seule. */
7
+ export type NavItemDepth = 0 | 1 | 2 | 3;
8
+
9
+ /** Ton sémantique de la rangée. `error` est un VRAI état (un « HTTP 403 »
10
+ * devient rouge sémantique), pas une teinte décorative. */
11
+ export type NavItemStatus = "neutral" | "info" | "success" | "warning" | "error";
12
+
13
+ export type NavItemSwatch = {
14
+ /** Couleur arbitraire (hex/rgb/var) → rendue par ColorSwatch. */
15
+ color?: string;
16
+ /** Ton sémantique → rendu par StatusDot (un point). Ignoré si `color`. */
17
+ tone?: "neutral" | "info" | "success" | "warning" | "error";
18
+ /** Forme de la pastille couleur (ColorSwatch). Défaut « square ». */
19
+ shape?: "square" | "circle" | "pill";
20
+ };
21
+
22
+ export type NavItemProps = {
23
+ /** Clé de sélection, passée telle quelle à SelectableRow (data-value). */
24
+ value?: string;
25
+ /** Libellé principal (1ʳᵉ ligne). */
26
+ title: string;
27
+ /** 2ᵉ ligne MUETTE, ellipsée indépendamment du titre. */
28
+ caption?: string;
29
+ /** Profondeur (défaut 0) → échelle typo + indentation de la tête. */
30
+ depth?: NavItemDepth;
31
+ /** Pastille de tête : couleur arbitraire (ColorSwatch) ou ton (StatusDot). */
32
+ swatch?: NavItemSwatch;
33
+ /** Bulle de compte en queue (Badge circle/sm, tabular-nums). */
34
+ count?: number;
35
+ /** Ton sémantique de la rangée. */
36
+ status?: NavItemStatus;
37
+ /** Sélection (bindable, honorée en standalone ; la liste prime si encadrée). */
38
+ selected?: boolean;
39
+ /** Non-interactif. */
40
+ disabled?: boolean;
41
+ /** Rend la rangée comme un lien (ancre) — anatomie identique. */
42
+ href?: string;
43
+ /** Tête personnalisée (prime sur `swatch`). MUST NOT être interactif en option. */
44
+ leading?: Snippet;
45
+ /** Queue personnalisée (prime sur `count`). MUST NOT être interactif en option. */
46
+ trailing?: Snippet;
47
+ /** Séparateur token-only rendu APRÈS la rangée. */
48
+ divider?: boolean;
49
+ class?: string;
50
+ };
51
+ </script>
52
+
53
+ <script lang="ts">
54
+ // NavItem — l'ANATOMIE DE RANGÉE CANONIQUE du système de navigation (vague 2).
55
+ // C'est la brique que tout rail/drawer instancie : tête (pastille/icône) +
56
+ // titre + caption muette + queue (bulle de compte) + sélection + profondeur
57
+ // typographique + séparateur optionnel.
58
+ //
59
+ // Zéro-entropie : NavItem NE RÉIMPLÉMENTE RIEN. Il COMPOSE SelectableRow (qui
60
+ // possède déjà leading/trailing, la caption à ellipse indépendante, la
61
+ // sélection à 2 signaux, l'accentBar opt-in, le rôle ARIA dérivé du conteneur
62
+ // et la propagation du roving-tabindex) et réutilise les primitives vague 1 :
63
+ // • ColorSwatch → tête quand `swatch.color` (couleur arbitraire inline)
64
+ // • StatusDot → tête quand `swatch.tone` (point sémantique)
65
+ // • Badge → queue (shape="circle" size="sm", tabular-nums) pour `count`
66
+ // Style PROPRE token-only scopé : chaque --st-component-navItem-* retombe sur
67
+ // un littéral de base, AUCUN hex en dur. Un thème qui n'émet rien rend
68
+ // byte-identique.
69
+ //
70
+ // a11y : NavItem N'INTRODUIT AUCUN interactif dans la rangée — pastille, badge
71
+ // et caption sont décoratifs/textuels. Le href passe par l'ancre native de
72
+ // SelectableRow en standalone ; dans une SelectableList, la liste garde son
73
+ // rôle option/roving-tabindex. La rangée reste UN seul tab stop.
74
+ import SelectableRow from "./SelectableRow.svelte";
75
+ import ColorSwatch from "./ColorSwatch.svelte";
76
+ import StatusDot from "./StatusDot.svelte";
77
+ import Badge from "./Badge.svelte";
78
+
79
+ let {
80
+ value,
81
+ title,
82
+ caption,
83
+ depth = 0,
84
+ swatch,
85
+ count,
86
+ status = "neutral",
87
+ selected = $bindable(false),
88
+ disabled = false,
89
+ href,
90
+ leading,
91
+ trailing,
92
+ divider = false,
93
+ class: className
94
+ }: NavItemProps = $props();
95
+
96
+ // Profondeur bornée [0..3] : une valeur hors-borne ne doit pas casser la classe.
97
+ const safeDepth = $derived(
98
+ (Math.min(Math.max(Math.trunc(Number(depth) || 0), 0), 3)) as NavItemDepth
99
+ );
100
+
101
+ // status → tone Badge : Badge ne connaît pas "neutral"+"info"+… exactement de
102
+ // la même façon, mais ses tons couvrent neutral/info/success/warning/error.
103
+ const badgeTone = $derived(status);
104
+
105
+ // Le count alimente un aria-label explicite « N title » (un compte nu est
106
+ // ambigu pour un lecteur d'écran — cf. la doc de Badge).
107
+ const countLabel = $derived(
108
+ count != null ? `${count} ${title}` : undefined
109
+ );
110
+
111
+ // Classes posées sur le WRAPPER NavItem (la rangée elle-même est SelectableRow).
112
+ // Le wrapper porte la profondeur (échelle typo + indentation) et le ton de
113
+ // rangée — ainsi SelectableRow reste inchangé et son rôle ARIA dérivé du
114
+ // conteneur (option/button) est préservé.
115
+ const wrapperClasses = $derived(
116
+ [
117
+ "st-navItem",
118
+ `st-navItem--depth${safeDepth}`,
119
+ status !== "neutral" ? `st-navItem--status-${status}` : null,
120
+ className
121
+ ]
122
+ .filter(Boolean)
123
+ .join(" ")
124
+ );
125
+
126
+ // Indentation de profondeur : un CSS var additif sur le wrapper, à fallback
127
+ // littéral par palier (aucun hex). SelectableRow ne forwarde pas `style` ; on
128
+ // pose donc la var sur le wrapper et la rangée hérite via la cascade.
129
+ const depthStyle = $derived(
130
+ `--st-navItem-indent: var(--st-component-navItem-depth${safeDepth}-indent, ${
131
+ ["0rem", "0.75rem", "1.5rem", "2.25rem"][safeDepth]
132
+ });`
133
+ );
134
+
135
+ </script>
136
+
137
+ <!-- Snippet de caption déclaré HORS de SelectableRow : il n'est passé en prop
138
+ `caption` QUE lorsque `caption` est fournie, pour préserver l'invariant
139
+ byte-identité de SelectableRow (sans caption → rangée single-line). -->
140
+ {#snippet captionSnippet()}
141
+ <span class="st-navItem__caption">{caption}</span>
142
+ {/snippet}
143
+
144
+ <div class={wrapperClasses} style={depthStyle}>
145
+ <SelectableRow
146
+ bind:selected
147
+ {value}
148
+ {href}
149
+ {disabled}
150
+ caption={caption ? captionSnippet : undefined}
151
+ >
152
+ {#snippet leading()}
153
+ {#if leading}
154
+ {@render leading()}
155
+ {:else if swatch}
156
+ <!-- Tête : ColorSwatch pour une couleur arbitraire, sinon StatusDot pour
157
+ un ton sémantique. Décoratif → aria géré par la primitive. -->
158
+ {#if swatch.color}
159
+ <ColorSwatch color={swatch.color} shape={swatch.shape ?? "square"} size={14} />
160
+ {:else}
161
+ <StatusDot tone={swatch.tone ?? "neutral"} size={8} />
162
+ {/if}
163
+ {/if}
164
+ {/snippet}
165
+
166
+ {#snippet children()}
167
+ <span class="st-navItem__title">{title}</span>
168
+ {/snippet}
169
+
170
+ {#snippet trailing()}
171
+ {#if trailing}
172
+ {@render trailing()}
173
+ {:else if count != null}
174
+ <!-- Queue : bulle de compte. Badge shape="circle" size="sm" (tabular-nums),
175
+ ton sémantique aligné sur le `status` de la rangée. aria-label explicite. -->
176
+ <Badge shape="circle" size="sm" tone={badgeTone} aria-label={countLabel}>
177
+ {count}
178
+ </Badge>
179
+ {/if}
180
+ {/snippet}
181
+ </SelectableRow>
182
+
183
+ {#if divider}
184
+ <!-- Séparateur token-only APRÈS la rangée (anatomie de liste de nav). -->
185
+ <hr class="st-navItem__divider" aria-hidden="true" />
186
+ {/if}
187
+ </div>
188
+
189
+ <style>
190
+ .st-navItem {
191
+ display: block;
192
+ inline-size: 100%;
193
+ }
194
+
195
+ /* Indentation de PROFONDEUR : décale la rangée SelectableRow (sa tête comprise)
196
+ par un padding inline-start ADDITIF à son padding de base (0.75rem). On cible
197
+ la rangée via :global car c'est le DOM de SelectableRow (composé, pas inline).
198
+ La var --st-navItem-indent est posée sur le wrapper et héritée par cascade. */
199
+ .st-navItem :global(.st-selectableRow) {
200
+ padding-inline-start: calc(0.75rem + var(--st-navItem-indent, 0rem));
201
+ }
202
+
203
+ /* Échelle typographique : la graisse ET la taille DÉCROISSENT avec la
204
+ profondeur, pour que la hiérarchie se lise sans relief de couleur. Le wrapper
205
+ et le titre sont tous deux NavItem-authored (scope identique) → pas de
206
+ :global nécessaire. Chaque token retombe sur un littéral → thème silencieux =
207
+ byte-identique. */
208
+ .st-navItem--depth0 .st-navItem__title {
209
+ font-size: var(--st-component-navItem-depth0-fontSize, 0.875rem);
210
+ font-weight: var(--st-component-navItem-depth0-fontWeight, 600);
211
+ }
212
+ .st-navItem--depth1 .st-navItem__title {
213
+ font-size: var(--st-component-navItem-depth1-fontSize, 0.8125rem);
214
+ font-weight: var(--st-component-navItem-depth1-fontWeight, 500);
215
+ }
216
+ .st-navItem--depth2 .st-navItem__title {
217
+ font-size: var(--st-component-navItem-depth2-fontSize, 0.8125rem);
218
+ font-weight: var(--st-component-navItem-depth2-fontWeight, 400);
219
+ }
220
+ /* L3 — muted : même métrique que L2, couleur atténuée. */
221
+ .st-navItem--depth3 .st-navItem__title {
222
+ font-size: var(--st-component-navItem-depth3-fontSize, 0.8125rem);
223
+ font-weight: var(--st-component-navItem-depth3-fontWeight, 400);
224
+ color: var(--st-component-navItem-depth3-color, var(--st-semantic-text-muted));
225
+ }
226
+
227
+ .st-navItem__title {
228
+ display: block;
229
+ min-width: 0;
230
+ overflow: hidden;
231
+ text-overflow: ellipsis;
232
+ white-space: nowrap;
233
+ }
234
+
235
+ .st-navItem__caption {
236
+ display: block;
237
+ min-width: 0;
238
+ overflow: hidden;
239
+ text-overflow: ellipsis;
240
+ white-space: nowrap;
241
+ }
242
+
243
+ /* Ton sémantique de la RANGÉE : teinte le titre par le feedback correspondant.
244
+ Un « HTTP 403 » error devient rouge sémantique réel. Chaque ton retombe sur
245
+ son token --st-semantic-feedback-*. */
246
+ .st-navItem--status-info .st-navItem__title {
247
+ color: var(--st-component-navItem-status-info, var(--st-semantic-feedback-info));
248
+ }
249
+ .st-navItem--status-success .st-navItem__title {
250
+ color: var(--st-component-navItem-status-success, var(--st-semantic-feedback-success));
251
+ }
252
+ .st-navItem--status-warning .st-navItem__title {
253
+ color: var(--st-component-navItem-status-warning, var(--st-semantic-feedback-warning));
254
+ }
255
+ .st-navItem--status-error .st-navItem__title {
256
+ color: var(--st-component-navItem-status-error, var(--st-semantic-feedback-error));
257
+ }
258
+
259
+ /* Séparateur token-only : une fine règle pleine largeur après la rangée. */
260
+ .st-navItem__divider {
261
+ border: 0;
262
+ border-top: var(--st-border-width-thin, 1px) solid
263
+ var(--st-component-navItem-dividerColor, var(--st-semantic-border-subtle, rgba(0, 0, 0, 0.08)));
264
+ margin-block: var(--st-component-navItem-dividerGap, 0.375rem);
265
+ }
266
+ </style>
@@ -0,0 +1,49 @@
1
+ import type { Snippet } from "svelte";
2
+ /** Profondeur dans l'arbre de nav → échelle typographique DÉCROISSANTE.
3
+ * L0 = racine (base/600), chaque palier descend en taille ET en graisse pour
4
+ * que la hiérarchie se LISE sans indentation seule. */
5
+ export type NavItemDepth = 0 | 1 | 2 | 3;
6
+ /** Ton sémantique de la rangée. `error` est un VRAI état (un « HTTP 403 »
7
+ * devient rouge sémantique), pas une teinte décorative. */
8
+ export type NavItemStatus = "neutral" | "info" | "success" | "warning" | "error";
9
+ export type NavItemSwatch = {
10
+ /** Couleur arbitraire (hex/rgb/var) → rendue par ColorSwatch. */
11
+ color?: string;
12
+ /** Ton sémantique → rendu par StatusDot (un point). Ignoré si `color`. */
13
+ tone?: "neutral" | "info" | "success" | "warning" | "error";
14
+ /** Forme de la pastille couleur (ColorSwatch). Défaut « square ». */
15
+ shape?: "square" | "circle" | "pill";
16
+ };
17
+ export type NavItemProps = {
18
+ /** Clé de sélection, passée telle quelle à SelectableRow (data-value). */
19
+ value?: string;
20
+ /** Libellé principal (1ʳᵉ ligne). */
21
+ title: string;
22
+ /** 2ᵉ ligne MUETTE, ellipsée indépendamment du titre. */
23
+ caption?: string;
24
+ /** Profondeur (défaut 0) → échelle typo + indentation de la tête. */
25
+ depth?: NavItemDepth;
26
+ /** Pastille de tête : couleur arbitraire (ColorSwatch) ou ton (StatusDot). */
27
+ swatch?: NavItemSwatch;
28
+ /** Bulle de compte en queue (Badge circle/sm, tabular-nums). */
29
+ count?: number;
30
+ /** Ton sémantique de la rangée. */
31
+ status?: NavItemStatus;
32
+ /** Sélection (bindable, honorée en standalone ; la liste prime si encadrée). */
33
+ selected?: boolean;
34
+ /** Non-interactif. */
35
+ disabled?: boolean;
36
+ /** Rend la rangée comme un lien (ancre) — anatomie identique. */
37
+ href?: string;
38
+ /** Tête personnalisée (prime sur `swatch`). MUST NOT être interactif en option. */
39
+ leading?: Snippet;
40
+ /** Queue personnalisée (prime sur `count`). MUST NOT être interactif en option. */
41
+ trailing?: Snippet;
42
+ /** Séparateur token-only rendu APRÈS la rangée. */
43
+ divider?: boolean;
44
+ class?: string;
45
+ };
46
+ declare const NavItem: import("svelte").Component<NavItemProps, {}, "selected">;
47
+ type NavItem = ReturnType<typeof NavItem>;
48
+ export default NavItem;
49
+ //# sourceMappingURL=NavItem.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NavItem.svelte.d.ts","sourceRoot":"","sources":["../src/lib/NavItem.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC;;uDAEuD;AACvD,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAEzC;2DAC2D;AAC3D,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;AAEjF,MAAM,MAAM,aAAa,GAAG;IAC1B,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,IAAI,CAAC,EAAE,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;IAC5D,qEAAqE;IACrE,KAAK,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAC;CACtC,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,yDAAyD;IACzD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,8EAA8E;IAC9E,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mCAAmC;IACnC,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,gFAAgF;IAChF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sBAAsB;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,iEAAiE;IACjE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mFAAmF;IACnF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,mFAAmF;IACnF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,mDAAmD;IACnD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAgKJ,QAAA,MAAM,OAAO,0DAAwC,CAAC;AACtD,KAAK,OAAO,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC;AAC1C,eAAe,OAAO,CAAC"}
@@ -0,0 +1,147 @@
1
+ <script lang="ts" module>
2
+ import type { Snippet } from "svelte";
3
+
4
+ export interface NavRailItem {
5
+ id: string;
6
+ label: string;
7
+ href?: string;
8
+ active?: boolean;
9
+ disabled?: boolean;
10
+ badge?: string | number;
11
+ icon?: Snippet;
12
+ }
13
+
14
+ export interface NavRailProps {
15
+ items?: NavRailItem[];
16
+ label?: string;
17
+ activeItemId?: string;
18
+ onItemSelect?: (id: string) => void;
19
+ footer?: Snippet;
20
+ children?: Snippet;
21
+ class?: string;
22
+ }
23
+ </script>
24
+
25
+ <script lang="ts">
26
+ let {
27
+ items = [],
28
+ label = "Primary navigation",
29
+ activeItemId,
30
+ onItemSelect,
31
+ footer,
32
+ children,
33
+ class: className
34
+ }: NavRailProps = $props();
35
+
36
+ const classes = $derived(["st-navRail", className].filter(Boolean).join(" "));
37
+
38
+ function isActive(item: NavRailItem) {
39
+ return item.active === true || item.id === activeItemId;
40
+ }
41
+
42
+ function selectItem(item: NavRailItem) {
43
+ if (!item.disabled) onItemSelect?.(item.id);
44
+ }
45
+ </script>
46
+
47
+ <nav class={classes} aria-label={label}>
48
+ <div class="st-navRail__items">
49
+ {#each items as item (item.id)}
50
+ {#if item.href && !item.disabled}
51
+ <a class="st-navRail__item" class:st-navRail__item--active={isActive(item)} href={item.href} aria-current={isActive(item) ? "page" : undefined} title={item.label} onclick={() => selectItem(item)}>
52
+ {#if item.icon}<span class="st-navRail__icon">{@render item.icon()}</span>{/if}
53
+ <span class="st-navRail__label">{item.label}</span>
54
+ {#if item.badge != null}<span class="st-navRail__badge">{item.badge}</span>{/if}
55
+ </a>
56
+ {:else}
57
+ <button class="st-navRail__item" class:st-navRail__item--active={isActive(item)} type="button" disabled={item.disabled} aria-current={isActive(item) ? "page" : undefined} title={item.label} onclick={() => selectItem(item)}>
58
+ {#if item.icon}<span class="st-navRail__icon">{@render item.icon()}</span>{/if}
59
+ <span class="st-navRail__label">{item.label}</span>
60
+ {#if item.badge != null}<span class="st-navRail__badge">{item.badge}</span>{/if}
61
+ </button>
62
+ {/if}
63
+ {/each}
64
+ {@render children?.()}
65
+ </div>
66
+ {#if footer}
67
+ <footer class="st-navRail__footer">{@render footer()}</footer>
68
+ {/if}
69
+ </nav>
70
+
71
+ <style>
72
+ .st-navRail {
73
+ align-items: stretch;
74
+ background: var(--st-component-navRail-surface, var(--st-semantic-surface-raised));
75
+ color: var(--st-semantic-text-primary);
76
+ display: grid;
77
+ grid-template-rows: 1fr auto;
78
+ block-size: 100%;
79
+ inline-size: var(--st-component-navRail-width, 4.5rem);
80
+ padding: var(--st-spacing-2, 0.5rem);
81
+ }
82
+
83
+ .st-navRail__items,
84
+ .st-navRail__footer {
85
+ align-items: center;
86
+ display: flex;
87
+ flex-direction: column;
88
+ gap: var(--st-spacing-2, 0.5rem);
89
+ }
90
+
91
+ .st-navRail__items {
92
+ min-block-size: 0;
93
+ overflow-y: auto;
94
+ }
95
+
96
+ .st-navRail__item {
97
+ align-items: center;
98
+ background: transparent;
99
+ border: 1px solid transparent;
100
+ border-radius: var(--st-radius-md, 0.375rem);
101
+ color: var(--st-semantic-text-secondary);
102
+ cursor: pointer;
103
+ display: inline-flex;
104
+ flex-direction: column;
105
+ font: inherit;
106
+ gap: var(--st-spacing-1, 0.25rem);
107
+ inline-size: 100%;
108
+ min-block-size: 3.25rem;
109
+ padding: var(--st-spacing-2, 0.5rem);
110
+ position: relative;
111
+ text-align: center;
112
+ text-decoration: none;
113
+ }
114
+
115
+ .st-navRail__item:hover,
116
+ .st-navRail__item--active {
117
+ background: var(--st-component-navRail-activeBackground, var(--st-semantic-surface-subtle));
118
+ color: var(--st-semantic-text-primary);
119
+ }
120
+
121
+ .st-navRail__item--active {
122
+ border-color: var(--st-component-navRail-activeBorder, var(--st-semantic-border-strong));
123
+ }
124
+
125
+ .st-navRail__item:disabled {
126
+ cursor: not-allowed;
127
+ opacity: 0.5;
128
+ }
129
+
130
+ .st-navRail__label {
131
+ font-size: 0.6875rem;
132
+ font-weight: 650;
133
+ line-height: 1.1;
134
+ }
135
+
136
+ .st-navRail__badge {
137
+ background: var(--st-component-navRail-badgeBackground, var(--st-semantic-surface-inverse));
138
+ border-radius: 999px;
139
+ color: var(--st-semantic-text-inverse);
140
+ font-size: 0.625rem;
141
+ line-height: 1;
142
+ padding: 0.125rem 0.3rem;
143
+ position: absolute;
144
+ right: 0.25rem;
145
+ top: 0.25rem;
146
+ }
147
+ </style>
@@ -0,0 +1,23 @@
1
+ import type { Snippet } from "svelte";
2
+ export interface NavRailItem {
3
+ id: string;
4
+ label: string;
5
+ href?: string;
6
+ active?: boolean;
7
+ disabled?: boolean;
8
+ badge?: string | number;
9
+ icon?: Snippet;
10
+ }
11
+ export interface NavRailProps {
12
+ items?: NavRailItem[];
13
+ label?: string;
14
+ activeItemId?: string;
15
+ onItemSelect?: (id: string) => void;
16
+ footer?: Snippet;
17
+ children?: Snippet;
18
+ class?: string;
19
+ }
20
+ declare const NavRail: import("svelte").Component<NavRailProps, {}, "">;
21
+ type NavRail = ReturnType<typeof NavRail>;
22
+ export default NavRail;
23
+ //# sourceMappingURL=NavRail.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NavRail.svelte.d.ts","sourceRoot":"","sources":["../src/lib/NavRail.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,EAAE,WAAW,EAAE,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAsDH,QAAA,MAAM,OAAO,kDAAwC,CAAC;AACtD,KAAK,OAAO,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC;AAC1C,eAAe,OAAO,CAAC"}
@@ -0,0 +1,152 @@
1
+ <script lang="ts" module>
2
+ /** Niveau de titre porté par l'Overline d'en-tête de section. `h2`/`h3` quand la
3
+ * section est une vraie région titrée d'un rail/drawer ; choisis selon la
4
+ * profondeur du sommaire. */
5
+ export type NavSectionHeadingLevel = "h2" | "h3";
6
+ </script>
7
+
8
+ <script lang="ts">
9
+ // NavSection — EN-TÊTE DE GROUPE d'un rail / drawer (« COMMUNITIES »,
10
+ // « SIGNAUX », « Pastilles », « Zonage / Potentiel », « DOCUMENTATION »).
11
+ // Donne la hiérarchie typographique qui manque à une liste plate + un foyer
12
+ // normé pour l'action locale (au lieu d'un bouton primaire planté dans la
13
+ // liste). ZÉRO-ENTROPIE : on RÉUTILISE les primitives déjà livrées —
14
+ // • Overline → le libellé small-caps muet (rendu en h2/h3 pour la
15
+ // sémantique de titre quand la section n'est PAS repliable) ;
16
+ // • Badge shape="circle" → le compteur de section en queue d'en-tête ;
17
+ // • Collapsible → le disclosure (trigger aria-expanded + région
18
+ // aria-labelledby) quand `collapsible`.
19
+ // On ne réimplémente NI disclosure NI libellé. Style token-only scopé, aucun hex.
20
+ import type { Snippet } from "svelte";
21
+ import Badge from "./Badge.svelte";
22
+ import Collapsible from "./Collapsible.svelte";
23
+ import Overline from "./Overline.svelte";
24
+
25
+ type NavSectionProps = {
26
+ /** Libellé de la section, rendu via Overline (small-caps muet). */
27
+ label: string;
28
+ /** Compteur optionnel → Badge circle en queue de l'en-tête. */
29
+ count?: number;
30
+ /** Si true, l'en-tête devient le trigger d'un Collapsible (aria-expanded)
31
+ * qui montre/cache le contenu. Sinon : groupe titré statique. */
32
+ collapsible?: boolean;
33
+ /** État d'ouverture quand `collapsible` (bindable). */
34
+ open?: boolean;
35
+ /** Niveau de titre de l'Overline quand la section n'est pas repliable. */
36
+ as?: NavSectionHeadingLevel;
37
+ /** Emplacement normé d'une action de section (ex. Button « + Ajouter »),
38
+ * aligné à droite de l'en-tête. NON rendu quand `collapsible` (le trigger
39
+ * occupe l'en-tête en entier — place l'action dans le contenu). */
40
+ action?: Snippet;
41
+ /** Contenu de la section (NavItem / NavList) — rendu dans la région. */
42
+ children: Snippet;
43
+ class?: string;
44
+ };
45
+
46
+ let {
47
+ label,
48
+ count,
49
+ collapsible = false,
50
+ open = $bindable(true),
51
+ as = "h3",
52
+ action,
53
+ children,
54
+ class: className
55
+ }: NavSectionProps = $props();
56
+
57
+ const uid = `st-navSection-${Math.random().toString(36).slice(2, 9)}`;
58
+
59
+ const rootClasses = $derived(
60
+ [
61
+ "st-navSection",
62
+ collapsible ? "st-navSection--collapsible" : "st-navSection--static",
63
+ className
64
+ ]
65
+ .filter(Boolean)
66
+ .join(" ")
67
+ );
68
+
69
+ const hasCount = $derived(typeof count === "number");
70
+ </script>
71
+
72
+ {#if collapsible}
73
+ <!-- Repliable : l'en-tête EST le trigger du Collapsible. Le compteur passe par
74
+ son slot `trailing` (entre le titre et le chevron) ; on annonce le tout aux
75
+ lecteurs d'écran via aria-label. L'action de section n'a pas sa place dans
76
+ un trigger (un bouton dans un bouton) : la documenter dans le contenu. -->
77
+ <Collapsible
78
+ bind:open
79
+ title={label}
80
+ aria-label={hasCount ? `${label}, ${count}` : label}
81
+ class={rootClasses}
82
+ >
83
+ {#snippet trailing()}
84
+ {#if hasCount}
85
+ <Badge shape="circle" size="sm" aria-label={`${count} éléments`}>{count}</Badge>
86
+ {/if}
87
+ {/snippet}
88
+ {@render children()}
89
+ </Collapsible>
90
+ {:else}
91
+ <!-- Statique : groupe titré par l'Overline (h2/h3). Le contenu est relié au
92
+ titre via aria-labelledby — la liste devient une vraie région titrée. -->
93
+ <section {...{ class: rootClasses }} role="group" aria-labelledby={`${uid}-label`}>
94
+ <div class="st-navSection__header">
95
+ <Overline {as} id={`${uid}-label`} class="st-navSection__label">
96
+ {label}
97
+ {#if hasCount}
98
+ <Badge
99
+ shape="circle"
100
+ size="sm"
101
+ class="st-navSection__count"
102
+ aria-label={`${count} éléments`}>{count}</Badge>
103
+ {/if}
104
+ </Overline>
105
+ {#if action}
106
+ <span class="st-navSection__action">{@render action()}</span>
107
+ {/if}
108
+ </div>
109
+ <div class="st-navSection__body">
110
+ {@render children()}
111
+ </div>
112
+ </section>
113
+ {/if}
114
+
115
+ <style>
116
+ /* Token-only scopé. L'en-tête statique aligne le libellé (small-caps via
117
+ Overline), le compteur en queue, et pousse l'action de section à droite. */
118
+ .st-navSection {
119
+ inline-size: 100%;
120
+ }
121
+
122
+ .st-navSection__header {
123
+ align-items: baseline;
124
+ display: flex;
125
+ gap: var(--st-spacing-2, 0.5rem);
126
+ justify-content: space-between;
127
+ padding-block: var(--st-component-navSection-headerPaddingBlock, 0.375rem);
128
+ }
129
+
130
+ /* Le libellé et son compteur forment un bloc ; le Badge circle s'aligne au
131
+ centre vertical du texte small-caps. */
132
+ .st-navSection :global(.st-navSection__label) {
133
+ align-items: center;
134
+ display: inline-flex;
135
+ gap: var(--st-spacing-2, 0.5rem);
136
+ }
137
+
138
+ .st-navSection :global(.st-navSection__count) {
139
+ flex: 0 0 auto;
140
+ }
141
+
142
+ /* Foyer de l'action de section : ancré à droite, ne grandit pas. */
143
+ .st-navSection__action {
144
+ align-items: center;
145
+ display: inline-flex;
146
+ flex: 0 0 auto;
147
+ }
148
+
149
+ .st-navSection__body {
150
+ display: block;
151
+ }
152
+ </style>
@@ -0,0 +1,29 @@
1
+ /** Niveau de titre porté par l'Overline d'en-tête de section. `h2`/`h3` quand la
2
+ * section est une vraie région titrée d'un rail/drawer ; choisis selon la
3
+ * profondeur du sommaire. */
4
+ export type NavSectionHeadingLevel = "h2" | "h3";
5
+ import type { Snippet } from "svelte";
6
+ type NavSectionProps = {
7
+ /** Libellé de la section, rendu via Overline (small-caps muet). */
8
+ label: string;
9
+ /** Compteur optionnel → Badge circle en queue de l'en-tête. */
10
+ count?: number;
11
+ /** Si true, l'en-tête devient le trigger d'un Collapsible (aria-expanded)
12
+ * qui montre/cache le contenu. Sinon : groupe titré statique. */
13
+ collapsible?: boolean;
14
+ /** État d'ouverture quand `collapsible` (bindable). */
15
+ open?: boolean;
16
+ /** Niveau de titre de l'Overline quand la section n'est pas repliable. */
17
+ as?: NavSectionHeadingLevel;
18
+ /** Emplacement normé d'une action de section (ex. Button « + Ajouter »),
19
+ * aligné à droite de l'en-tête. NON rendu quand `collapsible` (le trigger
20
+ * occupe l'en-tête en entier — place l'action dans le contenu). */
21
+ action?: Snippet;
22
+ /** Contenu de la section (NavItem / NavList) — rendu dans la région. */
23
+ children: Snippet;
24
+ class?: string;
25
+ };
26
+ declare const NavSection: import("svelte").Component<NavSectionProps, {}, "open">;
27
+ type NavSection = ReturnType<typeof NavSection>;
28
+ export default NavSection;
29
+ //# sourceMappingURL=NavSection.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NavSection.svelte.d.ts","sourceRoot":"","sources":["../src/lib/NavSection.svelte.ts"],"names":[],"mappings":"AAGE;;6BAE6B;AAC7B,MAAM,MAAM,sBAAsB,GAAG,IAAI,GAAG,IAAI,CAAC;AAcnD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAMpC,KAAK,eAAe,GAAG;IACrB,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAC;IACd,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;qEACiE;IACjE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,uDAAuD;IACvD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,0EAA0E;IAC1E,EAAE,CAAC,EAAE,sBAAsB,CAAC;IAC5B;;uEAEmE;IACnE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,wEAAwE;IACxE,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAiFJ,QAAA,MAAM,UAAU,yDAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}