@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.
Files changed (80) hide show
  1. package/dist/editors/CodeMirror.svelte +295 -297
  2. package/dist/editors/tiptap/LinkActionMenu.svelte +6 -1
  3. package/dist/editors/tiptap/extensions/Alert.js +17 -17
  4. package/dist/editors/tiptap/extensions/Div.js +19 -19
  5. package/dist/editors/tiptap/extensions/Span.js +14 -14
  6. package/dist/editors/tiptap/toolbar/AlertButton.svelte +254 -253
  7. package/dist/editors/tiptap/toolbar/AlignCenterButton.svelte +11 -11
  8. package/dist/editors/tiptap/toolbar/AlignJustifyButton.svelte +11 -11
  9. package/dist/editors/tiptap/toolbar/AlignLeftButton.svelte +11 -11
  10. package/dist/editors/tiptap/toolbar/AlignRightButton.svelte +11 -11
  11. package/dist/editors/tiptap/toolbar/BoldButton.svelte +11 -11
  12. package/dist/editors/tiptap/toolbar/BulletListButton.svelte +11 -11
  13. package/dist/editors/tiptap/toolbar/CodeButton.svelte +11 -11
  14. package/dist/editors/tiptap/toolbar/ColorButton.svelte +265 -265
  15. package/dist/editors/tiptap/toolbar/DocumentButton.svelte +51 -51
  16. package/dist/editors/tiptap/toolbar/HeadingButton.svelte +21 -21
  17. package/dist/editors/tiptap/toolbar/HtmlButton.svelte +48 -52
  18. package/dist/editors/tiptap/toolbar/ImageButton.svelte +35 -39
  19. package/dist/editors/tiptap/toolbar/ItalicButton.svelte +11 -11
  20. package/dist/editors/tiptap/toolbar/LinkButton.svelte +249 -249
  21. package/dist/editors/tiptap/toolbar/OrderedListButton.svelte +11 -11
  22. package/dist/editors/tiptap/toolbar/RedoButton.svelte +11 -11
  23. package/dist/editors/tiptap/toolbar/StrikethroughButton.svelte +11 -11
  24. package/dist/editors/tiptap/toolbar/SubscriptButton.svelte +11 -11
  25. package/dist/editors/tiptap/toolbar/SuperscriptButton.svelte +11 -11
  26. package/dist/editors/tiptap/toolbar/TableButton.svelte +231 -224
  27. package/dist/editors/tiptap/toolbar/ToolbarButton.svelte +59 -65
  28. package/dist/editors/tiptap/toolbar/UnderlineButton.svelte +11 -11
  29. package/dist/editors/tiptap/toolbar/UndoButton.svelte +11 -11
  30. package/dist/form/Autocomplete.svelte +24 -22
  31. package/dist/form/AutocompleteMulti.svelte +24 -22
  32. package/dist/form/Checkbox.svelte +4 -4
  33. package/dist/form/Chips.svelte +15 -7
  34. package/dist/form/ColorPicker.svelte +5 -20
  35. package/dist/form/ComboBox.svelte +4 -4
  36. package/dist/form/ComboBoxItemBuilder.svelte +14 -32
  37. package/dist/form/ComboBoxMulti.svelte +7 -3
  38. package/dist/form/CronBuilder.svelte +32 -110
  39. package/dist/form/DatePicker.svelte +9 -28
  40. package/dist/form/DateRangePicker.svelte +46 -36
  41. package/dist/form/DateTimePicker.svelte +11 -30
  42. package/dist/form/Number.svelte +1 -3
  43. package/dist/form/RadioGroup.svelte +1 -3
  44. package/dist/form/Select.svelte +25 -28
  45. package/dist/form/SelectMulti.svelte +36 -38
  46. package/dist/form/StringArrayBuilder.svelte +1 -3
  47. package/dist/form/TextArea.svelte +5 -6
  48. package/dist/form/Textbox.svelte +10 -4
  49. package/dist/form/TimePicker.svelte +10 -4
  50. package/dist/icon/Icon.svelte +5 -1
  51. package/dist/index.d.ts +147 -146
  52. package/dist/index.js +78 -77
  53. package/dist/layout/CustomNavbar.svelte +1 -1
  54. package/dist/layout/NavbarItemDisplay.svelte +1 -1
  55. package/dist/layout/NavbarItemView.svelte +3 -1
  56. package/dist/layout/Sidenav.svelte +1 -1
  57. package/dist/models/ComboBoxItem.d.ts +4 -4
  58. package/dist/models/NotifyModel.js +0 -1
  59. package/dist/theme.svelte.d.ts +1 -1
  60. package/dist/theme.svelte.js +46 -22
  61. package/dist/ui/Accordion.svelte +1 -3
  62. package/dist/ui/Badge.svelte +4 -5
  63. package/dist/ui/Button.svelte +7 -1
  64. package/dist/ui/ContextMenu.svelte +5 -11
  65. package/dist/ui/Dialog.svelte +16 -3
  66. package/dist/ui/Dialog.svelte.d.ts +7 -1
  67. package/dist/ui/Dropdown.svelte +21 -10
  68. package/dist/ui/MultiSortable.svelte +1 -3
  69. package/dist/ui/Pagination.svelte +2 -2
  70. package/dist/ui/Popover.svelte +16 -37
  71. package/dist/ui/ProgressBar.svelte +1 -4
  72. package/dist/ui/ThemeSwitcher.svelte +2 -1
  73. package/dist/ui/Tooltip.svelte +1 -37
  74. package/dist/util/DateFunctions.js +1 -1
  75. package/dist/util/Floating.d.ts +70 -0
  76. package/dist/util/Floating.js +321 -0
  77. package/dist/util/NavigateTo.js +2 -2
  78. package/dist/util/Transitions.d.ts +1 -1
  79. package/dist/util/Transitions.js +1 -1
  80. package/package.json +1 -1
@@ -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 popover-{position}"
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
- /* Position-specific styles */
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 = $props();
17
+ let { lightVariant = 'primary-subtle', darkVariant = 'accent-subtle' }: ThemeSwitcherProps =
18
+ $props();
18
19
 
19
20
  const themeCycle = ['light', 'dark', 'system'] as const;
20
21
 
@@ -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 };
@@ -1,4 +1,4 @@
1
- import dayjs from "./dayjs.js";
1
+ import dayjs from './dayjs.js';
2
2
  export function getDateInXTimeAgo(date) {
3
3
  const d = dayjs(date);
4
4
  const now = dayjs();
@@ -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
+ }
@@ -1,5 +1,5 @@
1
- import { goto } from "$app/navigation";
2
- import { resolve } from "$app/paths";
1
+ import { goto } from '$app/navigation';
2
+ import { resolve } from '$app/paths';
3
3
  export function navigateTo(url) {
4
4
  goto(resolve(url));
5
5
  }
@@ -1,4 +1,4 @@
1
- import type { TransitionConfig } from "svelte/transition";
1
+ import type { TransitionConfig } from 'svelte/transition';
2
2
  type FlyAndScaleParams = {
3
3
  y?: number;
4
4
  x?: number;
@@ -1,4 +1,4 @@
1
- import { cubicOut } from "svelte/easing";
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.1.1",
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",