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.
package/README.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  A comprehensive Svelte wrapper library for Microsoft FluentUI web components (v2.6.x), providing a seamless way to use FluentUI components in Svelte applications.
4
4
 
5
+ ## What's New in v1.3.1
6
+
7
+ - **`Panel` — new `pinned` prop renders the panel as an inline `<aside>` instead of a portal'd overlay** — flips the rendering entirely: no portal, no backdrop, no transforms, no escape/outside-click handlers, `position: relative` so it participates in the parent's flex/grid layout, `align-self: stretch` so it fills its grid cell vertically. `open` is still respected so the panel can hide and let its grid cell collapse. Explicit z-index forms its own stacking context so overlay Panels opened on top still layer above as expected.
8
+ - **`TopNav` — new `drawerPinned` + `drawerWidth` props turn the drawer into a permanent left rail with an always-visible hamburger toggle** — composes Panel's `pinned` so the same `drawerContent` snippet that renders the mobile overlay drawer also renders the desktop rail. `drawerWidth` (default `"280px"`) propagates to both modes. The hamburger stays visible when pinned so the user can collapse the rail back away, and an effect re-syncs the drawer's `open` state to `drawerPinned` on viewport flips (narrow→wide auto-opens, wide→narrow auto-closes).
9
+ - **Docs layout switched to a single nav surface at every width via `drawerPinned`** — the duplicate desktop sidebar `GridItem` is gone; TopNav's drawer now renders as a pinned `<aside>` at ≥1024px and collapses to the hamburger overlay below. The layout uses CSS Grid with `grid-template-areas` and a `matchMedia` listener to drive `drawerPinned`. The pinned rail is `position: sticky` below the topnav and the scrollbar sits flush with the rail's inside edge.
10
+ - **`TopNav` — tightened horizontal padding from `1.5rem` to `0.5rem`** — the bar's inside edges felt cramped relative to the rest of the content. Brand + hamburger now sit closer to the inline-start edge and the action slot sits closer to the inline-end edge.
11
+
5
12
  ## What's New in v1.3.0
6
13
 
7
14
  - **`Panel` — new slide-in side-panel primitive** — fixed-position, edge-anchored container with a single `children` slot and CSS-driven open/close transitions. Props cover physical sides (`left`/`right`), logical sides (`start`/`end` that follow RTL), `width`, `overlay`, `closeOnOutsideClick`, `closeOnEscape`, and a new `top` offset so the panel + overlay can sit below a sticky header (e.g. TopNav) and leave the trigger button clickable. Portals to `document.body` with direction captured before portal so locally-RTL wrappers still anchor correctly. First-class demo at `/components/navigation-layout/panel`.
