svelte-fluentui 1.2.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 +15 -16
- package/dist/assets/styles/fluent-ui/fluent-components.scss +0 -10
- package/dist/assets/styles/layout.scss +43 -5
- package/dist/components/QuickGrid.svelte.d.ts +7 -7
- package/dist/components/layout/Footer.svelte +29 -3
- package/dist/components/layout/Footer.svelte.d.ts +5 -0
- package/dist/components/layout/Panel.svelte +311 -0
- package/dist/components/layout/Panel.svelte.d.ts +19 -0
- package/dist/components/layout/TopNav.svelte +390 -268
- package/dist/components/layout/TopNav.svelte.d.ts +6 -0
- package/dist/components/layout/index.d.ts +1 -0
- package/dist/components/layout/index.js +1 -0
- package/dist/main.css +45 -16
- package/dist/main.css.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,22 +2,21 @@
|
|
|
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.
|
|
6
|
-
|
|
7
|
-
- **`
|
|
8
|
-
- **`
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
- **`
|
|
17
|
-
- **`
|
|
18
|
-
- **`
|
|
19
|
-
-
|
|
20
|
-
- **Playwright e2e suite** — New `playwright.config.ts` targets fixture pages under `docs/src/routes/test/<feature>/+page.svelte` (intentionally separate from showcase pages so specs stay stable). First feature covered is Autocomplete with 567 lines of specs across two fixture pages.
|
|
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
|
+
|
|
12
|
+
## What's New in v1.3.0
|
|
13
|
+
|
|
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`.
|
|
15
|
+
- **`TopNav` drawer rebuilt on `Panel`; container queries replace media queries** — the bespoke `.mobile-sidebar` div with hand-rolled `left: -280px` / `transition` CSS is gone, replaced with `<Panel side="start" width="280px" top="{height}px">` so Escape-close, portal, overlay outside-click, and `aria-modal` all come for free. The collapse breakpoint is now an `@container topnav (max-width: 960px)` query keyed on the nav's own width (aligns with Grid's `md`), so behavior is intrinsic to the bar regardless of where it's mounted. `items` auto-populate the drawer, the hamburger only renders when there's content to open, and the action slot now stays visible at every width.
|
|
16
|
+
- **`TopNav` — `brandTemplate`, `drawerContent`, and `collapse` snippets/props** — `brandTemplate` replaces the default `<a>` brand rendering so consumers can compose logo + wordmark + version badge as one unit; `drawerContent: Snippet<[() => void]>` renders custom content inside the mobile drawer and receives a `closeDrawer` callback so link clicks can dismiss the drawer before navigation; `collapse: "auto" | "always" | "never"` overrides the container-query state when the consumer drives sidebar visibility from its own responsive breakpoint. First demo page added at `/components/navigation-layout/topnav`.
|
|
17
|
+
- **`Footer` — `start` / `center` / `end` named snippets with a truly-centered grid layout** — Footer previously had a single `children` slot, so a "© left, brand center, version right" arrangement required inline `justify-content: space-between` and hoping the middle item floated where you wanted. The new snippets switch Footer into a CSS Grid (`1fr auto 1fr`) where the center column is genuinely centered regardless of side-section widths, with explicit `grid-column` placement so a missing section doesn't reflow the others. New `stack` prop (default `true`) collapses to a single inline-start column below 600px via a container query keyed on the footer itself. RTL flips `start` ↔ `end` automatically.
|
|
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`.
|
|
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).
|
|
21
20
|
|
|
22
21
|
## Features
|
|
23
22
|
|
|
@@ -1068,16 +1068,6 @@ div[dragged-over="true"] {
|
|
|
1068
1068
|
animation: blinker 1s linear infinite;
|
|
1069
1069
|
}
|
|
1070
1070
|
|
|
1071
|
-
/* _content/Microsoft.FluentUI.AspNetCore.Components/Components/Footer/FluentFooter.razor.rz.scp.css */
|
|
1072
|
-
.footer {
|
|
1073
|
-
display: flex;
|
|
1074
|
-
z-index: 10;
|
|
1075
|
-
flex-direction: row;
|
|
1076
|
-
font-family: var(--body-font);
|
|
1077
|
-
font-weight: normal;
|
|
1078
|
-
font-size: var(--type-ramp-minus-1-font-size);
|
|
1079
|
-
line-height: var(--type-ramp-minus-1-line-height);
|
|
1080
|
-
}
|
|
1081
1071
|
|
|
1082
1072
|
/* _content/Microsoft.FluentUI.AspNetCore.Components/Components/Forms/FluentValidationMessage.razor.rz.scp.css */
|
|
1083
1073
|
.validation-message {
|
|
@@ -35,12 +35,50 @@
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
// Footer component
|
|
38
|
-
.footer {
|
|
38
|
+
.fluent-footer {
|
|
39
39
|
display: flex;
|
|
40
40
|
align-items: center;
|
|
41
|
-
background-color: var(--neutral-layer-4);
|
|
42
|
-
color: var(--neutral-foreground-rest);
|
|
43
|
-
border-top: 1px solid var(--neutral-stroke-layer-rest);
|
|
44
|
-
padding: 0 1rem;
|
|
41
|
+
background-color: var(--fluent-footer-bg, var(--neutral-layer-4));
|
|
42
|
+
color: var(--fluent-footer-text, var(--neutral-foreground-rest));
|
|
43
|
+
border-top: 1px solid var(--fluent-footer-border, var(--neutral-stroke-layer-rest));
|
|
44
|
+
padding: 0.75rem 1rem;
|
|
45
|
+
min-height: 48px;
|
|
46
|
+
z-index: 10;
|
|
45
47
|
transition: background-color 0.3s ease, color 0.3s ease;
|
|
46
48
|
}
|
|
49
|
+
|
|
50
|
+
// Three-part mode: switch to CSS Grid so the center column is truly centered
|
|
51
|
+
// (1fr | auto | 1fr — side columns absorb equal slack regardless of widths).
|
|
52
|
+
// Explicit grid-column placement so a missing section doesn't shift the
|
|
53
|
+
// remaining ones into the wrong column.
|
|
54
|
+
.fluent-footer--three-part {
|
|
55
|
+
display: grid;
|
|
56
|
+
grid-template-columns: 1fr auto 1fr;
|
|
57
|
+
gap: 1rem;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.fluent-footer__start { grid-column: 1; justify-self: start; }
|
|
61
|
+
.fluent-footer__center { grid-column: 2; justify-self: center; }
|
|
62
|
+
.fluent-footer__end { grid-column: 3; justify-self: end; }
|
|
63
|
+
|
|
64
|
+
// Stacking on narrow widths — opt-in via the `stack` prop (default true).
|
|
65
|
+
// Container query keyed on the footer itself so behavior is intrinsic to
|
|
66
|
+
// the bar's width, not the viewport (mirrors TopNav's approach).
|
|
67
|
+
.fluent-footer--stacks {
|
|
68
|
+
container-type: inline-size;
|
|
69
|
+
container-name: fluent-footer;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@container fluent-footer (max-width: 600px) {
|
|
73
|
+
.fluent-footer--stacks {
|
|
74
|
+
grid-template-columns: 1fr;
|
|
75
|
+
gap: 0.5rem;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.fluent-footer--stacks .fluent-footer__start,
|
|
79
|
+
.fluent-footer--stacks .fluent-footer__center,
|
|
80
|
+
.fluent-footer--stacks .fluent-footer__end {
|
|
81
|
+
grid-column: 1;
|
|
82
|
+
justify-self: start;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -32,7 +32,7 @@ declare function $$render<T>(): {
|
|
|
32
32
|
filter?: ((filterValue: string, row: T) => boolean | null) | undefined;
|
|
33
33
|
isEditable?: boolean | ((row: T) => boolean) | undefined;
|
|
34
34
|
editor?: "number" | "select" | "text" | "custom" | "autocomplete" | "combobox" | "checkbox" | "date";
|
|
35
|
-
editTrigger?: "
|
|
35
|
+
editTrigger?: "click" | "dblclick" | "button" | "always" | "navigate";
|
|
36
36
|
editorOptions?: {
|
|
37
37
|
options?: {
|
|
38
38
|
[key: string]: unknown;
|
|
@@ -123,7 +123,7 @@ declare function $$render<T>(): {
|
|
|
123
123
|
style?: string;
|
|
124
124
|
cellTemplate?: SlotType;
|
|
125
125
|
editable?: boolean;
|
|
126
|
-
editTrigger?: "
|
|
126
|
+
editTrigger?: "click" | "dblclick" | "button" | "always" | "navigate";
|
|
127
127
|
dropdownShowOnFocus?: boolean;
|
|
128
128
|
isRowEditable?: boolean | ((row: T) => boolean) | undefined;
|
|
129
129
|
checkboxAlwaysEditable?: boolean;
|
|
@@ -201,7 +201,7 @@ declare function $$render<T>(): {
|
|
|
201
201
|
filter?: ((filterValue: string, row: T) => boolean | null) | undefined;
|
|
202
202
|
isEditable?: boolean | ((row: T) => boolean) | undefined;
|
|
203
203
|
editor?: "number" | "select" | "text" | "custom" | "autocomplete" | "combobox" | "checkbox" | "date";
|
|
204
|
-
editTrigger?: "
|
|
204
|
+
editTrigger?: "click" | "dblclick" | "button" | "always" | "navigate";
|
|
205
205
|
editorOptions?: {
|
|
206
206
|
options?: {
|
|
207
207
|
[key: string]: unknown;
|
|
@@ -310,7 +310,7 @@ declare function $$render<T>(): {
|
|
|
310
310
|
filter?: ((filterValue: string, row: T) => boolean | null) | undefined;
|
|
311
311
|
isEditable?: boolean | ((row: T) => boolean) | undefined;
|
|
312
312
|
editor?: "number" | "select" | "text" | "custom" | "autocomplete" | "combobox" | "checkbox" | "date";
|
|
313
|
-
editTrigger?: "
|
|
313
|
+
editTrigger?: "click" | "dblclick" | "button" | "always" | "navigate";
|
|
314
314
|
editorOptions?: {
|
|
315
315
|
options?: {
|
|
316
316
|
[key: string]: unknown;
|
|
@@ -418,7 +418,7 @@ declare function $$render<T>(): {
|
|
|
418
418
|
filter?: ((filterValue: string, row: T) => boolean | null) | undefined;
|
|
419
419
|
isEditable?: boolean | ((row: T) => boolean) | undefined;
|
|
420
420
|
editor?: "number" | "select" | "text" | "custom" | "autocomplete" | "combobox" | "checkbox" | "date";
|
|
421
|
-
editTrigger?: "
|
|
421
|
+
editTrigger?: "click" | "dblclick" | "button" | "always" | "navigate";
|
|
422
422
|
editorOptions?: {
|
|
423
423
|
options?: {
|
|
424
424
|
[key: string]: unknown;
|
|
@@ -529,7 +529,7 @@ declare function $$render<T>(): {
|
|
|
529
529
|
filter?: ((filterValue: string, row: T) => boolean | null) | undefined;
|
|
530
530
|
isEditable?: boolean | ((row: T) => boolean) | undefined;
|
|
531
531
|
editor?: "number" | "select" | "text" | "custom" | "autocomplete" | "combobox" | "checkbox" | "date";
|
|
532
|
-
editTrigger?: "
|
|
532
|
+
editTrigger?: "click" | "dblclick" | "button" | "always" | "navigate";
|
|
533
533
|
editorOptions?: {
|
|
534
534
|
options?: {
|
|
535
535
|
[key: string]: unknown;
|
|
@@ -638,7 +638,7 @@ declare function $$render<T>(): {
|
|
|
638
638
|
filter?: ((filterValue: string, row: T) => boolean | null) | undefined;
|
|
639
639
|
isEditable?: boolean | ((row: T) => boolean) | undefined;
|
|
640
640
|
editor?: "number" | "select" | "text" | "custom" | "autocomplete" | "combobox" | "checkbox" | "date";
|
|
641
|
-
editTrigger?: "
|
|
641
|
+
editTrigger?: "click" | "dblclick" | "button" | "always" | "navigate";
|
|
642
642
|
editorOptions?: {
|
|
643
643
|
options?: {
|
|
644
644
|
[key: string]: unknown;
|
|
@@ -2,20 +2,46 @@
|
|
|
2
2
|
import type {SlotType} from "../../types/index.js"
|
|
3
3
|
|
|
4
4
|
type Props = {
|
|
5
|
+
start?: SlotType
|
|
6
|
+
center?: SlotType
|
|
7
|
+
end?: SlotType
|
|
5
8
|
children?: SlotType
|
|
9
|
+
/** Stack to a single column below 600px container width. Only applies in 3-part mode. */
|
|
10
|
+
stack?: boolean
|
|
6
11
|
class?: string
|
|
7
12
|
style?: string
|
|
8
13
|
}
|
|
9
14
|
|
|
10
15
|
let {
|
|
16
|
+
start = undefined,
|
|
17
|
+
center = undefined,
|
|
18
|
+
end = undefined,
|
|
11
19
|
children = undefined,
|
|
20
|
+
stack = true,
|
|
12
21
|
class: className = "",
|
|
13
22
|
style = ""
|
|
14
23
|
}: Props = $props()
|
|
15
24
|
|
|
16
|
-
|
|
25
|
+
const hasThreeParts = $derived(!!start || !!center || !!end)
|
|
17
26
|
</script>
|
|
18
27
|
|
|
19
|
-
<footer
|
|
20
|
-
{
|
|
28
|
+
<footer
|
|
29
|
+
{style}
|
|
30
|
+
class="fluent-footer {className}"
|
|
31
|
+
class:fluent-footer--three-part={hasThreeParts}
|
|
32
|
+
class:fluent-footer--stacks={hasThreeParts && stack}
|
|
33
|
+
>
|
|
34
|
+
{#if hasThreeParts}
|
|
35
|
+
{#if start}
|
|
36
|
+
<div class="fluent-footer__start">{@render start()}</div>
|
|
37
|
+
{/if}
|
|
38
|
+
{#if center}
|
|
39
|
+
<div class="fluent-footer__center">{@render center()}</div>
|
|
40
|
+
{/if}
|
|
41
|
+
{#if end}
|
|
42
|
+
<div class="fluent-footer__end">{@render end()}</div>
|
|
43
|
+
{/if}
|
|
44
|
+
{:else}
|
|
45
|
+
{@render children?.()}
|
|
46
|
+
{/if}
|
|
21
47
|
</footer>
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { SlotType } from "../../types/index.js";
|
|
2
2
|
type Props = {
|
|
3
|
+
start?: SlotType;
|
|
4
|
+
center?: SlotType;
|
|
5
|
+
end?: SlotType;
|
|
3
6
|
children?: SlotType;
|
|
7
|
+
/** Stack to a single column below 600px container width. Only applies in 3-part mode. */
|
|
8
|
+
stack?: boolean;
|
|
4
9
|
class?: string;
|
|
5
10
|
style?: string;
|
|
6
11
|
};
|
|
@@ -0,0 +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
|
+
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>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { SlotType } from "../../types/index.js";
|
|
2
|
+
type Side = "left" | "right" | "start" | "end";
|
|
3
|
+
type Props = {
|
|
4
|
+
open?: boolean;
|
|
5
|
+
side?: Side;
|
|
6
|
+
width?: string;
|
|
7
|
+
top?: string;
|
|
8
|
+
overlay?: boolean;
|
|
9
|
+
closeOnOutsideClick?: boolean;
|
|
10
|
+
closeOnEscape?: boolean;
|
|
11
|
+
pinned?: boolean;
|
|
12
|
+
onclose?: () => void;
|
|
13
|
+
children?: SlotType;
|
|
14
|
+
class?: string;
|
|
15
|
+
style?: string;
|
|
16
|
+
};
|
|
17
|
+
declare const Panel: import("svelte").Component<Props, {}, "open">;
|
|
18
|
+
type Panel = ReturnType<typeof Panel>;
|
|
19
|
+
export default Panel;
|