@placeholderco/placeholder-ui 2.1.1 → 2.2.0
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/dist/editors/CodeMirror.svelte +295 -297
- package/dist/editors/tiptap/LinkActionMenu.svelte +6 -1
- package/dist/editors/tiptap/extensions/Alert.js +17 -17
- package/dist/editors/tiptap/extensions/Div.js +19 -19
- package/dist/editors/tiptap/extensions/Span.js +14 -14
- package/dist/editors/tiptap/toolbar/AlertButton.svelte +254 -253
- package/dist/editors/tiptap/toolbar/AlignCenterButton.svelte +11 -11
- package/dist/editors/tiptap/toolbar/AlignJustifyButton.svelte +11 -11
- package/dist/editors/tiptap/toolbar/AlignLeftButton.svelte +11 -11
- package/dist/editors/tiptap/toolbar/AlignRightButton.svelte +11 -11
- package/dist/editors/tiptap/toolbar/BoldButton.svelte +11 -11
- package/dist/editors/tiptap/toolbar/BulletListButton.svelte +11 -11
- package/dist/editors/tiptap/toolbar/CodeButton.svelte +11 -11
- package/dist/editors/tiptap/toolbar/ColorButton.svelte +265 -265
- package/dist/editors/tiptap/toolbar/DocumentButton.svelte +51 -51
- package/dist/editors/tiptap/toolbar/HeadingButton.svelte +21 -21
- package/dist/editors/tiptap/toolbar/HtmlButton.svelte +48 -52
- package/dist/editors/tiptap/toolbar/ImageButton.svelte +35 -39
- package/dist/editors/tiptap/toolbar/ItalicButton.svelte +11 -11
- package/dist/editors/tiptap/toolbar/LinkButton.svelte +249 -249
- package/dist/editors/tiptap/toolbar/OrderedListButton.svelte +11 -11
- package/dist/editors/tiptap/toolbar/RedoButton.svelte +11 -11
- package/dist/editors/tiptap/toolbar/StrikethroughButton.svelte +11 -11
- package/dist/editors/tiptap/toolbar/SubscriptButton.svelte +11 -11
- package/dist/editors/tiptap/toolbar/SuperscriptButton.svelte +11 -11
- package/dist/editors/tiptap/toolbar/TableButton.svelte +231 -224
- package/dist/editors/tiptap/toolbar/ToolbarButton.svelte +59 -65
- package/dist/editors/tiptap/toolbar/UnderlineButton.svelte +11 -11
- package/dist/editors/tiptap/toolbar/UndoButton.svelte +11 -11
- package/dist/form/Autocomplete.svelte +24 -22
- package/dist/form/AutocompleteMulti.svelte +24 -22
- package/dist/form/Checkbox.svelte +4 -4
- package/dist/form/Chips.svelte +15 -7
- package/dist/form/ColorPicker.svelte +5 -20
- package/dist/form/ComboBox.svelte +4 -4
- package/dist/form/ComboBoxItemBuilder.svelte +14 -32
- package/dist/form/ComboBoxMulti.svelte +7 -3
- package/dist/form/CronBuilder.svelte +32 -110
- package/dist/form/DatePicker.svelte +9 -28
- package/dist/form/DateRangePicker.svelte +46 -36
- package/dist/form/DateTimePicker.svelte +11 -30
- package/dist/form/Number.svelte +1 -3
- package/dist/form/RadioGroup.svelte +1 -3
- package/dist/form/Select.svelte +25 -28
- package/dist/form/SelectMulti.svelte +36 -38
- package/dist/form/StringArrayBuilder.svelte +1 -3
- package/dist/form/TextArea.svelte +5 -6
- package/dist/form/Textbox.svelte +10 -4
- package/dist/form/TimePicker.svelte +10 -4
- package/dist/icon/Icon.svelte +5 -1
- package/dist/index.d.ts +147 -146
- package/dist/index.js +78 -77
- package/dist/layout/CustomNavbar.svelte +1 -1
- package/dist/layout/NavbarItemDisplay.svelte +1 -1
- package/dist/layout/NavbarItemView.svelte +3 -1
- package/dist/layout/Sidenav.svelte +1 -1
- package/dist/models/ComboBoxItem.d.ts +4 -4
- package/dist/models/NotifyModel.js +0 -1
- package/dist/theme.svelte.d.ts +1 -1
- package/dist/theme.svelte.js +46 -22
- package/dist/ui/Accordion.svelte +1 -3
- package/dist/ui/Badge.svelte +4 -5
- package/dist/ui/Button.svelte +7 -1
- package/dist/ui/ContextMenu.svelte +5 -11
- package/dist/ui/Dialog.svelte +16 -3
- package/dist/ui/Dialog.svelte.d.ts +7 -1
- package/dist/ui/Dropdown.svelte +21 -10
- package/dist/ui/MultiSortable.svelte +1 -3
- package/dist/ui/Pagination.svelte +2 -2
- package/dist/ui/Popover.svelte +16 -37
- package/dist/ui/ProgressBar.svelte +1 -4
- package/dist/ui/ThemeSwitcher.svelte +2 -1
- package/dist/ui/Tooltip.svelte +1 -37
- package/dist/util/DateFunctions.js +1 -1
- package/dist/util/Floating.d.ts +70 -0
- package/dist/util/Floating.js +321 -0
- package/dist/util/NavigateTo.js +2 -2
- package/dist/util/Transitions.d.ts +1 -1
- package/dist/util/Transitions.js +1 -1
- package/package.json +1 -1
package/dist/ui/Popover.svelte
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import type { Snippet } from 'svelte';
|
|
4
4
|
import Button from './Button.svelte';
|
|
5
5
|
import { clickOutside } from '../util/ClickOutside.js';
|
|
6
|
+
import { floating } from '../util/Floating.js';
|
|
6
7
|
|
|
7
8
|
type PopoverPosition = 'top' | 'bottom' | 'left' | 'right';
|
|
8
9
|
type PopoverTrigger = 'click' | 'hover';
|
|
@@ -40,6 +41,7 @@
|
|
|
40
41
|
hoverDelay = 1000
|
|
41
42
|
}: PopoverProps = $props();
|
|
42
43
|
|
|
44
|
+
let triggerElement: HTMLElement | undefined = $state(undefined);
|
|
43
45
|
let hoverTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
44
46
|
let isHovering = false;
|
|
45
47
|
|
|
@@ -99,7 +101,8 @@
|
|
|
99
101
|
{#if button}
|
|
100
102
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
101
103
|
<!-- svelte-ignore a11y_mouse_events_have_key_events -->
|
|
102
|
-
<button
|
|
104
|
+
<button
|
|
105
|
+
bind:this={triggerElement}
|
|
103
106
|
onclick={handleClick}
|
|
104
107
|
onmouseenter={handleMouseEnter}
|
|
105
108
|
onmouseleave={handleMouseLeave}
|
|
@@ -110,6 +113,7 @@
|
|
|
110
113
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
111
114
|
<!-- svelte-ignore a11y_mouse_events_have_key_events -->
|
|
112
115
|
<div
|
|
116
|
+
bind:this={triggerElement}
|
|
113
117
|
onmouseenter={handleMouseEnter}
|
|
114
118
|
onmouseleave={handleMouseLeave}
|
|
115
119
|
>
|
|
@@ -122,8 +126,12 @@
|
|
|
122
126
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
123
127
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
124
128
|
<!-- svelte-ignore a11y_mouse_events_have_key_events -->
|
|
129
|
+
<!-- Rendered in the top layer so a scrolling Dialog body cannot clip it.
|
|
130
|
+
data-placement (set by use:floating, after any flip) drives the arrow. -->
|
|
125
131
|
<div
|
|
126
|
-
class="popover-content
|
|
132
|
+
class="popover-content"
|
|
133
|
+
data-placement={position}
|
|
134
|
+
use:floating={{ anchor: () => triggerElement, placement: position, offset: 6 }}
|
|
127
135
|
onclick={(e) => e.stopPropagation()}
|
|
128
136
|
onmouseenter={handleContentMouseEnter}
|
|
129
137
|
onmouseleave={handleContentMouseLeave}
|
|
@@ -140,9 +148,9 @@
|
|
|
140
148
|
position: relative;
|
|
141
149
|
}
|
|
142
150
|
|
|
151
|
+
/* Position and top-layer promotion come from use:floating */
|
|
143
152
|
.popover-content {
|
|
144
153
|
display: block;
|
|
145
|
-
position: absolute;
|
|
146
154
|
background-color: var(--paper-body-bg);
|
|
147
155
|
border: 1px solid var(--border-color);
|
|
148
156
|
min-width: 160px;
|
|
@@ -151,64 +159,35 @@
|
|
|
151
159
|
border-radius: 4px;
|
|
152
160
|
}
|
|
153
161
|
|
|
154
|
-
/*
|
|
155
|
-
.popover-bottom {
|
|
156
|
-
top: 100%;
|
|
157
|
-
left: 50%;
|
|
158
|
-
transform: translateX(-50%);
|
|
159
|
-
margin-top: 4px;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
.popover-top {
|
|
163
|
-
bottom: 100%;
|
|
164
|
-
left: 50%;
|
|
165
|
-
transform: translateX(-50%);
|
|
166
|
-
margin-bottom: 4px;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
.popover-right {
|
|
170
|
-
top: 50%;
|
|
171
|
-
left: 100%;
|
|
172
|
-
transform: translateY(-50%);
|
|
173
|
-
margin-left: 4px;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
.popover-left {
|
|
177
|
-
top: 50%;
|
|
178
|
-
right: 100%;
|
|
179
|
-
transform: translateY(-50%);
|
|
180
|
-
margin-right: 4px;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/* Arrow indicators for each position */
|
|
162
|
+
/* Arrow indicators for each resolved placement */
|
|
184
163
|
.popover-content::before {
|
|
185
164
|
content: '';
|
|
186
165
|
position: absolute;
|
|
187
166
|
border: 6px solid transparent;
|
|
188
167
|
}
|
|
189
168
|
|
|
190
|
-
.popover-bottom::before {
|
|
169
|
+
.popover-content[data-placement='bottom']::before {
|
|
191
170
|
top: -12px;
|
|
192
171
|
left: 50%;
|
|
193
172
|
transform: translateX(-50%);
|
|
194
173
|
border-bottom-color: var(--border-color);
|
|
195
174
|
}
|
|
196
175
|
|
|
197
|
-
.popover-top::before {
|
|
176
|
+
.popover-content[data-placement='top']::before {
|
|
198
177
|
bottom: -12px;
|
|
199
178
|
left: 50%;
|
|
200
179
|
transform: translateX(-50%);
|
|
201
180
|
border-top-color: var(--border-color);
|
|
202
181
|
}
|
|
203
182
|
|
|
204
|
-
.popover-right::before {
|
|
183
|
+
.popover-content[data-placement='right']::before {
|
|
205
184
|
top: 50%;
|
|
206
185
|
left: -12px;
|
|
207
186
|
transform: translateY(-50%);
|
|
208
187
|
border-right-color: var(--border-color);
|
|
209
188
|
}
|
|
210
189
|
|
|
211
|
-
.popover-left::before {
|
|
190
|
+
.popover-content[data-placement='left']::before {
|
|
212
191
|
top: 50%;
|
|
213
192
|
right: -12px;
|
|
214
193
|
transform: translateY(-50%);
|
|
@@ -63,10 +63,7 @@
|
|
|
63
63
|
</div>
|
|
64
64
|
{/each}
|
|
65
65
|
{:else}
|
|
66
|
-
<div
|
|
67
|
-
class="fill {variant} {animated ? 'animated' : ''}"
|
|
68
|
-
style="width: {clampedValue}%"
|
|
69
|
-
>
|
|
66
|
+
<div class="fill {variant} {animated ? 'animated' : ''}" style="width: {clampedValue}%">
|
|
70
67
|
{#if label}
|
|
71
68
|
<span class="label">{label}</span>
|
|
72
69
|
{/if}
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
darkVariant?: ButtonVariant;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
let { lightVariant = 'primary-subtle', darkVariant = 'accent-subtle' }: ThemeSwitcherProps =
|
|
17
|
+
let { lightVariant = 'primary-subtle', darkVariant = 'accent-subtle' }: ThemeSwitcherProps =
|
|
18
|
+
$props();
|
|
18
19
|
|
|
19
20
|
const themeCycle = ['light', 'dark', 'system'] as const;
|
|
20
21
|
|
package/dist/ui/Tooltip.svelte
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import { fade } from 'svelte/transition';
|
|
4
|
+
import { findFixedContainingBlock } from '../util/Floating.js';
|
|
4
5
|
|
|
5
6
|
export type TooltipLocation =
|
|
6
7
|
| 'top'
|
|
@@ -140,43 +141,6 @@
|
|
|
140
141
|
return Math.max(min, Math.min(value, max));
|
|
141
142
|
}
|
|
142
143
|
|
|
143
|
-
/**
|
|
144
|
-
* A transform, filter, perspective, containment or container-type on an ancestor
|
|
145
|
-
* makes that ancestor the containing block for `position: fixed` descendants —
|
|
146
|
-
* left/top stop meaning "from the viewport" while getBoundingClientRect() keeps
|
|
147
|
-
* reporting viewport coordinates. Dialog hits this: its open animation uses a
|
|
148
|
-
* `forwards` fill, which leaves `transform: translateY(0)` applied while open.
|
|
149
|
-
*/
|
|
150
|
-
function findFixedContainingBlock(element: HTMLElement): HTMLElement | null {
|
|
151
|
-
let node = element.parentElement;
|
|
152
|
-
|
|
153
|
-
while (node) {
|
|
154
|
-
const style = getComputedStyle(node);
|
|
155
|
-
// Newer properties go through getPropertyValue so an older DOM lib (or a
|
|
156
|
-
// browser without support) reports '' instead of failing to compile.
|
|
157
|
-
const containerType = style.getPropertyValue('container-type');
|
|
158
|
-
const backdropFilter =
|
|
159
|
-
style.getPropertyValue('backdrop-filter') ||
|
|
160
|
-
style.getPropertyValue('-webkit-backdrop-filter');
|
|
161
|
-
|
|
162
|
-
if (
|
|
163
|
-
style.transform !== 'none' ||
|
|
164
|
-
style.perspective !== 'none' ||
|
|
165
|
-
style.filter !== 'none' ||
|
|
166
|
-
(backdropFilter !== '' && backdropFilter !== 'none') ||
|
|
167
|
-
(containerType !== '' && containerType !== 'normal') ||
|
|
168
|
-
/\b(paint|layout|strict|content)\b/.test(style.contain) ||
|
|
169
|
-
/\b(transform|perspective|filter)\b/.test(style.willChange)
|
|
170
|
-
) {
|
|
171
|
-
return node;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
node = node.parentElement;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
144
|
/** Viewport coordinates of the origin that left/top are measured from. */
|
|
181
145
|
function containingBlockOrigin(): { x: number; y: number } {
|
|
182
146
|
if (!fixedContainer) return { x: 0, y: 0 };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Floating panel positioning for dropdowns, pickers and popovers.
|
|
3
|
+
*
|
|
4
|
+
* Panels are promoted to the browser's top layer with the Popover API
|
|
5
|
+
* (`popover="manual"` + `showPopover()`), so they are never clipped by an
|
|
6
|
+
* `overflow: auto` ancestor — a scrolling Dialog body in particular — and they
|
|
7
|
+
* paint above modal dialogs without any z-index coordination. The panel stays
|
|
8
|
+
* where it is in the DOM, so focus, keyboard handling and click-outside logic
|
|
9
|
+
* keep working; only rendering moves.
|
|
10
|
+
*
|
|
11
|
+
* Browsers without the Popover API fall back to `position: fixed`, correcting
|
|
12
|
+
* for any transformed ancestor that would otherwise become the containing block.
|
|
13
|
+
*/
|
|
14
|
+
export type FloatingSide = 'top' | 'bottom' | 'left' | 'right';
|
|
15
|
+
export type FloatingAlignment = 'start' | 'end';
|
|
16
|
+
export type FloatingPlacement = FloatingSide | `${FloatingSide}-${FloatingAlignment}`;
|
|
17
|
+
export interface FloatingOptions {
|
|
18
|
+
/** Element the panel is positioned against. A getter defers resolution until layout. */
|
|
19
|
+
anchor: HTMLElement | null | undefined | (() => HTMLElement | null | undefined);
|
|
20
|
+
/** Preferred side and alignment relative to the anchor. Default `bottom-start`. */
|
|
21
|
+
placement?: FloatingPlacement;
|
|
22
|
+
/** Gap between anchor and panel in px. Default 0. */
|
|
23
|
+
offset?: number;
|
|
24
|
+
/** Give the panel the anchor's width. Default false. */
|
|
25
|
+
matchWidth?: boolean;
|
|
26
|
+
/** Flip to the opposite side when the preferred side has no room. Default true. */
|
|
27
|
+
flip?: boolean;
|
|
28
|
+
/** Slide along the anchor's edge to stay inside the viewport. Default true. */
|
|
29
|
+
shift?: boolean;
|
|
30
|
+
/** Minimum distance from the viewport edges in px. Default 8. */
|
|
31
|
+
padding?: number;
|
|
32
|
+
/** Notified with the resolved placement after each positioning pass. */
|
|
33
|
+
onPlacement?: (placement: FloatingPlacement) => void;
|
|
34
|
+
}
|
|
35
|
+
export declare function supportsPopover(): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Promote an element to the top layer for as long as it is mounted. The element
|
|
38
|
+
* keeps its DOM position; only its rendering escapes overflow and transform
|
|
39
|
+
* ancestors. Falls back to `position: fixed` where the Popover API is missing.
|
|
40
|
+
*/
|
|
41
|
+
export declare function topLayer(node: HTMLElement): {
|
|
42
|
+
destroy(): void;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* A transform, filter, perspective, containment or container-type on an ancestor
|
|
46
|
+
* makes that ancestor the containing block for `position: fixed` descendants.
|
|
47
|
+
* Dialog hits this: its open animation uses a `forwards` fill, leaving a
|
|
48
|
+
* transform applied while open. Only relevant to the non-popover fallback —
|
|
49
|
+
* top-layer elements always resolve against the viewport.
|
|
50
|
+
*/
|
|
51
|
+
export declare function findFixedContainingBlock(element: HTMLElement): HTMLElement | null;
|
|
52
|
+
/**
|
|
53
|
+
* Svelte action: keep `node` anchored to another element while it is mounted.
|
|
54
|
+
*
|
|
55
|
+
* ```svelte
|
|
56
|
+
* {#if open}
|
|
57
|
+
* <div use:floating={{ anchor: () => triggerEl, placement: 'bottom-start', matchWidth: true }}>
|
|
58
|
+
* ...
|
|
59
|
+
* </div>
|
|
60
|
+
* {/if}
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* The node receives `data-placement` with the resolved placement (after any
|
|
64
|
+
* flip) and a `--ui-floating-max-height` custom property holding the space
|
|
65
|
+
* available on the chosen side, which descendants can use to cap their height.
|
|
66
|
+
*/
|
|
67
|
+
export declare function floating(node: HTMLElement, options: FloatingOptions): {
|
|
68
|
+
update(next: FloatingOptions): void;
|
|
69
|
+
destroy(): void;
|
|
70
|
+
};
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Floating panel positioning for dropdowns, pickers and popovers.
|
|
3
|
+
*
|
|
4
|
+
* Panels are promoted to the browser's top layer with the Popover API
|
|
5
|
+
* (`popover="manual"` + `showPopover()`), so they are never clipped by an
|
|
6
|
+
* `overflow: auto` ancestor — a scrolling Dialog body in particular — and they
|
|
7
|
+
* paint above modal dialogs without any z-index coordination. The panel stays
|
|
8
|
+
* where it is in the DOM, so focus, keyboard handling and click-outside logic
|
|
9
|
+
* keep working; only rendering moves.
|
|
10
|
+
*
|
|
11
|
+
* Browsers without the Popover API fall back to `position: fixed`, correcting
|
|
12
|
+
* for any transformed ancestor that would otherwise become the containing block.
|
|
13
|
+
*/
|
|
14
|
+
const CLASS = 'ui-floating';
|
|
15
|
+
const STYLE_ID = 'ui-floating-style';
|
|
16
|
+
const OPPOSITE = {
|
|
17
|
+
top: 'bottom',
|
|
18
|
+
bottom: 'top',
|
|
19
|
+
left: 'right',
|
|
20
|
+
right: 'left'
|
|
21
|
+
};
|
|
22
|
+
export function supportsPopover() {
|
|
23
|
+
return typeof HTMLElement !== 'undefined' && 'showPopover' in HTMLElement.prototype;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Zero-specificity resets (`:where`) so component styles on the panel always win.
|
|
27
|
+
* The UA stylesheet gives `[popover]` a centred inset, a border, padding and a
|
|
28
|
+
* canvas background; all of that is undone here so the panel looks exactly as
|
|
29
|
+
* it did before being promoted.
|
|
30
|
+
*/
|
|
31
|
+
function ensureStyles() {
|
|
32
|
+
if (typeof document === 'undefined' || document.getElementById(STYLE_ID))
|
|
33
|
+
return;
|
|
34
|
+
const style = document.createElement('style');
|
|
35
|
+
style.id = STYLE_ID;
|
|
36
|
+
style.textContent = `
|
|
37
|
+
:where(.${CLASS}) { position: fixed; top: 0; left: 0; }
|
|
38
|
+
:where(.${CLASS}[popover]) {
|
|
39
|
+
inset: auto; margin: 0; padding: 0; border: none;
|
|
40
|
+
background: transparent; color: inherit; overflow: visible;
|
|
41
|
+
width: auto; height: auto; max-width: none; max-height: none;
|
|
42
|
+
}
|
|
43
|
+
`;
|
|
44
|
+
document.head.appendChild(style);
|
|
45
|
+
}
|
|
46
|
+
function isPopoverOpen(node) {
|
|
47
|
+
try {
|
|
48
|
+
return node.matches(':popover-open');
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Promote an element to the top layer for as long as it is mounted. The element
|
|
56
|
+
* keeps its DOM position; only its rendering escapes overflow and transform
|
|
57
|
+
* ancestors. Falls back to `position: fixed` where the Popover API is missing.
|
|
58
|
+
*/
|
|
59
|
+
export function topLayer(node) {
|
|
60
|
+
ensureStyles();
|
|
61
|
+
node.classList.add(CLASS);
|
|
62
|
+
const popover = supportsPopover();
|
|
63
|
+
if (popover) {
|
|
64
|
+
node.setAttribute('popover', 'manual');
|
|
65
|
+
try {
|
|
66
|
+
if (!isPopoverOpen(node))
|
|
67
|
+
node.showPopover();
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
/* not connected yet, or already shown */
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
destroy() {
|
|
75
|
+
if (popover && node.isConnected && isPopoverOpen(node)) {
|
|
76
|
+
try {
|
|
77
|
+
node.hidePopover();
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
/* already hidden */
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* A transform, filter, perspective, containment or container-type on an ancestor
|
|
88
|
+
* makes that ancestor the containing block for `position: fixed` descendants.
|
|
89
|
+
* Dialog hits this: its open animation uses a `forwards` fill, leaving a
|
|
90
|
+
* transform applied while open. Only relevant to the non-popover fallback —
|
|
91
|
+
* top-layer elements always resolve against the viewport.
|
|
92
|
+
*/
|
|
93
|
+
export function findFixedContainingBlock(element) {
|
|
94
|
+
let node = element.parentElement;
|
|
95
|
+
while (node) {
|
|
96
|
+
if (isPopoverOpen(node))
|
|
97
|
+
return null;
|
|
98
|
+
const style = getComputedStyle(node);
|
|
99
|
+
const containerType = style.getPropertyValue('container-type');
|
|
100
|
+
const backdropFilter = style.getPropertyValue('backdrop-filter') ||
|
|
101
|
+
style.getPropertyValue('-webkit-backdrop-filter');
|
|
102
|
+
if (style.transform !== 'none' ||
|
|
103
|
+
style.perspective !== 'none' ||
|
|
104
|
+
style.filter !== 'none' ||
|
|
105
|
+
(backdropFilter !== '' && backdropFilter !== 'none') ||
|
|
106
|
+
(containerType !== '' && containerType !== 'normal') ||
|
|
107
|
+
/\b(paint|layout|strict|content)\b/.test(style.contain) ||
|
|
108
|
+
/\b(transform|perspective|filter)\b/.test(style.willChange)) {
|
|
109
|
+
return node;
|
|
110
|
+
}
|
|
111
|
+
node = node.parentElement;
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
/** Ancestors that clip their content; scrolling one of them can hide the anchor. */
|
|
116
|
+
function findClippingAncestors(element) {
|
|
117
|
+
const result = [];
|
|
118
|
+
let node = element.parentElement;
|
|
119
|
+
while (node && node !== document.body) {
|
|
120
|
+
const style = getComputedStyle(node);
|
|
121
|
+
if (style.overflow !== 'visible' ||
|
|
122
|
+
style.overflowX !== 'visible' ||
|
|
123
|
+
style.overflowY !== 'visible') {
|
|
124
|
+
result.push(node);
|
|
125
|
+
}
|
|
126
|
+
node = node.parentElement;
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
/** Whether any part of the anchor is still visible through its clipping ancestors. */
|
|
131
|
+
function anchorIsVisible(rect, clippers) {
|
|
132
|
+
let { top, bottom, left, right } = rect;
|
|
133
|
+
for (const clipper of clippers) {
|
|
134
|
+
const c = clipper.getBoundingClientRect();
|
|
135
|
+
top = Math.max(top, c.top);
|
|
136
|
+
bottom = Math.min(bottom, c.bottom);
|
|
137
|
+
left = Math.max(left, c.left);
|
|
138
|
+
right = Math.min(right, c.right);
|
|
139
|
+
if (bottom <= top || right <= left)
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
function containingBlockOrigin(container) {
|
|
145
|
+
if (!container)
|
|
146
|
+
return { x: 0, y: 0 };
|
|
147
|
+
const rect = container.getBoundingClientRect();
|
|
148
|
+
const style = getComputedStyle(container);
|
|
149
|
+
// Fixed descendants resolve against the padding box, so drop the border.
|
|
150
|
+
return {
|
|
151
|
+
x: rect.left + (parseFloat(style.borderLeftWidth) || 0),
|
|
152
|
+
y: rect.top + (parseFloat(style.borderTopWidth) || 0)
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function splitPlacement(placement) {
|
|
156
|
+
const [side, alignment] = placement.split('-');
|
|
157
|
+
return [side, alignment];
|
|
158
|
+
}
|
|
159
|
+
function joinPlacement(side, alignment) {
|
|
160
|
+
return alignment ? `${side}-${alignment}` : side;
|
|
161
|
+
}
|
|
162
|
+
function clamp(value, min, max) {
|
|
163
|
+
return Math.max(min, Math.min(value, max));
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Svelte action: keep `node` anchored to another element while it is mounted.
|
|
167
|
+
*
|
|
168
|
+
* ```svelte
|
|
169
|
+
* {#if open}
|
|
170
|
+
* <div use:floating={{ anchor: () => triggerEl, placement: 'bottom-start', matchWidth: true }}>
|
|
171
|
+
* ...
|
|
172
|
+
* </div>
|
|
173
|
+
* {/if}
|
|
174
|
+
* ```
|
|
175
|
+
*
|
|
176
|
+
* The node receives `data-placement` with the resolved placement (after any
|
|
177
|
+
* flip) and a `--ui-floating-max-height` custom property holding the space
|
|
178
|
+
* available on the chosen side, which descendants can use to cap their height.
|
|
179
|
+
*/
|
|
180
|
+
export function floating(node, options) {
|
|
181
|
+
let opts = options;
|
|
182
|
+
let rafId = null;
|
|
183
|
+
let observedAnchor = null;
|
|
184
|
+
let clippers = [];
|
|
185
|
+
let clippersFor = null;
|
|
186
|
+
const layer = topLayer(node);
|
|
187
|
+
const usingPopover = supportsPopover();
|
|
188
|
+
// Walking the ancestor chain with getComputedStyle is too costly to repeat on
|
|
189
|
+
// every scroll frame, so resolve the containing block once per mount.
|
|
190
|
+
const fixedContainer = usingPopover ? null : findFixedContainingBlock(node);
|
|
191
|
+
function resolveAnchor() {
|
|
192
|
+
const a = opts.anchor;
|
|
193
|
+
const el = typeof a === 'function' ? a() : a;
|
|
194
|
+
return el ?? null;
|
|
195
|
+
}
|
|
196
|
+
function position() {
|
|
197
|
+
const anchor = resolveAnchor();
|
|
198
|
+
if (!anchor || !node.isConnected)
|
|
199
|
+
return;
|
|
200
|
+
const placement = opts.placement ?? 'bottom-start';
|
|
201
|
+
const offset = opts.offset ?? 0;
|
|
202
|
+
const padding = opts.padding ?? 8;
|
|
203
|
+
const flip = opts.flip ?? true;
|
|
204
|
+
const shift = opts.shift ?? true;
|
|
205
|
+
const a = anchor.getBoundingClientRect();
|
|
206
|
+
// A panel floating in the top layer is not clipped along with its anchor,
|
|
207
|
+
// so hide it once the anchor is scrolled out of a clipping ancestor (a
|
|
208
|
+
// Dialog body, for instance) rather than leaving it stranded over the
|
|
209
|
+
// dialog chrome.
|
|
210
|
+
if (clippersFor !== anchor) {
|
|
211
|
+
clippers = findClippingAncestors(anchor);
|
|
212
|
+
clippersFor = anchor;
|
|
213
|
+
}
|
|
214
|
+
node.style.visibility = anchorIsVisible(a, clippers) ? '' : 'hidden';
|
|
215
|
+
// Size before measuring so the measured box is the one that gets placed.
|
|
216
|
+
if (opts.matchWidth)
|
|
217
|
+
node.style.width = `${a.width}px`;
|
|
218
|
+
const n = node.getBoundingClientRect();
|
|
219
|
+
// A zero-sized box means nothing is laid out yet; the ResizeObserver will
|
|
220
|
+
// call again once content arrives.
|
|
221
|
+
if (n.width === 0 && n.height === 0)
|
|
222
|
+
return;
|
|
223
|
+
// clientWidth/Height exclude scrollbars, which window.innerWidth includes.
|
|
224
|
+
const vw = document.documentElement.clientWidth;
|
|
225
|
+
const vh = document.documentElement.clientHeight;
|
|
226
|
+
const [preferredSide, alignment] = splitPlacement(placement);
|
|
227
|
+
const space = {
|
|
228
|
+
top: a.top - padding,
|
|
229
|
+
bottom: vh - a.bottom - padding,
|
|
230
|
+
left: a.left - padding,
|
|
231
|
+
right: vw - a.right - padding
|
|
232
|
+
};
|
|
233
|
+
const vertical = preferredSide === 'top' || preferredSide === 'bottom';
|
|
234
|
+
const required = (vertical ? n.height : n.width) + offset;
|
|
235
|
+
// Flip only when the preferred side cannot fit *and* the opposite side is
|
|
236
|
+
// roomier; flipping into an equally cramped side just moves the problem.
|
|
237
|
+
let side = preferredSide;
|
|
238
|
+
if (flip && space[side] < required && space[OPPOSITE[side]] > space[side]) {
|
|
239
|
+
side = OPPOSITE[side];
|
|
240
|
+
}
|
|
241
|
+
let left = 0;
|
|
242
|
+
let top = 0;
|
|
243
|
+
if (side === 'top' || side === 'bottom') {
|
|
244
|
+
top = side === 'bottom' ? a.bottom + offset : a.top - n.height - offset;
|
|
245
|
+
if (alignment === 'start')
|
|
246
|
+
left = a.left;
|
|
247
|
+
else if (alignment === 'end')
|
|
248
|
+
left = a.right - n.width;
|
|
249
|
+
else
|
|
250
|
+
left = a.left + a.width / 2 - n.width / 2;
|
|
251
|
+
if (shift)
|
|
252
|
+
left = clamp(left, padding, Math.max(padding, vw - n.width - padding));
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
left = side === 'right' ? a.right + offset : a.left - n.width - offset;
|
|
256
|
+
if (alignment === 'start')
|
|
257
|
+
top = a.top;
|
|
258
|
+
else if (alignment === 'end')
|
|
259
|
+
top = a.bottom - n.height;
|
|
260
|
+
else
|
|
261
|
+
top = a.top + a.height / 2 - n.height / 2;
|
|
262
|
+
if (shift)
|
|
263
|
+
top = clamp(top, padding, Math.max(padding, vh - n.height - padding));
|
|
264
|
+
}
|
|
265
|
+
const available = side === 'bottom'
|
|
266
|
+
? vh - top - padding
|
|
267
|
+
: side === 'top'
|
|
268
|
+
? a.top - offset - padding
|
|
269
|
+
: vh - padding * 2;
|
|
270
|
+
const origin = containingBlockOrigin(fixedContainer);
|
|
271
|
+
node.style.left = `${Math.round(left - origin.x)}px`;
|
|
272
|
+
node.style.top = `${Math.round(top - origin.y)}px`;
|
|
273
|
+
node.style.setProperty('--ui-floating-max-height', `${Math.max(0, Math.floor(available))}px`);
|
|
274
|
+
const resolved = joinPlacement(side, alignment);
|
|
275
|
+
if (node.dataset.placement !== resolved) {
|
|
276
|
+
node.dataset.placement = resolved;
|
|
277
|
+
opts.onPlacement?.(resolved);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/** Coalesce bursts of scroll/resize events into one reposition per frame. */
|
|
281
|
+
function schedule() {
|
|
282
|
+
if (rafId !== null)
|
|
283
|
+
return;
|
|
284
|
+
rafId = requestAnimationFrame(() => {
|
|
285
|
+
rafId = null;
|
|
286
|
+
position();
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
const observer = new ResizeObserver(() => position());
|
|
290
|
+
observer.observe(node);
|
|
291
|
+
function observeAnchor() {
|
|
292
|
+
const anchor = resolveAnchor();
|
|
293
|
+
if (anchor === observedAnchor)
|
|
294
|
+
return;
|
|
295
|
+
if (observedAnchor)
|
|
296
|
+
observer.unobserve(observedAnchor);
|
|
297
|
+
observedAnchor = anchor;
|
|
298
|
+
if (anchor)
|
|
299
|
+
observer.observe(anchor);
|
|
300
|
+
}
|
|
301
|
+
// Capture phase so scrolling any ancestor container counts, not just the page.
|
|
302
|
+
window.addEventListener('scroll', schedule, true);
|
|
303
|
+
window.addEventListener('resize', schedule);
|
|
304
|
+
observeAnchor();
|
|
305
|
+
position();
|
|
306
|
+
return {
|
|
307
|
+
update(next) {
|
|
308
|
+
opts = next;
|
|
309
|
+
observeAnchor();
|
|
310
|
+
position();
|
|
311
|
+
},
|
|
312
|
+
destroy() {
|
|
313
|
+
if (rafId !== null)
|
|
314
|
+
cancelAnimationFrame(rafId);
|
|
315
|
+
observer.disconnect();
|
|
316
|
+
window.removeEventListener('scroll', schedule, true);
|
|
317
|
+
window.removeEventListener('resize', schedule);
|
|
318
|
+
layer.destroy();
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
}
|
package/dist/util/NavigateTo.js
CHANGED
package/dist/util/Transitions.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cubicOut } from
|
|
1
|
+
import { cubicOut } from 'svelte/easing';
|
|
2
2
|
export const flyAndScale = (node, params = { y: -8, x: 0, start: 0.95, duration: 150 }) => {
|
|
3
3
|
const style = getComputedStyle(node);
|
|
4
4
|
const transform = style.transform === 'none' ? '' : style.transform;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@placeholderco/placeholder-ui",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "A modern, customizable Svelte 5 UI component library with comprehensive theming support. Re-brand it with a single config object.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Placeholderco",
|