@placeholderco/placeholder-ui 2.2.1 → 2.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,32 @@
1
+ <script lang="ts" module>
2
+ // One lock shared by every instance so a nested dialog closing does not
3
+ // hand the page its scrollbar back while the parent is still open
4
+ let lockCount = 0;
5
+ let savedOverflow = '';
6
+ let savedPaddingRight = '';
7
+
8
+ function lockBodyScroll() {
9
+ if (lockCount++ > 0) return;
10
+ const body = document.body;
11
+ savedOverflow = body.style.overflow;
12
+ savedPaddingRight = body.style.paddingRight;
13
+ // Hiding the scrollbar widens the page and shifts everything sideways,
14
+ // which reads as a flicker behind the backdrop; pad by its width instead
15
+ const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
16
+ if (scrollbarWidth > 0) {
17
+ const current = parseFloat(getComputedStyle(body).paddingRight) || 0;
18
+ body.style.paddingRight = `${current + scrollbarWidth}px`;
19
+ }
20
+ body.style.overflow = 'hidden';
21
+ }
22
+
23
+ function unlockBodyScroll() {
24
+ if (--lockCount > 0) return;
25
+ document.body.style.overflow = savedOverflow;
26
+ document.body.style.paddingRight = savedPaddingRight;
27
+ }
28
+ </script>
29
+
1
30
  <script lang="ts">
2
31
  import Paper from '../display/Paper.svelte';
3
32
  import { iconX } from '../icon/index.js';
@@ -110,10 +139,8 @@
110
139
  // Lock body scroll while open; effects only run in the browser, so this is SSR-safe
111
140
  $effect(() => {
112
141
  if (show) {
113
- document.body.style.overflow = 'hidden';
114
- return () => {
115
- document.body.style.overflow = '';
116
- };
142
+ lockBodyScroll();
143
+ return unlockBodyScroll;
117
144
  }
118
145
  });
119
146
 
@@ -193,66 +220,21 @@
193
220
  </dialog>
194
221
 
195
222
  <style>
