@sentropic/design-system-svelte 0.34.67 → 0.34.68

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.
@@ -7,6 +7,16 @@
7
7
  href: string;
8
8
  /** Marqué actif (souligné, aria-current=page). */
9
9
  active?: boolean;
10
+ /** Roles autorisés à voir l'item. Vide/undefined = visible par tous. */
11
+ roles?: string[];
12
+ /** Alias pratique pour un seul rôle autorisé. */
13
+ role?: string;
14
+ /** Coupe la navigation tout en gardant l'item visible/annoncé. */
15
+ disabled?: boolean;
16
+ ariaLabel?: string;
17
+ target?: string;
18
+ rel?: string;
19
+ onClick?: (event: MouseEvent) => void;
10
20
  }
11
21
 
12
22
  /** Une option du sélecteur de thème. */
@@ -38,6 +48,8 @@
38
48
  nav?: AppChromeNavItem[];
39
49
  /** aria-label de la nav principale. */
40
50
  navLabel?: string;
51
+ /** Roles de l'utilisateur courant pour filtrer les nav items gatés par rôle. */
52
+ userRoles?: string[];
41
53
 
42
54
  // ── Contrôle thème (contrôlé) ─────────────────────────────────────────────
43
55
  /** Options de thème. Vide => le sélecteur est masqué. */
@@ -118,6 +130,7 @@
118
130
  brandLabel,
119
131
  nav = [],
120
132
  navLabel = "Primary",
133
+ userRoles = [],
121
134
  themes = [],
122
135
  theme,
123
136
  onThemeChange,
@@ -172,6 +185,23 @@
172
185
  }
173
186
 
174
187
  const classes = () => ["st-appChrome", className].filter(Boolean).join(" ");
188
+ const visibleNav = $derived(
189
+ nav.filter((item) => {
190
+ const allowed = item.roles ?? (item.role ? [item.role] : []);
191
+ return allowed.length === 0 || allowed.some((role) => userRoles.includes(role));
192
+ }),
193
+ );
194
+
195
+ function navRel(item: AppChromeNavItem): string | undefined {
196
+ if (item.disabled) return undefined;
197
+ return item.rel ?? (item.target === "_blank" ? "noreferrer" : undefined);
198
+ }
199
+
200
+ function handleNavClick(event: MouseEvent, item: AppChromeNavItem, closeDrawer = false) {
201
+ if (item.disabled) event.preventDefault();
202
+ item.onClick?.(event);
203
+ if (!item.disabled && closeDrawer) onMobileMenuToggle?.();
204
+ }
175
205
 
