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.
- package/README.md +12 -17
- package/dist/components/Badge.svelte +5 -1
- package/dist/components/Badge.svelte.d.ts +1 -0
- package/dist/components/QuickGrid.svelte.d.ts +2 -2
- package/dist/components/layout/Panel.svelte +311 -266
- package/dist/components/layout/Panel.svelte.d.ts +1 -0
- package/dist/components/layout/TopNav.svelte +412 -369
- package/dist/components/layout/TopNav.svelte.d.ts +4 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
wasOpen =
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
pointer-events: none;
|
|
206
|
-
transition:
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
box-shadow:
|
|
238
|
-
transform: translateX(100%);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
.fluent-panel-root.fluent-panel-root--
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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>
|