@@ -11,15 +18,6 @@ A comprehensive Svelte wrapper library for Microsoft FluentUI web components (v2
11
18
  - **`Footer` — class namespaced as `.fluent-footer`, wired to the dedicated theme tokens, thin-strip bug fixed** — the rendered class went from generic `.footer` (collided with anything else using that name) to `.fluent-footer`, the rule now reads from `--fluent-footer-bg/text/border` (which were defined in the theme system but unused) with the previous neutral-layer values as fallback, a duplicate Blazor-port `.footer` rule was deleted, and `min-height: 48px` + `padding: 0.75rem 1rem` replaces the original horizontal-only padding so a default Footer is no longer a ~24px thin strip. First dedicated demo page at `/components/navigation-layout/footer`.
12
19
  - **Docs site dogfoods the new primitives** — the hand-rolled `<div class="topnav">` with ~90 lines of parallel CSS is replaced with `<TopNav class="docs-topnav" collapse="always">` using `brandTemplate` for the brand+version pair and `drawerContent` for the mobile sidebar nav. The sidebar nav is extracted into a single `{#snippet sidebarNav(closeDrawer)}` rendered both as the desktop sidebar and inside the TopNav drawer, with the sidebar/hamburger handoff at `lg` (1280px) instead of `md` so laptops at 1280×800 stay roomy. The footer at the bottom of every page now uses the three-part layout (© + MIT / attribution / GitHub + npm + version chip).
13
20
 
14
- ## What's New in v1.2.0
15
-
16
- - **`Divider` — `orientation` and `role` props now forwarded** — the wrapper previously only exposed `class` and `style`, so vertical dividers and `role="presentation"` (for decorative separators that should be skipped by screen readers) weren't expressible through the Svelte API. `<fluent-divider>` natively supports both via fast-foundation; the wrapper just forwards them. Defaults preserved (`orientation="horizontal"`, `role="separator"`) so every existing call site renders identically. First proper demo page at `/components/divider` — the previous `/divider` route was an HMR sandbox and the components-list link to `/components/divider` was a 404.
17
- - **`Select` rewritten as a custom component** — the wrapped `<fluent-select>` rendered its dropdown listbox inside its own shadow DOM with a static `--max-height`, so inside Dialogs and other overflow-hidden ancestors the option list extended past the viewport with no scroll cutoff. New custom Select portals the listbox to `document.body` via `PositioningRegion` (Floating-UI), so it escapes any clipping ancestor and stays inside the viewport regardless of where the trigger sits. Full keyboard wiring (Arrow/Home/End/Enter/Space/Escape/Tab, single-char type-ahead with cycle-through-matches), multi-mode renders an always-expanded inline listbox matching FluentUI Blazor, and a hidden `<input>` keeps native form submission working. Public API mostly preserved; `value` widened to `string | string[]` for multi mode, `open` is now bindable, and `position` means "force placement" rather than the old static attribute.
18
- - **`PositioningRegion` — new `availableHeight` prop** — writes the computed remaining viewport height into the `--available-height` CSS custom property on the floating element on every reposition, so dropdowns can cap their max-height with `max-height: var(--available-height, <fallback>)` instead of hard-coding a value that overflows on small screens or near viewport edges. Works alongside `matchWidth` in a single `size()` middleware pass.
19
- - **`.fluent-value-label` — new canonical class for inline value labels next to a control** — separates field labels (the bold, stacked-above text that names a form field) from value labels (the inline text next to a single control that describes the value, e.g. "Remember me" next to a checkbox). Value labels are normal weight with a pointer cursor, matching FluentUI Blazor's pickers and `<fluent-radio>`'s slotted label. `data-disabled` / `data-readonly` on the wrapper drive a `not-allowed` cursor when the host is inactive.
20
- - **`Checkbox` & `Switch` — inline label fixes** — labels were rendering bold with a text cursor (using the field-label class) and clicks on the label didn't reliably toggle the control when no `id` was passed. Switched to the new `.fluent-value-label` class, and a stable per-instance `fallbackId` is now used as `<label for>` / element `id` when the caller didn't supply one, so label-click-to-toggle works regardless of whether an `id` was passed in. Applies across all three `labelPosition` values (`top` / `start` / `end`).
21
- - **`Select` & `Combobox` — field-label class consolidated to shared `.fluent-label`** — both components had component-scoped `.select-label` / `.combobox-label` rules that duplicated the canonical `.fluent-label` declarations from `assets/styles/components.scss`. A single library-wide tweak to label styling now lives in one place. After this change, every component that renders a stacked field label uses `.fluent-label`, Field uses its BEM-namespaced `fluent-field__label`, and Checkbox/Switch use `.fluent-value-label` — three classes total, each with a clear semantic role.
22
-
23
21
  ## Features
24
22
 
25
23
  - 🎨 **Complete FluentUI Component Set** - Wraps all major FluentUI web components
@@ -1,266 +1,311 @@
1
- <script lang="ts" module>
2
- type PanelHandle = {
3
- closeOnEscape: () => boolean
4
- hide: () => void
5
- }
6
- const openPanelStack: PanelHandle[] = []
7
- let escListenerAttached = false
8
-
9
- function handleGlobalEscape(e: KeyboardEvent) {
10
- if (e.key !== "Escape") return
11
- const top = openPanelStack[openPanelStack.length - 1]
12
- if (!top) return
13
- if (!top.closeOnEscape()) return
14
- e.preventDefault()
15
- e.stopPropagation()
16
- top.hide()
17
- }
18
-
19
- function pushPanel(handle: PanelHandle) {
20
- openPanelStack.push(handle)
21
- if (!escListenerAttached && typeof document !== "undefined") {
22
- document.addEventListener("keydown", handleGlobalEscape, true)
23
- escListenerAttached = true
24
- }
25
- }
26
-
27
- function popPanel(handle: PanelHandle) {
28
- const idx = openPanelStack.lastIndexOf(handle)
29
- if (idx >= 0) openPanelStack.splice(idx, 1)
30
- if (openPanelStack.length === 0 && escListenerAttached && typeof document !== "undefined") {
31
- document.removeEventListener("keydown", handleGlobalEscape, true)
32
- escListenerAttached = false
33
- }
34
- }
35
- </script>
36
-
37
- <script lang="ts">
38
- import {onDestroy} from "svelte"
39
- import type {SlotType} from "../../types/index.js"
40
-
41
- type Side = "left" | "right" | "start" | "end"
42
-
43
- type Props = {
44
- open?: boolean
45
- side?: Side
46
- width?: string
47
- top?: string
48
- overlay?: boolean
49
- closeOnOutsideClick?: boolean
50
- closeOnEscape?: boolean
51
- onclose?: () => void
52
- children?: SlotType
53
- class?: string
54
- style?: string
55
- }
56
-
57
- let {
58
- open = $bindable(false),
59
- side = "right",
60
- width = "320px",
61
- top = "0",
62
- overlay = true,
63
- closeOnOutsideClick = true,
64
- closeOnEscape = true,
65
- onclose = undefined,
66
- children = undefined,
67
- class: className = undefined,
68
- style = undefined
69
- }: Props = $props()
70
-
71
- let panelEl: HTMLElement | undefined = $state()
72
- let isRTL = $state(false)
73
-
74
- const handle: PanelHandle = {
75
- closeOnEscape: () => closeOnEscape,
76
- hide: () => close()
77
- }
78
-
79
- let wasOpen = false
80
- $effect(() => {
81
- if (open && !wasOpen) {
82
- pushPanel(handle)
83
- wasOpen = true
84
- } else if (!open && wasOpen) {
85
- popPanel(handle)
86
- wasOpen = false
87
- }
88
- })
89
-
90
- // Document-level outside-click detection — only needed when there's no
91
- // overlay to catch the click. Deferred to the next task so the same click
92
- // that opened the panel doesn't immediately close it.
93
- $effect(() => {
94
- if (!open || overlay || !closeOnOutsideClick) return
95
- const id = setTimeout(() => {
96
- document.addEventListener("pointerdown", handleOutsideClick, true)
97
- }, 0)
98
- return () => {
99
- clearTimeout(id)
100
- document.removeEventListener("pointerdown", handleOutsideClick, true)
101
- }
102
- })
103
-
104
- function handleOutsideClick(e: PointerEvent) {
105
- if (!panelEl) return
106
- if (panelEl.contains(e.target as Node)) return
107
- close()
108
- }
109
-
110
- onDestroy(() => {
111
- if (wasOpen) popPanel(handle)
112
- })
113
-
114
- function close() {
115
- open = false
116
- onclose?.()
117
- }
118
-
119
- function onOverlayClick() {
120
- if (closeOnOutsideClick) close()
121
- }
122
-
123
- // Combined: capture the original parent's direction BEFORE moving the node
124
- // under <body>. Doing both in one action avoids relying on Svelte's
125
- // multi-action source-order. We set `dir="rtl"` (so `inset-inline-*`
126
- // resolves correctly) AND flip an isRTL flag (so a class-based selector can
127
- // drive the rest of the styling without depending on attribute matching).
128
- function setupRoot(node: HTMLElement) {
129
- const parent = node.parentElement
130
- if (parent && getComputedStyle(parent).direction === "rtl") {
131
- node.setAttribute("dir", "rtl")
132
- isRTL = true
133
- }
134
- document.body.appendChild(node)
135
- return {
136
- destroy() {
137
- node.parentNode?.removeChild(node)
138
- }
139
- }
140
- }
141
- </script>
142
-
143
- <div
144
- class="fluent-panel-root fluent-panel-root--{side} {className || ''}"
145
- class:fluent-panel-root--open={open}
146
- class:fluent-panel-root--rtl={isRTL}
147
- style="--panel-top: {top};"
148
- aria-hidden={!open}
149
- use:setupRoot
150
- >
151
- {#if overlay}
152
- <div
153
- class="fluent-panel-overlay"
154
- role="button"
155
- tabindex={-1}
156
- aria-label="Close panel"
157
- onclick={onOverlayClick}
158
- onkeydown={(e) => (e.key === "Enter" || e.key === " ") && onOverlayClick()}
159
- ></div>
160
- {/if}
161
-
162
- <div
163
- bind:this={panelEl}
164
- class="fluent-panel"
165
- role="dialog"
166
- aria-modal={overlay}
167
- style="width: {width}; {style || ''}"
168
- >
169
- {@render children?.()}
170
- </div>
171
- </div>
172
-
173
- <style lang="scss">
174
- .fluent-panel-root {
175
- position: fixed;
176
- top: var(--panel-top, 0);
177
- right: 0;
178
- bottom: 0;
179
- left: 0;
180
- z-index: var(--fluent-z-modal, 1050);
181
- // Never block the page — only the overlay and the panel surface
182
- // (re-enabled below) capture pointer events.
183
- pointer-events: none;
184
- }
185
-
186
- .fluent-panel-overlay {
187
- position: absolute;
188
- inset: 0;
189
- background-color: rgba(0, 0, 0, 0);
190
- cursor: pointer;
191
- pointer-events: none;
192
- transition: background-color 0.25s ease;
193
- z-index: var(--fluent-z-modal-backdrop, 1040);
194
- }
195
-
196
- .fluent-panel {
197
- position: absolute;
198
- top: 0;
199
- height: 100%;
200
- max-width: 90vw;
201
- background-color: var(--neutral-layer-1, #ffffff);
202
- display: flex;
203
- flex-direction: column;
204
- overflow: hidden;
205
- pointer-events: none;
206
- transition: transform 0.25s ease, visibility 0.25s ease;
207
- visibility: hidden;
208
- z-index: calc(var(--fluent-z-modal, 1050) + 1);
209
- }
210
-
211
- // Closed-state per-side transforms — declared BEFORE the --open rule so
212
- // the open rule wins on source order at equal specificity.
213
-
214
- // Physical sides — never flip with direction.
215
- .fluent-panel-root--right .fluent-panel {
216
- right: 0;
217
- box-shadow: -4px 0 16px rgba(0, 0, 0, 0.15);
218
- transform: translateX(100%);
219
- }
220
-
221
- .fluent-panel-root--left .fluent-panel {
222
- left: 0;
223
- box-shadow: 4px 0 16px rgba(0, 0, 0, 0.15);
224
- transform: translateX(-100%);
225
- }
226
-
227
- // Logical sides — follow the writing direction (start = left in LTR, right in RTL).
228
- .fluent-panel-root--start .fluent-panel {
229
- inset-inline-start: 0;
230
- box-shadow: 4px 0 16px rgba(0, 0, 0, 0.15);
231
- transform: translateX(-100%);
232
- }
233
-
234
- .fluent-panel-root--start.fluent-panel-root--rtl .fluent-panel {
235
- inset-inline-start: unset;
236
- right: 0;
237
- box-shadow: -4px 0 16px rgba(0, 0, 0, 0.15);
238
- transform: translateX(100%);
239
- }
240
-
241
- .fluent-panel-root--end .fluent-panel {
242
- inset-inline-end: 0;
243
- box-shadow: -4px 0 16px rgba(0, 0, 0, 0.15);
244
- transform: translateX(100%);
245
- }
246
-
247
- .fluent-panel-root--end.fluent-panel-root--rtl .fluent-panel {
248
- inset-inline-end: unset;
249
- left: 0;
250
- box-shadow: 4px 0 16px rgba(0, 0, 0, 0.15);
251
- transform: translateX(-100%);
252
- }
253
-
254
- .fluent-panel-root--open .fluent-panel-overlay {
255
- background-color: rgba(0, 0, 0, 0.3);
256
- pointer-events: auto;
257
- }
258
-
259
- // Match the (0,3,0) specificity of the RTL side overrides so the open
260
- // transform wins on source order (this rule is last).
261
- .fluent-panel-root.fluent-panel-root--open .fluent-panel {
262
- transform: translateX(0);
263
- visibility: visible;
264
- pointer-events: auto;
265
- }
266
- </style>
1
+ <script lang="ts" module>
2
+ type PanelHandle = {
3
+ closeOnEscape: () => boolean
4
+ hide: () => void
5
+ }
6
+ const openPanelStack: PanelHandle[] = []
7
+ let escListenerAttached = false
8
+
9
+ function handleGlobalEscape(e: KeyboardEvent) {
10
+ if (e.key !== "Escape") return
11
+ const top = openPanelStack[openPanelStack.length - 1]
12
+ if (!top) return
13
+ if (!top.closeOnEscape()) return
14
+ e.preventDefault()
15
+ e.stopPropagation()
16
+ top.hide()
17
+ }
18
+
19
+ function pushPanel(handle: PanelHandle) {
20
+ openPanelStack.push(handle)
21
+ if (!escListenerAttached && typeof document !== "undefined") {
22
+ document.addEventListener("keydown", handleGlobalEscape, true)
23
+ escListenerAttached = true
24
+ }
25
+ }
26
+
27
+ function popPanel(handle: PanelHandle) {
28
+ const idx = openPanelStack.lastIndexOf(handle)
29
+ if (idx >= 0) openPanelStack.splice(idx, 1)
30
+ if (openPanelStack.length === 0 && escListenerAttached && typeof document !== "undefined") {
31
+ document.removeEventListener("keydown", handleGlobalEscape, true)
32
+ escListenerAttached = false
33
+ }
34
+ }
35
+ </script>
36
+
37
+ <script lang="ts">
38
+ import {onDestroy} from "svelte"
39
+ import type {SlotType} from "../../types/index.js"
40
+
41
+ type Side = "left" | "right" | "start" | "end"
42
+
43
+ type Props = {
44
+ open?: boolean
45
+ side?: Side
46
+ width?: string
47
+ top?: string
48
+ overlay?: boolean
49
+ closeOnOutsideClick?: boolean
50
+ closeOnEscape?: boolean
51
+ pinned?: boolean
52
+ onclose?: () => void
53
+ children?: SlotType
54
+ class?: string
55
+ style?: string
56
+ }
57
+
58
+ let {
59
+ open = $bindable(false),
60
+ side = "right",
61
+ width = "320px",
62
+ top = "0",
63
+ overlay = true,
64
+ closeOnOutsideClick = true,
65
+ closeOnEscape = true,
66
+ pinned = false,
67
+ onclose = undefined,
68
+ children = undefined,
69
+ class: className = undefined,
70
+ style = undefined
71
+ }: Props = $props()
72
+
73
+ let panelEl: HTMLElement | undefined = $state()
74
+ let isRTL = $state(false)
75
+
76
+ const handle: PanelHandle = {
77
+ closeOnEscape: () => closeOnEscape,
78
+ hide: () => close()
79
+ }
80
+
81
+ let wasOpen = false
82
+ $effect(() => {
83
+ if (pinned) return
84
+ if (open && !wasOpen) {
85
+ pushPanel(handle)
86
+ wasOpen = true
87
+ } else if (!open && wasOpen) {
88
+ popPanel(handle)
89
+ wasOpen = false
90
+ }
91
+ })
92
+
93
+ // Document-level outside-click detection — only needed when there's no
94
+ // overlay to catch the click. Deferred to the next task so the same click
95
+ // that opened the panel doesn't immediately close it.
96
+ $effect(() => {
97
+ if (pinned || !open || overlay || !closeOnOutsideClick) return
98
+ const id = setTimeout(() => {
99
+ document.addEventListener("pointerdown", handleOutsideClick, true)
100
+ }, 0)
101
+ return () => {
102
+ clearTimeout(id)
103
+ document.removeEventListener("pointerdown", handleOutsideClick, true)
104
+ }
105
+ })
106
+
107
+ function handleOutsideClick(e: PointerEvent) {
108
+ if (!panelEl) return
109
+ if (panelEl.contains(e.target as Node)) return
110
+ close()
111
+ }
112
+
113
+ onDestroy(() => {
114
+ if (wasOpen) popPanel(handle)
115
+ })
116
+
117
+ function close() {
118
+ open = false
119
+ onclose?.()
120
+ }
121
+
122
+ function onOverlayClick() {
123
+ if (closeOnOutsideClick) close()
124
+ }
125
+
126
+ // Combined: capture the original parent's direction BEFORE moving the node
127
+ // under <body>. Doing both in one action avoids relying on Svelte's
128
+ // multi-action source-order. We set `dir="rtl"` (so `inset-inline-*`
129
+ // resolves correctly) AND flip an isRTL flag (so a class-based selector can
130
+ // drive the rest of the styling without depending on attribute matching).
131
+ function setupRoot(node: HTMLElement) {
132
+ const parent = node.parentElement
133
+ if (parent && getComputedStyle(parent).direction === "rtl") {
134
+ node.setAttribute("dir", "rtl")
135
+ isRTL = true
136
+ }
137
+ document.body.appendChild(node)
138
+ return {
139
+ destroy() {
140
+ node.parentNode?.removeChild(node)
141
+ }
142
+ }
143
+ }
144
+ </script>
145
+
146
+ {#if pinned}
147
+ {#if open}
148
+ <aside
149
+ class="fluent-panel fluent-panel--pinned fluent-panel--pinned-{side} {className || ''}"
150
+ style="width: {width}; {style || ''}"
151
+ >
152
+ {@render children?.()}
153
+ </aside>
154
+ {/if}
155
+ {:else}
156
+ <div
157
+ class="fluent-panel-root fluent-panel-root--{side} {className || ''}"
158
+ class:fluent-panel-root--open={open}
159
+ class:fluent-panel-root--rtl={isRTL}
160
+ style="--panel-top: {top};"
161
+ aria-hidden={!open}
162
+ use:setupRoot
163
+ >
164
+ {#if overlay}
165
+ <div
166
+ class="fluent-panel-overlay"
167
+ role="button"
168
+ tabindex={-1}
169
+ aria-label="Close panel"
170
+ onclick={onOverlayClick}
171
+ onkeydown={(e) => (e.key === "Enter" || e.key === " ") && onOverlayClick()}
172
+ ></div>
173
+ {/if}
174
+
175
+ <div
176
+ bind:this={panelEl}
177
+ class="fluent-panel"
178
+ role="dialog"
179
+ aria-modal={overlay}
180
+ style="width: {width}; {style || ''}"
181
+ >
182
+ {@render children?.()}
183
+ </div>
184
+ </div>
185
+ {/if}
186
+
187
+ <style lang="scss">
188
+ .fluent-panel-root {
189
+ position: fixed;
190
+ top: var(--panel-top, 0);
191
+ right: 0;
192
+ bottom: 0;
193
+ left: 0;
194
+ z-index: var(--fluent-z-modal, 1050);
195
+ // Never block the page — only the overlay and the panel surface
196
+ // (re-enabled below) capture pointer events.
197
+ pointer-events: none;
198
+ }
199
+
200
+ .fluent-panel-overlay {
201
+ position: absolute;
202
+ inset: 0;
203
+ background-color: rgba(0, 0, 0, 0);
204
+ cursor: pointer;
205
+ pointer-events: none;
206
+ transition: background-color 0.25s ease;
207
+ z-index: var(--fluent-z-modal-backdrop, 1040);
208
+ }
209
+
210
+ .fluent-panel {
211
+ position: absolute;
212
+ top: 0;
213
+ height: 100%;
214
+ max-width: 90vw;
215
+ background-color: var(--neutral-layer-1, #ffffff);
216
+ display: flex;
217
+ flex-direction: column;
218
+ overflow: hidden;
219
+ pointer-events: none;
220
+ transition: transform 0.25s ease, visibility 0.25s ease;
221
+ visibility: hidden;
222
+ z-index: calc(var(--fluent-z-modal, 1050) + 1);
223
+ }
224
+
225
+ // Closed-state per-side transforms — declared BEFORE the --open rule so
226
+ // the open rule wins on source order at equal specificity.
227
+
228
+ // Physical sides — never flip with direction.
229
+ .fluent-panel-root--right .fluent-panel {
230
+ right: 0;
231
+ box-shadow: -4px 0 16px rgba(0, 0, 0, 0.15);
232
+ transform: translateX(100%);
233
+ }
234
+
235
+ .fluent-panel-root--left .fluent-panel {
236
+ left: 0;
237
+ box-shadow: 4px 0 16px rgba(0, 0, 0, 0.15);
238
+ transform: translateX(-100%);
239
+ }
240
+
241
+ // Logical sides — follow the writing direction (start = left in LTR, right in RTL).
242
+ .fluent-panel-root--start .fluent-panel {
243
+ inset-inline-start: 0;
244
+ box-shadow: 4px 0 16px rgba(0, 0, 0, 0.15);
245
+ transform: translateX(-100%);
246
+ }
247
+
248
+ .fluent-panel-root--start.fluent-panel-root--rtl .fluent-panel {
249
+ inset-inline-start: unset;
250
+ right: 0;
251
+ box-shadow: -4px 0 16px rgba(0, 0, 0, 0.15);
252
+ transform: translateX(100%);
253
+ }
254
+
255
+ .fluent-panel-root--end .fluent-panel {
256
+ inset-inline-end: 0;
257
+ box-shadow: -4px 0 16px rgba(0, 0, 0, 0.15);
258
+ transform: translateX(100%);
259
+ }
260
+
261
+ .fluent-panel-root--end.fluent-panel-root--rtl .fluent-panel {
262
+ inset-inline-end: unset;
263
+ left: 0;
264
+ box-shadow: 4px 0 16px rgba(0, 0, 0, 0.15);
265
+ transform: translateX(-100%);
266
+ }
267
+
268
+ .fluent-panel-root--open .fluent-panel-overlay {
269
+ background-color: rgba(0, 0, 0, 0.3);
270
+ pointer-events: auto;
271
+ }
272
+
273
+ // Match the (0,3,0) specificity of the RTL side overrides so the open
274
+ // transform wins on source order (this rule is last).
275
+ .fluent-panel-root.fluent-panel-root--open .fluent-panel {
276
+ transform: translateX(0);
277
+ visibility: visible;
278
+ pointer-events: auto;
279
+ }
280
+
281
+ // Pinned: render in normal flow as a regular column. None of the
282
+ // floating-panel positioning, transforms, or visibility gymnastics apply
283
+ // — caller is responsible for placing the <aside> via parent layout.
284
+ //
285
+ // `z-index` is explicit (not auto) so pinned forms its own stacking
286
+ // context — keeps overlay drawers (z-index 1050+) and their backdrops
287
+ // (1040) reliably above any pinned drawer, regardless of paint order.
288
+ // Override via --fluent-z-panel-pinned on the consumer side if multiple
289
+ // pinned panels need to layer relative to each other.
290
+ .fluent-panel--pinned {
291
+ position: relative;
292
+ top: auto;
293
+ height: auto;
294
+ max-width: none;
295
+ transform: none;
296
+ visibility: visible;
297
+ pointer-events: auto;
298
+ transition: none;
299
+ flex-shrink: 0;
300
+ align-self: stretch;
301
+ box-shadow: none;
302
+ border-inline-end: 1px solid var(--neutral-stroke-layer-rest, #e0e0e0);
303
+ z-index: var(--fluent-z-panel-pinned, 1);
304
+ }
305
+
306
+ .fluent-panel--pinned-end,
307
+ .fluent-panel--pinned-right {
308
+ border-inline-end: none;
309
+ border-inline-start: 1px solid var(--neutral-stroke-layer-rest, #e0e0e0);
310
+ }
311
+ </style>
@@ -8,6 +8,7 @@ type Props = {
8
8
  overlay?: boolean;
9
9
  closeOnOutsideClick?: boolean;
10
10
  closeOnEscape?: boolean;
11
+ pinned?: boolean;
11
12
  onclose?: () => void;
12
13
  children?: SlotType;
13
14
  class?: string;