svelte-fluentui 1.3.0 → 1.3.2

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,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;