svelte-fluentui 1.3.0 → 1.3.1

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.
@@ -1,369 +1,390 @@
1
- <script lang="ts">
2
- import type {SlotType} from "../../types/index.js"
3
- import Button from "../Button.svelte"
4
- import Panel from "./Panel.svelte"
5
- import NavMenu from "../nav/NavMenu.svelte"
6
- import NavGroup from "../nav/NavGroup.svelte"
7
- import NavLinkItem from "../nav/NavLinkItem.svelte"
8
- import DismissIcon from "../icons/DismissIcon.svelte"
9
-
10
- type NavItem = {
11
- label: string
12
- href: string
13
- icon?: string
14
- onClick?: () => void
15
- }
16
-
17
- type NavGroupItem = {
18
- title: string
19
- icon: string
20
- items: Array<{label: string; href: string}>
21
- }
22
-
23
- type CollapseMode = "auto" | "always" | "never"
24
-
25
- type Props = {
26
- brand?: string
27
- brandHref?: string
28
- brandTemplate?: SlotType
29
- items?: NavItem[]
30
- children?: SlotType
31
- navigationGroups?: NavGroupItem[]
32
- drawerContent?: SlotType
33
- collapse?: CollapseMode
34
- height?: number
35
- class?: string
36
- style?: string
37
- }
38
-
39
- let {
40
- brand = "Brand",
41
- brandHref = "/",
42
- brandTemplate = undefined,
43
- items = [],
44
- children = undefined,
45
- navigationGroups = [],
46
- drawerContent = undefined,
47
- collapse = "auto",
48
- height = 60,
49
- class: className = "",
50
- style = ""
51
- }: Props = $props()
52
-
53
- let mobileMenuOpen = $state(false)
54
-
55
- // Drawer is for collapsed nav items / groups OR custom drawerContent.
56
- // The action slot is intentionally NOT counted — it stays visible at every
57
- // width (search, account menu, theme toggle should remain reachable on
58
- // mobile rather than being hidden inside a hamburger).
59
- const hasDrawerContent = $derived(
60
- items.length > 0 || navigationGroups.length > 0 || !!drawerContent
61
- )
62
-
63
- function toggleMobileMenu() {
64
- mobileMenuOpen = !mobileMenuOpen
65
- }
66
-
67
- function closeMobileMenu() {
68
- mobileMenuOpen = false
69
- }
70
-
71
- function handleNavClick(item: NavItem) {
72
- item.onClick?.()
73
- closeMobileMenu()
74
- }
75
- </script>
76
-
77
- <nav
78
- class="topnav {className}"
79
- class:topnav--force-collapse={collapse === "always"}
80
- class:topnav--force-expand={collapse === "never"}
81
- style="height: {height}px; {style}"
82
- >
83
- <div class="topnav-container">
84
- {#if hasDrawerContent}
85
- <Button
86
- appearance="stealth"
87
- class="mobile-menu-toggle"
88
- onclick={toggleMobileMenu}
89
- aria-label={mobileMenuOpen ? "Close menu" : "Open menu"}
90
- aria-expanded={mobileMenuOpen}
91
- >
92
- <span class="hamburger-icon">
93
- {#if mobileMenuOpen}
94
- <DismissIcon size={20} />
95
- {:else}
96
-
97
- {/if}
98
- </span>
99
- </Button>
100
- {/if}
101
-
102
- {#if brandTemplate}
103
- {@render brandTemplate()}
104
- {:else}
105
- <a href={brandHref} class="topnav-brand">
106
- {brand}
107
- </a>
108
- {/if}
109
-
110
- <div class="topnav-trailing">
111
- {#if items.length > 0}
112
- <div class="topnav-items">
113
- {#each items as item}
114
- <a
115
- href={item.href}
116
- class="nav-item"
117
- onclick={() => handleNavClick(item)}
118
- >
119
- {#if item.icon}
120
- <span class="nav-icon">{item.icon}</span>
121
- {/if}
122
- <span class="nav-text">{item.label}</span>
123
- </a>
124
- {/each}
125
- </div>
126
- {/if}
127
-
128
- {#if children}
129
- {#if items.length > 0}
130
- <div class="nav-divider"></div>
131
- {/if}
132
- <div class="topnav-actions">
133
- {@render children()}
134
- </div>
135
- {/if}
136
- </div>
137
- </div>
138
- </nav>
139
-
140
- {#if hasDrawerContent}
141
- <Panel
142
- bind:open={mobileMenuOpen}
143
- side="start"
144
- width="280px"
145
- top="{height}px"
146
- class="topnav-drawer"
147
- >
148
- <div class="topnav-drawer-body">
149
- {#if items.length > 0}
150
- <div class="topnav-drawer-items">
151
- {#each items as item}
152
- <a
153
- href={item.href}
154
- class="nav-item"
155
- onclick={() => handleNavClick(item)}
156
- >
157
- {#if item.icon}
158
- <span class="nav-icon">{item.icon}</span>
159
- {/if}
160
- <span class="nav-text">{item.label}</span>
161
- </a>
162
- {/each}
163
- </div>
164
- {/if}
165
-
166
- {#if navigationGroups.length > 0}
167
- {#if items.length > 0}
168
- <hr class="topnav-drawer-separator" />
169
- {/if}
170
- <NavMenu>
171
- {#each navigationGroups as group}
172
- <NavGroup>
173
- {#snippet linkIcon()}
174
- <span class="nav-icon">{group.icon}</span>
175
- {/snippet}
176
- {#snippet linkText()}
177
- {group.title}
178
- {/snippet}
179
-
180
- {#each group.items as item}
181
- <NavLinkItem href={item.href}>{item.label}</NavLinkItem>
182
- {/each}
183
- </NavGroup>
184
- {/each}
185
- </NavMenu>
186
- {/if}
187
-
188
- {#if drawerContent}
189
- {#if items.length > 0 || navigationGroups.length > 0}
190
- <hr class="topnav-drawer-separator" />
191
- {/if}
192
- {@render drawerContent(closeMobileMenu)}
193
- {/if}
194
- </div>
195
- </Panel>
196
- {/if}
197
-
198
- <style>
199
- .topnav {
200
- display: flex;
201
- align-items: center;
202
- background: var(--neutral-layer-1, #ffffff);
203
- border-bottom: 1px solid var(--neutral-stroke-layer-rest, #e0e0e0);
204
- padding: 0 1.5rem;
205
- container-type: inline-size;
206
- container-name: topnav;
207
- }
208
-
209
- .topnav-container {
210
- display: flex;
211
- align-items: center;
212
- width: 100%;
213
- position: relative;
214
- gap: 1rem;
215
- min-width: 0;
216
- }
217
-
218
- .topnav-brand {
219
- text-decoration: none;
220
- color: inherit;
221
- font-size: 1.25rem;
222
- font-weight: 600;
223
- flex-shrink: 0;
224
- }
225
-
226
- /* Mobile menu toggle button — hidden by default, shown when container is narrow */
227
- :global(.topnav .mobile-menu-toggle) {
228
- display: none;
229
- order: -1;
230
- flex-shrink: 0;
231
- width: 42px;
232
- min-width: 42px;
233
- height: 42px;
234
- }
235
-
236
- /* Override fluent-button's internal control padding/min-height so the
237
- hamburger fills the fixed 42×42 box and the icon centers in it. */
238
- :global(.topnav .mobile-menu-toggle::part(control)) {
239
- width: 100%;
240
- height: 100%;
241
- min-width: 0;
242
- min-height: 0;
243
- padding: 0;
244
- }
245
-
246
- .hamburger-icon {
247
- font-size: 1.5rem;
248
- line-height: 1;
249
- display: flex;
250
- align-items: center;
251
- }
252
-
253
- /* Trailing flex group: items + divider + actions. `margin-left: auto`
254
- pushes it to the inline-end edge so brand stays next to hamburger
255
- (or hugs the start edge when hamburger is hidden), instead of being
256
- visually centered the way justify-content: space-between would do
257
- with three flex children. */
258
- .topnav-trailing {
259
- display: flex;
260
- align-items: center;
261
- gap: 1.5rem;
262
- min-width: 0;
263
- margin-inline-start: auto;
264
- }
265
-
266
- .topnav-items {
267
- display: flex;
268
- gap: 1.5rem;
269
- align-items: center;
270
- min-width: 0;
271
- overflow: hidden;
272
- }
273
-
274
- .nav-item {
275
- text-decoration: none;
276
- color: inherit;
277
- display: flex;
278
- align-items: center;
279
- gap: 0.5rem;
280
- padding: 0.5rem 0.75rem;
281
- border-radius: var(--fluent-border-radius-md);
282
- transition: background-color 0.2s;
283
- }
284
-
285
- .nav-item:hover {
286
- background-color: var(--neutral-fill-stealth-hover, rgba(0, 0, 0, 0.05));
287
- }
288
-
289
- .nav-icon {
290
- font-size: 1.1rem;
291
- }
292
-
293
- .nav-divider {
294
- width: 1px;
295
- height: 24px;
296
- background: var(--neutral-stroke-divider-rest, #e0e0e0);
297
- }
298
-
299
- .topnav-actions {
300
- display: flex;
301
- align-items: center;
302
- gap: 0.5rem;
303
- flex-shrink: 0;
304
- }
305
-
306
- /* Drawer styling — Panel handles position / transition / a11y */
307
- .topnav-drawer-body {
308
- flex: 1 1 0;
309
- min-height: 0;
310
- overflow-y: auto;
311
- }
312
-
313
- .topnav-drawer-items {
314
- display: flex;
315
- flex-direction: column;
316
- gap: 0.125rem;
317
- padding: 0 0.75rem;
318
- }
319
-
320
- .topnav-drawer-items .nav-item {
321
- padding: 0.6rem 0.75rem;
322
- }
323
-
324
- .topnav-drawer-separator {
325
- border: none;
326
- border-top: 1px solid var(--neutral-stroke-divider-rest, #e0e0e0);
327
- margin: 0.75rem 0;
328
- }
329
-
330
- @container topnav (max-width: 960px) {
331
- :global(.topnav .mobile-menu-toggle) {
332
- display: flex;
333
- }
334
-
335
- .topnav-items,
336
- .nav-divider {
337
- display: none;
338
- }
339
- }
340
-
341
- @container topnav (max-width: 480px) {
342
- .topnav-brand {
343
- font-size: 1rem;
344
- }
345
- }
346
-
347
- /* collapse="always" — force hamburger visible + items hidden regardless of width */
348
- :global(.topnav.topnav--force-collapse .mobile-menu-toggle) {
349
- display: flex;
350
- }
351
-
352
- .topnav--force-collapse .topnav-items,
353
- .topnav--force-collapse .nav-divider {
354
- display: none;
355
- }
356
-
357
- /* collapse="never" — force items/divider visible + hamburger hidden regardless of width */
358
- :global(.topnav.topnav--force-expand .mobile-menu-toggle) {
359
- display: none;
360
- }
361
-
362
- .topnav--force-expand .topnav-items {
363
- display: flex;
364
- }
365
-
366
- .topnav--force-expand .nav-divider {
367
- display: block;
368
- }
369
- </style>
1
+ <script lang="ts">
2
+ import type {SlotType} from "../../types/index.js"
3
+ import Button from "../Button.svelte"
4
+ import Panel from "./Panel.svelte"
5
+ import NavMenu from "../nav/NavMenu.svelte"
6
+ import NavGroup from "../nav/NavGroup.svelte"
7
+ import NavLinkItem from "../nav/NavLinkItem.svelte"
8
+ import DismissIcon from "../icons/DismissIcon.svelte"
9
+
10
+ type NavItem = {
11
+ label: string
12
+ href: string
13
+ icon?: string
14
+ onClick?: () => void
15
+ }
16
+
17
+ type NavGroupItem = {
18
+ title: string
19
+ icon: string
20
+ items: Array<{label: string; href: string}>
21
+ }
22
+
23
+ type CollapseMode = "auto" | "always" | "never"
24
+
25
+ type Props = {
26
+ brand?: string
27
+ brandHref?: string
28
+ brandTemplate?: SlotType
29
+ items?: NavItem[]
30
+ children?: SlotType
31
+ navigationGroups?: NavGroupItem[]
32
+ drawerContent?: SlotType
33
+ collapse?: CollapseMode
34
+ drawerPinned?: boolean
35
+ drawerWidth?: string
36
+ height?: number
37
+ class?: string
38
+ style?: string
39
+ }
40
+
41
+ let {
42
+ brand = "Brand",
43
+ brandHref = "/",
44
+ brandTemplate = undefined,
45
+ items = [],
46
+ children = undefined,
47
+ navigationGroups = [],
48
+ drawerContent = undefined,
49
+ collapse = "auto",
50
+ drawerPinned = false,
51
+ drawerWidth = "280px",
52
+ height = 60,
53
+ class: className = "",
54
+ style = ""
55
+ }: Props = $props()
56
+
57
+ let mobileMenuOpen = $state(false)
58
+
59
+ // Drawer is for collapsed nav items / groups OR custom drawerContent.
60
+ // The action slot is intentionally NOT counted — it stays visible at every
61
+ // width (search, account menu, theme toggle should remain reachable on
62
+ // mobile rather than being hidden inside a hamburger).
63
+ const hasDrawerContent = $derived(
64
+ items.length > 0 || navigationGroups.length > 0 || !!drawerContent
65
+ )
66
+
67
+ // Sync drawer's open state with `drawerPinned` so transitioning to pinned
68
+ // mode auto-reveals the rail (desktop default) and transitioning back to
69
+ // overlay mode starts closed (no surprise overlay on viewport resize).
70
+ // User toggles via the hamburger between transitions stand on their own
71
+ // since this effect only re-runs when drawerPinned itself changes.
72
+ $effect(() => {
73
+ mobileMenuOpen = drawerPinned
74
+ })
75
+
76
+ function toggleMobileMenu() {
77
+ mobileMenuOpen = !mobileMenuOpen
78
+ }
79
+
80
+ function closeMobileMenu() {
81
+ mobileMenuOpen = false
82
+ }
83
+
84
+ function handleNavClick(item: NavItem) {
85
+ item.onClick?.()
86
+ closeMobileMenu()
87
+ }
88
+ </script>
89
+
90
+ <nav
91
+ class="topnav {className}"
92
+ class:topnav--force-collapse={collapse === "always"}
93
+ class:topnav--force-expand={collapse === "never"}
94
+ class:topnav--drawer-pinned={drawerPinned}
95
+ style="height: {height}px; {style}"
96
+ >
97
+ <div class="topnav-container">
98
+ {#if hasDrawerContent}
99
+ <Button
100
+ appearance="stealth"
101
+ class="mobile-menu-toggle"
102
+ onclick={toggleMobileMenu}
103
+ aria-label={mobileMenuOpen ? "Close menu" : "Open menu"}
104
+ aria-expanded={mobileMenuOpen}
105
+ >
106
+ <span class="hamburger-icon">
107
+ {#if mobileMenuOpen}
108
+ <DismissIcon size={20} />
109
+ {:else}
110
+
111
+ {/if}
112
+ </span>
113
+ </Button>
114
+ {/if}
115
+
116
+ {#if brandTemplate}
117
+ {@render brandTemplate()}
118
+ {:else}
119
+ <a href={brandHref} class="topnav-brand">
120
+ {brand}
121
+ </a>
122
+ {/if}
123
+
124
+ <div class="topnav-trailing">
125
+ {#if items.length > 0}
126
+ <div class="topnav-items">
127
+ {#each items as item}
128
+ <a
129
+ href={item.href}
130
+ class="nav-item"
131
+ onclick={() => handleNavClick(item)}
132
+ >
133
+ {#if item.icon}
134
+ <span class="nav-icon">{item.icon}</span>
135
+ {/if}
136
+ <span class="nav-text">{item.label}</span>
137
+ </a>
138
+ {/each}
139
+ </div>
140
+ {/if}
141
+
142
+ {#if children}
143
+ {#if items.length > 0}
144
+ <div class="nav-divider"></div>
145
+ {/if}
146
+ <div class="topnav-actions">
147
+ {@render children()}
148
+ </div>
149
+ {/if}
150
+ </div>
151
+ </div>
152
+ </nav>
153
+
154
+ {#if hasDrawerContent}
155
+ <Panel
156
+ bind:open={mobileMenuOpen}
157
+ side="start"
158
+ width={drawerWidth}
159
+ top="{height}px"
160
+ pinned={drawerPinned}
161
+ class="topnav-drawer {drawerPinned ? 'topnav-drawer--pinned' : ''}"
162
+ >
163
+ <div class="topnav-drawer-body">
164
+ {#if items.length > 0}
165
+ <div class="topnav-drawer-items">
166
+ {#each items as item}
167
+ <a
168
+ href={item.href}
169
+ class="nav-item"
170
+ onclick={() => handleNavClick(item)}
171
+ >
172
+ {#if item.icon}
173
+ <span class="nav-icon">{item.icon}</span>
174
+ {/if}
175
+ <span class="nav-text">{item.label}</span>
176
+ </a>
177
+ {/each}
178
+ </div>
179
+ {/if}
180
+
181
+ {#if navigationGroups.length > 0}
182
+ {#if items.length > 0}
183
+ <hr class="topnav-drawer-separator" />
184
+ {/if}
185
+ <NavMenu>
186
+ {#each navigationGroups as group}
187
+ <NavGroup>
188
+ {#snippet linkIcon()}
189
+ <span class="nav-icon">{group.icon}</span>
190
+ {/snippet}
191
+ {#snippet linkText()}
192
+ {group.title}
193
+ {/snippet}
194
+
195
+ {#each group.items as item}
196
+ <NavLinkItem href={item.href}>{item.label}</NavLinkItem>
197
+ {/each}
198
+ </NavGroup>
199
+ {/each}
200
+ </NavMenu>
201
+ {/if}
202
+
203
+ {#if drawerContent}
204
+ {#if items.length > 0 || navigationGroups.length > 0}
205
+ <hr class="topnav-drawer-separator" />
206
+ {/if}
207
+ {@render drawerContent(closeMobileMenu)}
208
+ {/if}
209
+ </div>
210
+ </Panel>
211
+ {/if}
212
+
213
+ <style>
214
+ .topnav {
215
+ display: flex;
216
+ align-items: center;
217
+ background: var(--neutral-layer-1, #ffffff);
218
+ border-bottom: 1px solid var(--neutral-stroke-layer-rest, #e0e0e0);
219
+ padding: 0 0.5rem;
220
+ container-type: inline-size;
221
+ container-name: topnav;
222
+ }
223
+
224
+ .topnav-container {
225
+ display: flex;
226
+ align-items: center;
227
+ width: 100%;
228
+ position: relative;
229
+ gap: 1rem;
230
+ min-width: 0;
231
+ }
232
+
233
+ .topnav-brand {
234
+ text-decoration: none;
235
+ color: inherit;
236
+ font-size: 1.25rem;
237
+ font-weight: 600;
238
+ flex-shrink: 0;
239
+ }
240
+
241
+ /* Mobile menu toggle button — hidden by default, shown when container is narrow */
242
+ :global(.topnav .mobile-menu-toggle) {
243
+ display: none;
244
+ order: -1;
245
+ flex-shrink: 0;
246
+ width: 42px;
247
+ min-width: 42px;
248
+ height: 42px;
249
+ }
250
+
251
+ /* Override fluent-button's internal control padding/min-height so the
252
+ hamburger fills the fixed 42×42 box and the icon centers in it. */
253
+ :global(.topnav .mobile-menu-toggle::part(control)) {
254
+ width: 100%;
255
+ height: 100%;
256
+ min-width: 0;
257
+ min-height: 0;
258
+ padding: 0;
259
+ }
260
+
261
+ .hamburger-icon {
262
+ font-size: 1.5rem;
263
+ line-height: 1;
264
+ display: flex;
265
+ align-items: center;
266
+ }
267
+
268
+ /* Trailing flex group: items + divider + actions. `margin-left: auto`
269
+ pushes it to the inline-end edge so brand stays next to hamburger
270
+ (or hugs the start edge when hamburger is hidden), instead of being
271
+ visually centered the way justify-content: space-between would do
272
+ with three flex children. */
273
+ .topnav-trailing {
274
+ display: flex;
275
+ align-items: center;
276
+ gap: 1.5rem;
277
+ min-width: 0;
278
+ margin-inline-start: auto;
279
+ }
280
+
281
+ .topnav-items {
282
+ display: flex;
283
+ gap: 1.5rem;
284
+ align-items: center;
285
+ min-width: 0;
286
+ overflow: hidden;
287
+ }
288
+
289
+ .nav-item {
290
+ text-decoration: none;
291
+ color: inherit;
292
+ display: flex;
293
+ align-items: center;
294
+ gap: 0.5rem;
295
+ padding: 0.5rem 0.75rem;
296
+ border-radius: var(--fluent-border-radius-md);
297
+ transition: background-color 0.2s;
298
+ }
299
+
300
+ .nav-item:hover {
301
+ background-color: var(--neutral-fill-stealth-hover, rgba(0, 0, 0, 0.05));
302
+ }
303
+
304
+ .nav-icon {
305
+ font-size: 1.1rem;
306
+ }
307
+
308
+ .nav-divider {
309
+ width: 1px;
310
+ height: 24px;
311
+ background: var(--neutral-stroke-divider-rest, #e0e0e0);
312
+ }
313
+
314
+ .topnav-actions {
315
+ display: flex;
316
+ align-items: center;
317
+ gap: 0.5rem;
318
+ flex-shrink: 0;
319
+ }
320
+
321
+ /* Drawer styling — Panel handles position / transition / a11y */
322
+ .topnav-drawer-body {
323
+ flex: 1 1 0;
324
+ min-height: 0;
325
+ overflow-y: auto;
326
+ }
327
+
328
+ .topnav-drawer-items {
329
+ display: flex;
330
+ flex-direction: column;
331
+ gap: 0.125rem;
332
+ padding: 0 0.75rem;
333
+ }
334
+
335
+ .topnav-drawer-items .nav-item {
336
+ padding: 0.6rem 0.75rem;
337
+ }
338
+
339
+ .topnav-drawer-separator {
340
+ border: none;
341
+ border-top: 1px solid var(--neutral-stroke-divider-rest, #e0e0e0);
342
+ margin: 0.75rem 0;
343
+ }
344
+
345
+ @container topnav (max-width: 960px) {
346
+ :global(.topnav .mobile-menu-toggle) {
347
+ display: flex;
348
+ }
349
+
350
+ .topnav-items,
351
+ .nav-divider {
352
+ display: none;
353
+ }
354
+ }
355
+
356
+ @container topnav (max-width: 480px) {
357
+ .topnav-brand {
358
+ font-size: 1rem;
359
+ }
360
+ }
361
+
362
+ /* collapse="always" — force hamburger visible + items hidden regardless of width */
363
+ :global(.topnav.topnav--force-collapse .mobile-menu-toggle) {
364
+ display: flex;
365
+ }
366
+
367
+ /* When drawer is pinned the hamburger acts as a show/hide toggle for the
368
+ pinned rail itself, so it must stay visible at every width. */
369
+ :global(.topnav.topnav--drawer-pinned .mobile-menu-toggle) {
370
+ display: flex;
371
+ }
372
+
373
+ .topnav--force-collapse .topnav-items,
374
+ .topnav--force-collapse .nav-divider {
375
+ display: none;
376
+ }
377
+
378
+ /* collapse="never" — force items/divider visible + hamburger hidden regardless of width */
379
+ :global(.topnav.topnav--force-expand .mobile-menu-toggle) {
380
+ display: none;
381
+ }
382
+
383
+ .topnav--force-expand .topnav-items {
384
+ display: flex;
385
+ }
386
+
387
+ .topnav--force-expand .nav-divider {
388
+ display: block;
389
+ }
390
+ </style>