196
- /* Animate transform, not margin the native dialog centers itself via
197
- `margin: auto`, and a margin animation with `forwards` fill would
198
- permanently override it and pin the dialog to the top of the viewport */
199
- @keyframes fadeIn {
200
- from {
201
- opacity: 0;
202
- transform: translateY(-0.5rem);
203
- }
204
- to {
205
- opacity: 1;
206
- transform: translateY(0);
207
- }
208
- }
209
- @keyframes fadeOut {
210
- from {
211
- opacity: 1;
212
- transform: translateY(0);
213
- }
214
- to {
215
- opacity: 0;
216
- transform: translateY(-0.5rem);
217
- }
218
- }
219
-
220
- @keyframes fadeInFull {
221
- from {
222
- opacity: 0;
223
- }
224
- to {
225
- opacity: 1;
226
- }
227
- }
228
- @keyframes fadeOutFull {
229
- from {
230
- opacity: 1;
231
- }
232
- to {
233
- opacity: 0;
234
- }
235
- }
236
-
237
- /* Keyframes for the backdrop pseudo-element */
238
- @keyframes backdropFadeIn {
239
- from {
240
- background: hsl(0 0% 0% / 0%);
241
- }
242
- to {
243
- background: hsl(0 0% 0% / 65%);
244
- }
245
- }
246
- @keyframes backdropFadeOut {
247
- from {
248
- background: hsl(0 0% 0% / 65%);
249
- }
250
- to {
251
- background: hsl(0 0% 0% / 0%);
252
- }
253
- }
254
-
223
+ /* Open/close is a transition driven by [open] plus @starting-style, not a
224
+ keyframe animation. Keyframes with a `forwards` fill kept a transform
225
+ applied while open, and the backdrop's static colour could paint for a
226
+ frame before its animation took over, showing as a flash on open.
227
+ Transitions have no fill state: the closed values are the base rule, the
228
+ open values live on [open], and @starting-style supplies the from-values
229
+ for the first frame after display switches on */
255
230
  dialog {
231
+ /* Everything that shapes the box lives here, not on [open]: the close
232
+ transition runs after [open] is removed, so rules scoped to it would
233
+ drop out while the dialog is still fading. That includes the browser's
234
+ own :modal positioning, which stops applying the moment close() runs */
235
+ position: fixed;
236
+ inset: 0;
237
+ flex-direction: column;
256
238
  color: var(--text-color);
257
239
  border: none;
258
240
  /* The dialog element itself is focused by showModal(); don't draw a
@@ -270,22 +252,56 @@
270
252
  max-height: calc(100vh - 2rem);
271
253
  max-height: calc(100dvh - 2rem);
272
254
 
273
- animation: fadeOut 0.2s forwards;
255
+ opacity: 0;
256
+ /* Animate transform, not margin: the native dialog centres itself via
257
+ `margin: auto` */
258
+ transform: translateY(-0.5rem);
274
259
  transition:
260
+ opacity 0.2s ease,
261
+ transform 0.2s ease,
275
262
  display 0.2s allow-discrete,
276
263
  overlay 0.2s allow-discrete;
277
- &::backdrop {
278
- animation: backdropFadeOut 0.2s forwards;
264
+ }
265
+
266
+ dialog[open] {
267
+ /* Only when [open] — an unconditional author display would override
268
+ the UA's display: none on closed dialogs and make them visible */
269
+ display: flex;
270
+ opacity: 1;
271
+ /* `none`, not translateY(0): a transform of any value makes the dialog
272
+ the containing block for position: fixed descendants */
273
+ transform: none;
274
+ }
275
+
276
+ @starting-style {
277
+ dialog[open] {
278
+ opacity: 0;
279
+ transform: translateY(-0.5rem);
279
280
  }
280
- &[open] {
281
- /* Only when [open] — an unconditional author display would override
282
- the UA's display: none on closed dialogs and make them visible */
283
- display: flex;
284
- flex-direction: column;
285
- animation: fadeIn 0.2s forwards;
286
- &::backdrop {
287
- animation: backdropFadeIn 0.2s forwards;
288
- }
281
+ }
282
+
283
+ dialog::backdrop {
284
+ background-color: rgb(0 0 0 / 0);
285
+ transition:
286
+ background-color 0.2s ease,
287
+ display 0.2s allow-discrete,
288
+ overlay 0.2s allow-discrete;
289
+ }
290
+
291
+ dialog[open]::backdrop {
292
+ background-color: rgb(0 0 0 / 0.65);
293
+ }
294
+
295
+ @starting-style {
296
+ dialog[open]::backdrop {
297
+ background-color: rgb(0 0 0 / 0);
298
+ }
299
+ }
300
+
301
+ @media (prefers-reduced-motion: reduce) {
302
+ dialog,
303
+ dialog::backdrop {
304
+ transition-duration: 0.01ms;
289
305
  }
290
306
  }
291
307
 
@@ -389,10 +405,6 @@
389
405
  background-color: var(--paper-body-bg);
390
406
  }
391
407
 
392
- dialog::backdrop {
393
- background-color: #0008;
394
- }
395
-
396
408
  .dialog.md {
397
409
  width: 30rem;
398
410
  }
@@ -410,11 +422,9 @@
410
422
  max-width: 100%;
411
423
  max-height: 100%;
412
424
  margin-top: auto;
413
- animation: fadeOutFull 0.2s forwards;
414
-
415
- &[open] {
416
- animation: fadeInFull 0.2s forwards;
417
- }
425
+ /* Full-screen fades in place; more specific than the @starting-style
426
+ rule above, so it wins for both the starting and the open state */
427
+ transform: none;
418
428
  }
419
429
 
420
430
  .dialog.full,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@placeholderco/placeholder-ui",
3
- "version": "2.2.1",
3
+ "version": "2.2.2",
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",