176
206
  function closeMenus(target: EventTarget | null) {
177
207
  const el = target as Element | null;
@@ -207,11 +237,17 @@
207
237
 
208
238
  <!-- ── Nav principale ─────────────────────────────────────────────────────── -->
209
239
  {#snippet navContent()}
210
- {#each nav as item (item.href)}
240
+ {#each visibleNav as item (item.href)}
211
241
  <a
212
242
  class="st-appChrome__navLink st-appHeader__navLink"
213
- href={item.href}
243
+ class:st-appChrome__navLink--disabled={item.disabled}
244
+ href={item.disabled ? undefined : item.href}
214
245
  aria-current={item.active ? "page" : undefined}
246
+ aria-disabled={item.disabled ? "true" : undefined}
247
+ aria-label={item.ariaLabel}
248
+ target={item.disabled ? undefined : item.target}
249
+ rel={navRel(item)}
250
+ onclick={(event) => handleNavClick(event, item)}
215
251
  >
216
252
  {item.label}
217
253
  </a>
@@ -357,12 +393,17 @@
357
393
  {#if mobileMenuOpen}
358
394
  <nav id={drawerId} class="st-appChrome__drawer" aria-label={navLabel}>
359
395
  <div class="st-appChrome__drawerSection">
360
- {#each nav as item (item.href)}
396
+ {#each visibleNav as item (item.href)}
361
397
  <a
362
398
  class="st-appChrome__drawerLink"
363
- href={item.href}
399
+ class:st-appChrome__navLink--disabled={item.disabled}
400
+ href={item.disabled ? undefined : item.href}
364
401
  aria-current={item.active ? "page" : undefined}
365
- onclick={() => onMobileMenuToggle?.()}
402
+ aria-disabled={item.disabled ? "true" : undefined}
403
+ aria-label={item.ariaLabel}
404
+ target={item.disabled ? undefined : item.target}
405
+ rel={navRel(item)}
406
+ onclick={(event) => handleNavClick(event, item, true)}
366
407
  >
367
408
  {item.label}
368
409
  </a>
@@ -5,6 +5,16 @@ export interface AppChromeNavItem {
5
5
  href: string;
6
6
  /** Marqué actif (souligné, aria-current=page). */
7
7
  active?: boolean;
8
+ /** Roles autorisés à voir l'item. Vide/undefined = visible par tous. */
9
+ roles?: string[];
10
+ /** Alias pratique pour un seul rôle autorisé. */
11
+ role?: string;
12
+ /** Coupe la navigation tout en gardant l'item visible/annoncé. */
13
+ disabled?: boolean;
14
+ ariaLabel?: string;
15
+ target?: string;
16
+ rel?: string;
17
+ onClick?: (event: MouseEvent) => void;
8
18
  }
9
19
  /** Une option du sélecteur de thème. */
10
20
  export interface AppChromeThemeOption {
@@ -30,6 +40,8 @@ export interface AppChromeProps {
30
40
  nav?: AppChromeNavItem[];
31
41
  /** aria-label de la nav principale. */
32
42
  navLabel?: string;
43
+ /** Roles de l'utilisateur courant pour filtrer les nav items gatés par rôle. */
44
+ userRoles?: string[];
33
45
  /** Options de thème. Vide => le sélecteur est masqué. */
34
46
  themes?: AppChromeThemeOption[];
35
47
  /** Id du thème actif. */
@@ -1 +1 @@
1
- {"version":3,"file":"AppChrome.svelte.d.ts","sourceRoot":"","sources":["../src/lib/AppChrome.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,iDAAiD;AACjD,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,wCAAwC;AACxC,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;AAC3D,MAAM,MAAM,eAAe,GAAG,IAAI,GAAG,IAAI,CAAC;AAE1C,MAAM,WAAW,cAAc;IAE7B,4CAA4C;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2EAA2E;IAC3E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,MAAM,CAAC;IAGpB,+DAA+D;IAC/D,GAAG,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACzB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAGlB,yDAAyD;IACzD,MAAM,CAAC,EAAE,oBAAoB,EAAE,CAAC;IAChC,yBAAyB;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,aAAa,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,wCAAwC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;IAGpB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,kBAAkB,CAAC;IAC/B,4DAA4D;IAC5D,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACvD,0DAA0D;IAC1D,eAAe,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAGhE,2DAA2D;IAC3D,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,wCAAwC;IACxC,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,CAAC;IACnD,yCAAyC;IACzC,WAAW,CAAC,EAAE,MAAM,CAAC;IAGrB,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iCAAiC;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC;IAGrB,kEAAkE;IAClE,QAAQ,CAAC,EAAE,OAAO,CAAC;IAGnB,sDAAsD;IACtD,cAAc,CAAC,EAAE,OAAO,CAAC;IAGzB,+CAA+C;IAC/C,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,4CAA4C;IAC5C,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;IAChC,mCAAmC;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAuRH,QAAA,MAAM,SAAS,oDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
1
+ {"version":3,"file":"AppChrome.svelte.d.ts","sourceRoot":"","sources":["../src/lib/AppChrome.svelte.ts"],"names":[],"mappings":"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"}
@@ -43,6 +43,15 @@ describe("AppChrome — navigation", () => {
43
43
  expect(links[1].getAttribute("aria-current")).toBeNull();
44
44
  expect(links[0].classList.contains("st-appHeader__navLink")).toBe(true);
45
45
  });
46
+ it("filters role-gated items and renders disabled nav without href", () => {
47
+ const { container } = render(AppChrome, {
48
+ props: { nav: [...nav, { label: "Admin", href: "/admin", role: "admin" }, { label: "Soon", href: "/soon", disabled: true }], userRoles: ["member"] },
49
+ });
50
+ const links = Array.from(container.querySelectorAll(".st-appChrome__navLink"));
51
+ expect(links.map((a) => a.textContent?.trim())).toEqual(["Vues", "Données", "Réglages", "Soon"]);
52
+ expect(links.at(-1)?.hasAttribute("href")).toBe(false);
53
+ expect(links.at(-1)?.getAttribute("aria-disabled")).toBe("true");
54
+ });
46
55
  });
47
56
  describe("AppChrome — contrôle thème", () => {
48
57
  it("shows the active theme label and fires onThemeChange on selection", async () => {
@@ -388,6 +388,15 @@
388
388
  font-weight: 650;
389
389
  }
390
390
 
391
+ :global(.st-appChrome__navLink--disabled),
392
+ :global(.st-appChrome__navLink--disabled:hover),
393
+ :global(.st-appChrome__navLink--disabled:focus-visible) {
394
+ color: var(--st-semantic-text-disabled, var(--st-semantic-text-secondary));
395
+ cursor: not-allowed;
396
+ opacity: 0.56;
397
+ pointer-events: none;
398
+ }
399
+
391
400
  /* --- Contrôle utilitaire canonique (pill : thème / langue / icône) --- */
392
401
  :global(.st-appHeader__control) {
393
402
  align-items: center;
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { ChevronDown } from "@lucide/svelte";
3
3
  import type { HTMLAttributes } from "svelte/elements";
4
+ import Portal from "./Portal.svelte";
4
5
 
5
6
  export type DropdownOption = {
6
7
  label: string;
@@ -40,6 +41,7 @@
40
41
  let currentValue = $state<string | undefined>(undefined);
41
42
  let syncedOpen = $state<boolean | undefined>(undefined);
42
43
  let syncedValue = $state<string | undefined>(undefined);
44
+ let listEl: HTMLDivElement | undefined = $state();
43
45
  let listPos = $state({ top: 0, left: 0, width: 0 });
44
46
 
45
47
  const classes = () => ["st-dropdown", className].filter(Boolean).join(" ");
@@ -72,7 +74,7 @@
72
74
  function onWindowPointerDown(event: MouseEvent) {
73
75
  if (!expanded) return;
74
76
  const target = event.target as Node | null;
75
- if (host && target && !host.contains(target)) close();
77
+ if (target && !host?.contains(target) && !listEl?.contains(target)) close();
76
78
  }
77
79
 
78
80
  function onWindowScroll() {
@@ -122,7 +124,9 @@
122
124
  />
123
125
  </button>
124
126
  {#if expanded}
127
+ <Portal>
125
128
  <div
129
+ bind:this={listEl}
126
130
  class="st-dropdown__list"
127
131
  role="listbox"
128
132
  aria-label={label}
@@ -142,6 +146,7 @@
142
146
  </button>
143
147
  {/each}
144
148
  </div>
149
+ </Portal>
145
150
  {/if}
146
151
  </div>
147
152
 
@@ -1 +1 @@
1
- {"version":3,"file":"Dropdown.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Dropdown.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGpD,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,KAAK,aAAa,GAAG,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC,GAAG;IAChF,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC,CAAC;AAgHJ,QAAA,MAAM,QAAQ,mDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
1
+ {"version":3,"file":"Dropdown.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Dropdown.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAIpD,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,KAAK,aAAa,GAAG,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC,GAAG;IAChF,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC,CAAC;AAoHJ,QAAA,MAAM,QAAQ,mDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentropic/design-system-svelte",
3
- "version": "0.34.67",
3
+ "version": "0.34.68",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
package/dist/.srchash DELETED
@@ -1 +0,0 @@
1
- 5fa4af626e8a967b1e34a8ab542a01e4b9b3051441b1a26defa3e5b10ca0e657