@pure-ds/storybook 0.6.7 → 0.6.9

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.
@@ -1318,6 +1318,22 @@ dialog[open] {
1318
1318
  }
1319
1319
  }
1320
1320
 
1321
+ /* Safari fallback: disable scale animation to avoid dialog clipping bugs */
1322
+ dialog.dialog-no-scale-animation {
1323
+ transform: translate(-50%, -50%);
1324
+ transition: opacity var(--transition-normal) ease;
1325
+ }
1326
+
1327
+ dialog.dialog-no-scale-animation[open] {
1328
+ transform: translate(-50%, -50%);
1329
+ animation: pds-dialog-fade-enter var(--transition-normal) ease;
1330
+ }
1331
+
1332
+ @keyframes pds-dialog-fade-enter {
1333
+ from { opacity: 0; }
1334
+ to { opacity: 1; }
1335
+ }
1336
+
1321
1337
  /* Backdrop styling */
1322
1338
  dialog::backdrop {
1323
1339
  background: var(--backdrop-bg);
@@ -1437,9 +1453,24 @@ dialog.dialog-full { width: calc(100vw - var(--spacing-8)); max-width: calc(100v
1437
1453
 
1438
1454
  /* Mobile responsiveness - maximize on mobile */
1439
1455
  @media (max-width: 639px) {
1456
+ dialog,
1457
+ dialog.dialog-no-scale-animation,
1458
+ dialog.dialog-no-scale-animation[open] {
1459
+ left: 0 !important;
1460
+ top: 0 !important;
1461
+ }
1462
+
1463
+ dialog.dialog-no-scale-animation,
1464
+ dialog.dialog-no-scale-animation[open] {
1465
+ transform: none !important;
1466
+ }
1467
+
1468
+ dialog[open] {
1469
+ left: 0 !important;
1470
+ top: 0 !important;
1471
+ }
1472
+
1440
1473
  dialog {
1441
- left: 0;
1442
- top: 0;
1443
1474
  max-width: 100vw;
1444
1475
  width: 100vw;
1445
1476
  height: 100dvh;
@@ -1447,16 +1478,23 @@ dialog.dialog-full { width: calc(100vw - var(--spacing-8)); max-width: calc(100v
1447
1478
  --dialog-max-height: 100dvh; /* Override custom maxHeight on mobile */
1448
1479
  border-radius: 0;
1449
1480
  margin: 0;
1450
- transform: scale(0.95);
1481
+ transform: scale(0.98);
1451
1482
  }
1452
1483
  dialog[open] {
1453
1484
  transform: scale(1);
1454
1485
  animation: pds-dialog-enter-mobile var(--transition-normal) ease;
1455
1486
  }
1487
+
1488
+ dialog.dialog-no-scale-animation {
1489
+ transition: opacity var(--transition-normal) ease;
1490
+ }
1491
+ dialog.dialog-no-scale-animation[open] {
1492
+ animation: pds-dialog-fade-enter var(--transition-normal) ease;
1493
+ }
1456
1494
  @keyframes pds-dialog-enter-mobile {
1457
1495
  from {
1458
1496
  opacity: 0;
1459
- transform: scale(0.95);
1497
+ transform: scale(0.98);
1460
1498
  }
1461
1499
  to {
1462
1500
  opacity: 1;
@@ -1322,6 +1322,22 @@ dialog[open] {
1322
1322
  }
1323
1323
  }
1324
1324
 
1325
+ /* Safari fallback: disable scale animation to avoid dialog clipping bugs */
1326
+ dialog.dialog-no-scale-animation {
1327
+ transform: translate(-50%, -50%);
1328
+ transition: opacity var(--transition-normal) ease;
1329
+ }
1330
+
1331
+ dialog.dialog-no-scale-animation[open] {
1332
+ transform: translate(-50%, -50%);
1333
+ animation: pds-dialog-fade-enter var(--transition-normal) ease;
1334
+ }
1335
+
1336
+ @keyframes pds-dialog-fade-enter {
1337
+ from { opacity: 0; }
1338
+ to { opacity: 1; }
1339
+ }
1340
+
1325
1341
  /* Backdrop styling */
1326
1342
  dialog::backdrop {
1327
1343
  background: var(--backdrop-bg);
@@ -1441,9 +1457,24 @@ dialog.dialog-full { width: calc(100vw - var(--spacing-8)); max-width: calc(100v
1441
1457
 
1442
1458
  /* Mobile responsiveness - maximize on mobile */
1443
1459
  @media (max-width: 639px) {
1460
+ dialog,
1461
+ dialog.dialog-no-scale-animation,
1462
+ dialog.dialog-no-scale-animation[open] {
1463
+ left: 0 !important;
1464
+ top: 0 !important;
1465
+ }
1466
+
1467
+ dialog.dialog-no-scale-animation,
1468
+ dialog.dialog-no-scale-animation[open] {
1469
+ transform: none !important;
1470
+ }
1471
+
1472
+ dialog[open] {
1473
+ left: 0 !important;
1474
+ top: 0 !important;
1475
+ }
1476
+
1444
1477
  dialog {
1445
- left: 0;
1446
- top: 0;
1447
1478
  max-width: 100vw;
1448
1479
  width: 100vw;
1449
1480
  height: 100dvh;
@@ -1451,16 +1482,23 @@ dialog.dialog-full { width: calc(100vw - var(--spacing-8)); max-width: calc(100v
1451
1482
  --dialog-max-height: 100dvh; /* Override custom maxHeight on mobile */
1452
1483
  border-radius: 0;
1453
1484
  margin: 0;
1454
- transform: scale(0.95);
1485
+ transform: scale(0.98);
1455
1486
  }
1456
1487
  dialog[open] {
1457
1488
  transform: scale(1);
1458
1489
  animation: pds-dialog-enter-mobile var(--transition-normal) ease;
1459
1490
  }
1491
+
1492
+ dialog.dialog-no-scale-animation {
1493
+ transition: opacity var(--transition-normal) ease;
1494
+ }
1495
+ dialog.dialog-no-scale-animation[open] {
1496
+ animation: pds-dialog-fade-enter var(--transition-normal) ease;
1497
+ }
1460
1498
  @keyframes pds-dialog-enter-mobile {
1461
1499
  from {
1462
1500
  opacity: 0;
1463
- transform: scale(0.95);
1501
+ transform: scale(0.98);
1464
1502
  }
1465
1503
  to {
1466
1504
  opacity: 1;
@@ -41,6 +41,34 @@ function appendMessageContent(container, message) {
41
41
  container.appendChild(document.createTextNode(text));
42
42
  }
43
43
 
44
+ function isSafariBrowser() {
45
+ const userAgent = navigator.userAgent;
46
+ const isSafariEngine = /Safari/i.test(userAgent);
47
+ const isOtherBrowser = /(Chrome|Chromium|CriOS|FxiOS|EdgiOS|OPiOS|Opera)/i.test(userAgent);
48
+ return isSafariEngine && !isOtherBrowser;
49
+ }
50
+
51
+ function playDialogEnterAnimation(dialog) {
52
+ if (window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) {
53
+ return;
54
+ }
55
+
56
+ const isMobile = window.matchMedia?.('(max-width: 639px)').matches;
57
+ const animationName = dialog.classList.contains('dialog-no-scale-animation')
58
+ ? 'pds-dialog-fade-enter'
59
+ : isMobile
60
+ ? 'pds-dialog-enter-mobile'
61
+ : 'pds-dialog-enter';
62
+
63
+ dialog.style.animation = 'none';
64
+ void dialog.offsetWidth;
65
+ dialog.style.animation = `${animationName} var(--transition-normal) ease`;
66
+
67
+ dialog.addEventListener('animationend', () => {
68
+ dialog.style.animation = '';
69
+ }, { once: true });
70
+ }
71
+
44
72
  /**
45
73
  * Create a PDS-compliant dialog with proper semantic structure
46
74
  * @param {string|Node|Array} message - Message content (string or DOM nodes)
@@ -63,6 +91,10 @@ export async function ask(message, options = {}) {
63
91
  return new Promise((resolve) => {
64
92
  // Create native dialog element
65
93
  const dialog = document.createElement("dialog");
94
+
95
+ if (isSafariBrowser()) {
96
+ dialog.classList.add("dialog-no-scale-animation");
97
+ }
66
98
 
67
99
  if(config.options?.liquidGlassEffects)
68
100
  dialog.classList.add("liquid-glass");
@@ -213,6 +245,8 @@ export async function ask(message, options = {}) {
213
245
 
214
246
  // Show the dialog as modal
215
247
  dialog.showModal();
248
+
249
+ requestAnimationFrame(() => playDialogEnterAnimation(dialog));
216
250
  });
217
251
  }
218
252
 
@@ -80,6 +80,30 @@ function enhanceDropdown(elem) {
80
80
  trigger.setAttribute("aria-expanded", "false");
81
81
  }
82
82
 
83
+ const measureMenuSize = () => {
84
+ const previousStyle = menu.getAttribute("style");
85
+ menu.style.visibility = "hidden";
86
+ menu.style.display = "inline-block";
87
+ menu.style.pointerEvents = "none";
88
+
89
+ const rect = menu.getBoundingClientRect();
90
+ const width = Math.max(menu.offsetWidth || 0, menu.scrollWidth || 0, rect.width || 0, 1);
91
+ const height = Math.max(
92
+ menu.offsetHeight || 0,
93
+ menu.scrollHeight || 0,
94
+ rect.height || 0,
95
+ 1,
96
+ );
97
+
98
+ if (previousStyle === null) {
99
+ menu.removeAttribute("style");
100
+ } else {
101
+ menu.setAttribute("style", previousStyle);
102
+ }
103
+
104
+ return { width, height };
105
+ };
106
+
83
107
  const resolveDirection = () => {
84
108
  const mode = (
85
109
  elem.getAttribute("data-direction") ||
@@ -88,18 +112,15 @@ function enhanceDropdown(elem) {
88
112
  "auto"
89
113
  ).toLowerCase();
90
114
  if (mode === "up" || mode === "down") return mode;
91
- const rect = elem.getBoundingClientRect();
92
- const menuRect = menu?.getBoundingClientRect?.() || { height: 0 };
93
- const menuHeight = Math.max(
94
- menu?.offsetHeight || 0,
95
- menu?.scrollHeight || 0,
96
- menuRect.height || 0,
97
- 200,
98
- );
99
- const spaceBelow = Math.max(0, window.innerHeight - rect.bottom);
100
- const spaceAbove = Math.max(0, rect.top);
101
- if (spaceBelow >= menuHeight) return "down";
102
- if (spaceAbove >= menuHeight) return "up";
115
+ const anchorRect = (trigger || elem).getBoundingClientRect();
116
+ const { height: menuHeight } = measureMenuSize();
117
+ const spaceBelow = Math.max(0, window.innerHeight - anchorRect.bottom);
118
+ const spaceAbove = Math.max(0, anchorRect.top);
119
+ const fitsDown = spaceBelow >= menuHeight;
120
+ const fitsUp = spaceAbove >= menuHeight;
121
+ if (fitsDown && !fitsUp) return "down";
122
+ if (fitsUp && !fitsDown) return "up";
123
+ if (fitsDown && fitsUp) return "down";
103
124
  return spaceAbove > spaceBelow ? "up" : "down";
104
125
  };
105
126
 
@@ -117,19 +138,17 @@ function enhanceDropdown(elem) {
117
138
  ) {
118
139
  return align === "start" ? "left" : align === "end" ? "right" : align;
119
140
  }
120
- const rect = elem.getBoundingClientRect();
121
- const menuRect = menu?.getBoundingClientRect?.() || { width: 0 };
122
- const menuWidth = Math.max(
123
- menu?.offsetWidth || 0,
124
- menu?.scrollWidth || 0,
125
- menuRect.width || 0,
126
- 240,
127
- );
128
- const spaceRight = Math.max(0, window.innerWidth - rect.left);
129
- const spaceLeft = Math.max(0, rect.right);
130
- if (spaceRight >= menuWidth) return "left";
131
- if (spaceLeft >= menuWidth) return "right";
132
- return spaceLeft > spaceRight ? "right" : "left";
141
+ const anchorRect = (trigger || elem).getBoundingClientRect();
142
+ const { width: menuWidth } = measureMenuSize();
143
+ const spaceForLeftAligned = Math.max(0, window.innerWidth - anchorRect.left);
144
+ const spaceForRightAligned = Math.max(0, anchorRect.right);
145
+ const fitsLeft = spaceForLeftAligned >= menuWidth;
146
+ const fitsRight = spaceForRightAligned >= menuWidth;
147
+
148
+ if (fitsLeft && !fitsRight) return "left";
149
+ if (fitsRight && !fitsLeft) return "right";
150
+ if (fitsLeft && fitsRight) return "left";
151
+ return spaceForRightAligned > spaceForLeftAligned ? "right" : "left";
133
152
  };
134
153
 
135
154
  // Store click handler reference for cleanup
@@ -3317,6 +3317,22 @@ dialog[open] {
3317
3317
  }
3318
3318
  }
3319
3319
 
3320
+ /* Safari fallback: disable scale animation to avoid dialog clipping bugs */
3321
+ dialog.dialog-no-scale-animation {
3322
+ transform: translate(-50%, -50%);
3323
+ transition: opacity var(--transition-normal) ease;
3324
+ }
3325
+
3326
+ dialog.dialog-no-scale-animation[open] {
3327
+ transform: translate(-50%, -50%);
3328
+ animation: pds-dialog-fade-enter var(--transition-normal) ease;
3329
+ }
3330
+
3331
+ @keyframes pds-dialog-fade-enter {
3332
+ from { opacity: 0; }
3333
+ to { opacity: 1; }
3334
+ }
3335
+
3320
3336
  /* Backdrop styling */
3321
3337
  dialog::backdrop {
3322
3338
  background: var(--backdrop-bg);
@@ -3436,9 +3452,24 @@ dialog.dialog-full { width: calc(100vw - var(--spacing-8)); max-width: calc(100v
3436
3452
 
3437
3453
  /* Mobile responsiveness - maximize on mobile */
3438
3454
  @media (max-width: ${breakpoints.sm - 1}px) {
3455
+ dialog,
3456
+ dialog.dialog-no-scale-animation,
3457
+ dialog.dialog-no-scale-animation[open] {
3458
+ left: 0 !important;
3459
+ top: 0 !important;
3460
+ }
3461
+
3462
+ dialog.dialog-no-scale-animation,
3463
+ dialog.dialog-no-scale-animation[open] {
3464
+ transform: none !important;
3465
+ }
3466
+
3467
+ dialog[open] {
3468
+ left: 0 !important;
3469
+ top: 0 !important;
3470
+ }
3471
+
3439
3472
  dialog {
3440
- left: 0;
3441
- top: 0;
3442
3473
  max-width: 100vw;
3443
3474
  width: 100vw;
3444
3475
  height: 100dvh;
@@ -3446,16 +3477,23 @@ dialog.dialog-full { width: calc(100vw - var(--spacing-8)); max-width: calc(100v
3446
3477
  --dialog-max-height: 100dvh; /* Override custom maxHeight on mobile */
3447
3478
  border-radius: 0;
3448
3479
  margin: 0;
3449
- transform: scale(0.95);
3480
+ transform: scale(0.98);
3450
3481
  }
3451
3482
  dialog[open] {
3452
3483
  transform: scale(1);
3453
3484
  animation: pds-dialog-enter-mobile var(--transition-normal) ease;
3454
3485
  }
3486
+
3487
+ dialog.dialog-no-scale-animation {
3488
+ transition: opacity var(--transition-normal) ease;
3489
+ }
3490
+ dialog.dialog-no-scale-animation[open] {
3491
+ animation: pds-dialog-fade-enter var(--transition-normal) ease;
3492
+ }
3455
3493
  @keyframes pds-dialog-enter-mobile {
3456
3494
  from {
3457
3495
  opacity: 0;
3458
- transform: scale(0.95);
3496
+ transform: scale(0.98);
3459
3497
  }
3460
3498
  to {
3461
3499
  opacity: 1;
@@ -3823,9 +3861,12 @@ nav[data-dropdown] {
3823
3861
  }
3824
3862
  }
3825
3863
 
3826
- &[data-mode="auto"] > :last-child {
3864
+ &[data-mode="auto"]:not([data-dropdown-direction]) > :last-child {
3827
3865
  top: 100%;
3828
3866
  bottom: auto;
3867
+ margin-top: var(--spacing-2);
3868
+ margin-bottom: 0;
3869
+ transform-origin: top center;
3829
3870
  }
3830
3871
 
3831
3872
  @media (prefers-reduced-motion: reduce) {
@@ -3,3 +3,4 @@
3
3
  * Keeps production pds.js lean; loaded dynamically in live mode only.
4
4
  */
5
5
  export { startLive } from "./pds-core/pds-live.js";
6
+ export { loadGoogleFont } from "./common/font-loader.js";
@@ -22,6 +22,26 @@ const dropdownStoryStyles = html`
22
22
  .story-glass-demo-travel {
23
23
  background-image: url('https://images.unsplash.com/photo-1517832207067-4db24a2ae47c?auto=format&fit=crop&w=1200&q=80');
24
24
  }
25
+ .story-corner-nav {
26
+ position: fixed;
27
+ z-index: var(--z-dropdown, 1050);
28
+ }
29
+ .story-corner-nav.top-left {
30
+ top: 10px;
31
+ left: 10px;
32
+ }
33
+ .story-corner-nav.top-right {
34
+ top: 10px;
35
+ right: 10px;
36
+ }
37
+ .story-corner-nav.bottom-left {
38
+ bottom: 10px;
39
+ left: 10px;
40
+ }
41
+ .story-corner-nav.bottom-right {
42
+ bottom: 10px;
43
+ right: 10px;
44
+ }
25
45
  </style>
26
46
  `;
27
47
 
@@ -208,3 +228,49 @@ export const ProfilePanel = () => html`
208
228
  </article>
209
229
  </nav>
210
230
  `;
231
+
232
+ export const AutoPositionCorners = () => html`
233
+ ${dropdownStoryStyles}
234
+ ${enhancementHeader('dropdown', {
235
+ description: 'Four corner triggers use automatic positioning (`data-mode="auto"`) so each menu opens in the only available direction and alignment.'
236
+ })}
237
+ <nav data-dropdown data-mode="auto" class="story-corner-nav top-left">
238
+ <button class="btn-primary icon-only" aria-label="Top Left menu">
239
+ <pds-icon icon="list" size="sm"></pds-icon>
240
+ </button>
241
+ <menu>
242
+ <li><a href="#">Item 1</a></li>
243
+ <li><a href="#">Item 2</a></li>
244
+ </menu>
245
+ </nav>
246
+
247
+ <nav data-dropdown data-mode="auto" class="story-corner-nav top-right">
248
+ <button class="btn-primary icon-only" aria-label="Top Right menu">
249
+ <pds-icon icon="list" size="sm"></pds-icon>
250
+ </button>
251
+ <menu>
252
+ <li><a href="#">Item 1</a></li>
253
+ <li><a href="#">Item 2</a></li>
254
+ </menu>
255
+ </nav>
256
+
257
+ <nav data-dropdown data-mode="auto" class="story-corner-nav bottom-left">
258
+ <button class="btn-primary icon-only" aria-label="Bottom Left menu">
259
+ <pds-icon icon="list" size="sm"></pds-icon>
260
+ </button>
261
+ <menu>
262
+ <li><a href="#">Item 1</a></li>
263
+ <li><a href="#">Item 2</a></li>
264
+ </menu>
265
+ </nav>
266
+
267
+ <nav data-dropdown data-mode="auto" class="story-corner-nav bottom-right">
268
+ <button class="btn-primary icon-only" aria-label="Bottom Right menu">
269
+ <pds-icon icon="list" size="sm"></pds-icon>
270
+ </button>
271
+ <menu>
272
+ <li><a href="#">Item 1</a></li>
273
+ <li><a href="#">Item 2</a></li>
274
+ </menu>
275
+ </nav>
276
+ `;