@placeholderco/placeholder-ui 2.1.0 → 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 +25 -25
@@ -194,7 +194,13 @@
194
194
  isParsingExpression = true;
195
195
 
196
196
  // Try to detect frequency type
197
- if (min.startsWith('*/') && hour === '*' && dayOfMonth === '*' && month === '*' && dayOfWeek === '*') {
197
+ if (
198
+ min.startsWith('*/') &&
199
+ hour === '*' &&
200
+ dayOfMonth === '*' &&
201
+ month === '*' &&
202
+ dayOfWeek === '*'
203
+ ) {
198
204
  frequency = 'minute';
199
205
  minuteInterval = parseInt(min.split('/')[1]) || 1;
200
206
  } else if (hour.startsWith('*/') && dayOfMonth === '*' && month === '*' && dayOfWeek === '*') {
@@ -210,7 +216,10 @@
210
216
  frequency = 'weekly';
211
217
  weeklyMinute = parseInt(min) || 0;
212
218
  weeklyHour = parseInt(hour) || 0;
213
- weeklyDays = dayOfWeek.split(',').map(d => parseInt(d)).filter(d => !isNaN(d));
219
+ weeklyDays = dayOfWeek
220
+ .split(',')
221
+ .map((d) => parseInt(d))
222
+ .filter((d) => !isNaN(d));
214
223
  } else if (month === '*' && dayOfWeek === '*' && !dayOfMonth.includes('*')) {
215
224
  frequency = 'monthly';
216
225
  monthlyMinute = parseInt(min) || 0;
@@ -236,7 +245,7 @@
236
245
 
237
246
  function toggleWeekday(day: number) {
238
247
  if (weeklyDays.includes(day)) {
239
- weeklyDays = weeklyDays.filter(d => d !== day);
248
+ weeklyDays = weeklyDays.filter((d) => d !== day);
240
249
  } else {
241
250
  weeklyDays = [...weeklyDays, day];
242
251
  }
@@ -298,14 +307,16 @@
298
307
  return `Every ${dayText} at ${dailyHour.toString().padStart(2, '0')}:${dailyMinute.toString().padStart(2, '0')}`;
299
308
 
300
309
  case 'weekly':
301
- const dayNames = weeklyDays.map(d => weekDays.find(wd => wd.value === d)?.label).join(', ');
310
+ const dayNames = weeklyDays
311
+ .map((d) => weekDays.find((wd) => wd.value === d)?.label)
312
+ .join(', ');
302
313
  return `Every ${dayNames} at ${weeklyHour.toString().padStart(2, '0')}:${weeklyMinute.toString().padStart(2, '0')}`;
303
314
 
304
315
  case 'monthly':
305
316
  return `Day ${monthlyDay} of every month at ${monthlyHour.toString().padStart(2, '0')}:${monthlyMinute.toString().padStart(2, '0')}`;
306
317
 
307
318
  case 'yearly':
308
- const monthName = monthOptions.find(m => m.value === yearlyMonth.toString())?.label;
319
+ const monthName = monthOptions.find((m) => m.value === yearlyMonth.toString())?.label;
309
320
  return `${monthName} ${yearlyDay} at ${yearlyHour.toString().padStart(2, '0')}:${yearlyMinute.toString().padStart(2, '0')}`;
310
321
 
311
322
  case 'custom':
@@ -329,12 +340,7 @@
329
340
  >
330
341
  <div class="cron-builder {classes}" class:disabled>
331
342
  <div class="cron-frequency">
332
- <Select
333
- label="Frequency"
334
- bind:value={frequency}
335
- options={frequencyOptions}
336
- {disabled}
337
- />
343
+ <Select label="Frequency" bind:value={frequency} options={frequencyOptions} {disabled} />
338
344
  </div>
339
345
 
340
346
  <div class="cron-settings">
@@ -347,42 +353,12 @@
347
353
  {disabled}
348
354
  />
349
355
  {:else if frequency === 'hourly'}
350
- <Number
351
- label="Interval (hours)"
352
- bind:value={hourlyInterval}
353
- min={1}
354
- max={23}
355
- {disabled}
356
- />
357
- <Number
358
- label="At minute"
359
- bind:value={hourlyMinute}
360
- min={0}
361
- max={59}
362
- {disabled}
363
- />
356
+ <Number label="Interval (hours)" bind:value={hourlyInterval} min={1} max={23} {disabled} />
357
+ <Number label="At minute" bind:value={hourlyMinute} min={0} max={59} {disabled} />
364
358
  {:else if frequency === 'daily'}
365
- <Number
366
- label="Interval (days)"
367
- bind:value={dailyInterval}
368
- min={1}
369
- max={31}
370
- {disabled}
371
- />
372
- <Number
373
- label="Hour"
374
- bind:value={dailyHour}
375
- min={0}
376
- max={23}
377
- {disabled}
378
- />
379
- <Number
380
- label="Minute"
381
- bind:value={dailyMinute}
382
- min={0}
383
- max={59}
384
- {disabled}
385
- />
359
+ <Number label="Interval (days)" bind:value={dailyInterval} min={1} max={31} {disabled} />
360
+ <Number label="Hour" bind:value={dailyHour} min={0} max={23} {disabled} />
361
+ <Number label="Minute" bind:value={dailyMinute} min={0} max={59} {disabled} />
386
362
  {:else if frequency === 'weekly'}
387
363
  <div class="weekday-selector">
388
364
  <span class="weekday-label">Days of Week</span>
@@ -400,71 +376,17 @@
400
376
  {/each}
401
377
  </div>
402
378
  </div>
403
- <Number
404
- label="Hour"
405
- bind:value={weeklyHour}
406
- min={0}
407
- max={23}
408
- {disabled}
409
- />
410
- <Number
411
- label="Minute"
412
- bind:value={weeklyMinute}
413
- min={0}
414
- max={59}
415
- {disabled}
416
- />
379
+ <Number label="Hour" bind:value={weeklyHour} min={0} max={23} {disabled} />
380
+ <Number label="Minute" bind:value={weeklyMinute} min={0} max={59} {disabled} />
417
381
  {:else if frequency === 'monthly'}
418
- <Number
419
- label="Day of Month"
420
- bind:value={monthlyDay}
421
- min={1}
422
- max={31}
423
- {disabled}
424
- />
425
- <Number
426
- label="Hour"
427
- bind:value={monthlyHour}
428
- min={0}
429
- max={23}
430
- {disabled}
431
- />
432
- <Number
433
- label="Minute"
434
- bind:value={monthlyMinute}
435
- min={0}
436
- max={59}
437
- {disabled}
438
- />
382
+ <Number label="Day of Month" bind:value={monthlyDay} min={1} max={31} {disabled} />
383
+ <Number label="Hour" bind:value={monthlyHour} min={0} max={23} {disabled} />
384
+ <Number label="Minute" bind:value={monthlyMinute} min={0} max={59} {disabled} />
439
385
  {:else if frequency === 'yearly'}
440
- <Number
441
- label="Month"
442
- bind:value={yearlyMonth}
443
- min={1}
444
- max={12}
445
- {disabled}
446
- />
447
- <Number
448
- label="Day"
449
- bind:value={yearlyDay}
450
- min={1}
451
- max={31}
452
- {disabled}
453
- />
454
- <Number
455
- label="Hour"
456
- bind:value={yearlyHour}
457
- min={0}
458
- max={23}
459
- {disabled}
460
- />
461
- <Number
462
- label="Minute"
463
- bind:value={yearlyMinute}
464
- min={0}
465
- max={59}
466
- {disabled}
467
- />
386
+ <Number label="Month" bind:value={yearlyMonth} min={1} max={12} {disabled} />
387
+ <Number label="Day" bind:value={yearlyDay} min={1} max={31} {disabled} />
388
+ <Number label="Hour" bind:value={yearlyHour} min={0} max={23} {disabled} />
389
+ <Number label="Minute" bind:value={yearlyMinute} min={0} max={59} {disabled} />
468
390
  {:else if frequency === 'custom'}
469
391
  <div class="custom-inputs">
470
392
  <FormGroup label="Minute">
@@ -708,4 +630,4 @@
708
630
  grid-template-columns: 1fr;
709
631
  }
710
632
  }
711
- </style>
633
+ </style>
@@ -32,6 +32,7 @@
32
32
  import { fade } from 'svelte/transition';
33
33
  import dayjs from '../util/dayjs.js';
34
34
  import { clickOutside } from '../util/ClickOutside.js';
35
+ import { floating } from '../util/Floating.js';
35
36
  import ActionIcon from '../ui/ActionIcon.svelte';
36
37
  import { iconChevronLeft, iconChevronRight, iconX } from '../icon/index.js';
37
38
 
@@ -204,7 +205,6 @@
204
205
  let readableDate: string = $state('');
205
206
  let showCalendar = $state(false);
206
207
  let showYearPicker = $state(false);
207
- let dropdownPosition: 'above' | 'below' = $state('below');
208
208
 
209
209
  // Year picker: show a range of years centered around the focused year
210
210
  const yearRangeSize = 12;
@@ -241,22 +241,6 @@
241
241
  showYearPicker = !showYearPicker;
242
242
  }
243
243
 
244
- function checkDropdownPosition() {
245
- if (!textboxElement) return;
246
-
247
- const rect = textboxElement.getBoundingClientRect();
248
- const dropdownHeight = 300; // Approximate height of the calendar dropdown
249
- const spaceBelow = window.innerHeight - rect.bottom;
250
- const spaceAbove = rect.top;
251
-
252
- // If not enough space below and more space above, position above
253
- if (spaceBelow < dropdownHeight && spaceAbove > spaceBelow) {
254
- dropdownPosition = 'above';
255
- } else {
256
- dropdownPosition = 'below';
257
- }
258
- }
259
-
260
244
  function onClickOutside(event: MouseEvent) {
261
245
  if (!showCalendar) return;
262
246
 
@@ -315,7 +299,6 @@
315
299
  bind:textboxElement
316
300
  value={readableDate}
317
301
  onfocus={() => {
318
- checkDropdownPosition();
319
302
  showCalendar = true;
320
303
  }}
321
304
  onkeyup={onDateTextChange}
@@ -339,7 +322,13 @@
339
322
  {/snippet}
340
323
  <div class="date-picker">
341
324
  {#if showCalendar}
342
- <div class="calendar {dropdownPosition}" transition:fade={{ duration: 200 }} use:clickOutside={onClickOutside}>
325
+ <!-- Rendered in the top layer so a scrolling Dialog body cannot clip it -->
326
+ <div
327
+ class="calendar"
328
+ transition:fade={{ duration: 200 }}
329
+ use:floating={{ anchor: () => textboxElement, placement: 'bottom-start', offset: 4 }}
330
+ use:clickOutside={onClickOutside}
331
+ >
343
332
  <div class="month-buttons">
344
333
  {#if showYearPicker}
345
334
  <ActionIcon
@@ -426,10 +415,10 @@
426
415
  line-height: 0;
427
416
  }
428
417
 
418
+ /* Position and top-layer promotion come from use:floating */
429
419
  .calendar {
430
420
  width: 18rem;
431
421
  text-align: center;
432
- position: absolute;
433
422
  z-index: 10;
434
423
  user-select: none;
435
424
  background-color: var(--paper-body-bg);
@@ -439,14 +428,6 @@
439
428
  padding: 0.5rem;
440
429
  }
441
430
 
442
- .calendar.below {
443
- top: 0
444
- }
445
-
446
- .calendar.above {
447
- bottom: 2.25rem;
448
- }
449
-
450
431
  .month-buttons {
451
432
  display: flex;
452
433
  justify-content: space-between;
@@ -26,6 +26,7 @@
26
26
  import { fade } from 'svelte/transition';
27
27
  import dayjs from '../util/dayjs.js';
28
28
  import { clickOutside } from '../util/ClickOutside.js';
29
+ import { floating } from '../util/Floating.js';
29
30
  import ActionIcon from '../ui/ActionIcon.svelte';
30
31
  import { iconChevronLeft, iconChevronRight, iconX } from '../icon/index.js';
31
32
 
@@ -65,7 +66,7 @@
65
66
  let focusedMonth = $state((selectedStart ?? todayDate).startOf('month'));
66
67
  let showCalendar = $state(false);
67
68
  let showYearPicker = $state(false);
68
- let dropdownPosition: 'above' | 'below' = $state('below');
69
+ let inputWrapElement: HTMLElement | undefined = $state(undefined);
69
70
  let containerElement: HTMLElement | undefined = $state(undefined);
70
71
 
71
72
  const yearRangeSize = 12;
@@ -163,7 +164,15 @@
163
164
  isInRange = date.isSame(effStart, 'date');
164
165
  }
165
166
 
166
- displayedDates.push({ date, text: date.date().toString(), color, isRangeStart, isRangeEnd, isInRange, isDisabled });
167
+ displayedDates.push({
168
+ date,
169
+ text: date.date().toString(),
170
+ color,
171
+ isRangeStart,
172
+ isRangeEnd,
173
+ isInRange,
174
+ isDisabled
175
+ });
167
176
  }
168
177
 
169
178
  return displayedDates;
@@ -242,14 +251,6 @@
242
251
  focusedMonth = focusedMonth.add(delta, 'month');
243
252
  }
244
253
 
245
- function checkDropdownPosition() {
246
- if (!containerElement) return;
247
- const rect = containerElement.getBoundingClientRect();
248
- const spaceBelow = window.innerHeight - rect.bottom;
249
- const spaceAbove = rect.top;
250
- dropdownPosition = spaceBelow < 340 && spaceAbove > spaceBelow ? 'above' : 'below';
251
- }
252
-
253
254
  function onClickOutside(event: MouseEvent) {
254
255
  if (!showCalendar) return;
255
256
  const target = event.target as HTMLElement;
@@ -301,16 +302,15 @@
301
302
  class:drp-open={showCalendar}
302
303
  role="button"
303
304
  tabindex={disabled ? -1 : 0}
305
+ bind:this={inputWrapElement}
304
306
  onclick={() => {
305
307
  if (!disabled) {
306
- checkDropdownPosition();
307
308
  showCalendar = true;
308
309
  }
309
310
  }}
310
311
  onkeydown={(e) => {
311
312
  if (!disabled && (e.key === 'Enter' || e.key === ' ')) {
312
313
  e.preventDefault();
313
- checkDropdownPosition();
314
314
  showCalendar = true;
315
315
  }
316
316
  }}
@@ -321,36 +321,49 @@
321
321
 
322
322
  {#if displayText && !disabled}
323
323
  <div class="drp-clear">
324
- <ActionIcon
325
- variant="secondary-subtle"
326
- svg={iconX}
327
- size="0.75rem"
328
- onclick={clearRange}
329
- />
324
+ <ActionIcon variant="secondary-subtle" svg={iconX} size="0.75rem" onclick={clearRange} />
330
325
  </div>
331
326
  {/if}
332
327
 
333
328
  <div class="drp-calendar-anchor">
334
329
  {#if showCalendar}
330
+ <!-- Rendered in the top layer so a scrolling Dialog body cannot clip it -->
335
331
  <div
336
- class="calendar {dropdownPosition}"
332
+ class="calendar"
337
333
  transition:fade={{ duration: 150 }}
334
+ use:floating={{ anchor: () => inputWrapElement, placement: 'bottom-start', offset: 4 }}
338
335
  use:clickOutside={onClickOutside}
339
336
  >
340
337
  <!-- Month navigation -->
341
338
  <div class="month-buttons">
342
339
  {#if showYearPicker}
343
- <ActionIcon svg={iconChevronLeft} size="1.5rem" onclick={() => incrementYearRange(-1)} />
340
+ <ActionIcon
341
+ svg={iconChevronLeft}
342
+ size="1.5rem"
343
+ onclick={() => incrementYearRange(-1)}
344
+ />
344
345
  <button class="month-label clickable" onclick={toggleYearPicker}>
345
346
  {yearRangeStart} – {yearRangeStart + yearRangeSize - 1}
346
347
  </button>
347
- <ActionIcon svg={iconChevronRight} size="1.5rem" onclick={() => incrementYearRange(1)} />
348
+ <ActionIcon
349
+ svg={iconChevronRight}
350
+ size="1.5rem"
351
+ onclick={() => incrementYearRange(1)}
352
+ />
348
353
  {:else}
349
- <ActionIcon svg={iconChevronLeft} size="1.5rem" onclick={() => incrementFocusedMonth(-1)} />
354
+ <ActionIcon
355
+ svg={iconChevronLeft}
356
+ size="1.5rem"
357
+ onclick={() => incrementFocusedMonth(-1)}
358
+ />
350
359
  <button class="month-label clickable" onclick={toggleYearPicker}>
351
360
  {focusedMonth.format('MMMM YYYY')}
352
361
  </button>
353
- <ActionIcon svg={iconChevronRight} size="1.5rem" onclick={() => incrementFocusedMonth(1)} />
362
+ <ActionIcon
363
+ svg={iconChevronRight}
364
+ size="1.5rem"
365
+ onclick={() => incrementFocusedMonth(1)}
366
+ />
354
367
  {/if}
355
368
  </div>
356
369
 
@@ -383,12 +396,16 @@
383
396
  <!-- svelte-ignore a11y_no_static_element_interactions -->
384
397
  <div
385
398
  class="days-grid"
386
- onmouseleave={() => { if (selectingEnd) hoverDate = undefined; }}
399
+ onmouseleave={() => {
400
+ if (selectingEnd) hoverDate = undefined;
401
+ }}
387
402
  >
388
403
  {#each calculateDays(effectiveStart, effectiveEnd, focusedMonth) as d, i (i)}
389
404
  <div
390
405
  class={buildCellClasses(d)}
391
- onmouseenter={() => { if (selectingEnd && !d.isDisabled) hoverDate = d.date; }}
406
+ onmouseenter={() => {
407
+ if (selectingEnd && !d.isDisabled) hoverDate = d.date;
408
+ }}
392
409
  >
393
410
  <button
394
411
  class={buildButtonClasses(d)}
@@ -477,10 +494,10 @@
477
494
 
478
495
  /* ---- Calendar ---- */
479
496
 
497
+ /* Position and top-layer promotion come from use:floating */
480
498
  .calendar {
481
499
  width: 19rem;
482
500
  text-align: center;
483
- position: absolute;
484
501
  z-index: 10;
485
502
  background-color: var(--paper-body-bg);
486
503
  border: 1px solid var(--border-color);
@@ -489,15 +506,6 @@
489
506
  padding: 0.5rem;
490
507
  }
491
508
 
492
- .calendar.below {
493
- top: 0.25rem;
494
- }
495
-
496
- .calendar.above {
497
- bottom: calc(100% + 0.25rem);
498
- top: auto;
499
- }
500
-
501
509
  .month-buttons {
502
510
  display: flex;
503
511
  justify-content: space-between;
@@ -655,7 +663,9 @@
655
663
  background: transparent;
656
664
  cursor: pointer;
657
665
  font-size: 0.875rem;
658
- transition: background-color 0.15s ease, color 0.15s ease;
666
+ transition:
667
+ background-color 0.15s ease,
668
+ color 0.15s ease;
659
669
  flex-shrink: 0;
660
670
  display: flex;
661
671
  align-items: center;
@@ -24,6 +24,7 @@
24
24
  import { fade } from 'svelte/transition';
25
25
  import dayjs from '../util/dayjs.js';
26
26
  import { clickOutside } from '../util/ClickOutside.js';
27
+ import { floating } from '../util/Floating.js';
27
28
  import ActionIcon from '../ui/ActionIcon.svelte';
28
29
  import { iconChevronLeft, iconChevronRight, iconX } from '../icon/index.js';
29
30
  import TimePicker from './TimePicker.svelte';
@@ -208,7 +209,6 @@
208
209
  let readableDate: string = $state('');
209
210
  let showCalendar = $state(false);
210
211
  let showYearPicker = $state(false);
211
- let dropdownPosition: 'above' | 'below' = $state('below');
212
212
 
213
213
  // Year picker: show a range of years centered around the focused year
214
214
  const yearRangeSize = 12;
@@ -245,22 +245,6 @@
245
245
  showYearPicker = !showYearPicker;
246
246
  }
247
247
 
248
- function checkDropdownPosition() {
249
- if (!textboxElement) return;
250
-
251
- const rect = textboxElement.getBoundingClientRect();
252
- const dropdownHeight = 400; // Approximate height of the calendar + time picker dropdown
253
- const spaceBelow = window.innerHeight - rect.bottom;
254
- const spaceAbove = rect.top;
255
-
256
- // If not enough space below and more space above, position above
257
- if (spaceBelow < dropdownHeight && spaceAbove > spaceBelow) {
258
- dropdownPosition = 'above';
259
- } else {
260
- dropdownPosition = 'below';
261
- }
262
- }
263
-
264
248
  function onClickOutside(event: MouseEvent) {
265
249
  if (!showCalendar) return;
266
250
 
@@ -311,7 +295,6 @@
311
295
  bind:textboxElement
312
296
  value={readableDate}
313
297
  onfocus={() => {
314
- checkDropdownPosition();
315
298
  showCalendar = true;
316
299
  }}
317
300
  onkeyup={onDateTextChange}
@@ -334,7 +317,13 @@
334
317
  {/snippet}
335
318
  <div class="date-picker">
336
319
  {#if showCalendar}
337
- <div class="calendar {dropdownPosition}" transition:fade={{ duration: 200 }} use:clickOutside={onClickOutside}>
320
+ <!-- Rendered in the top layer so a scrolling Dialog body cannot clip it -->
321
+ <div
322
+ class="calendar"
323
+ transition:fade={{ duration: 200 }}
324
+ use:floating={{ anchor: () => textboxElement, placement: 'bottom-start', offset: 4 }}
325
+ use:clickOutside={onClickOutside}
326
+ >
338
327
  <div class="month-buttons">
339
328
  {#if showYearPicker}
340
329
  <ActionIcon
@@ -405,8 +394,8 @@
405
394
 
406
395
  <TimePicker
407
396
  bind:value={selectedTime}
408
- minDate={minDate}
409
- maxDate={maxDate}
397
+ {minDate}
398
+ {maxDate}
410
399
  selectedDate={selectedDate?.format('YYYY-MM-DD')}
411
400
  onchange={(t) => {
412
401
  selectedTime = t!;
@@ -439,10 +428,10 @@
439
428
  line-height: 0;
440
429
  }
441
430
 
431
+ /* Position and top-layer promotion come from use:floating */
442
432
  .calendar {
443
433
  width: 18rem;
444
434
  text-align: center;
445
- position: absolute;
446
435
  z-index: 10;
447
436
  user-select: none;
448
437
  background-color: var(--paper-body-bg);
@@ -452,14 +441,6 @@
452
441
  padding: 0.5rem;
453
442
  }
454
443
 
455
- .calendar.below {
456
- top: 0
457
- }
458
-
459
- .calendar.above {
460
- bottom: 2.25rem;
461
- }
462
-
463
444
  .month-buttons {
464
445
  display: flex;
465
446
  justify-content: space-between;
@@ -108,9 +108,7 @@
108
108
  children
109
109
  }: NumberProps = $props();
110
110
 
111
- let resolvedName = $derived(
112
- (name || label || autoName).replace(/[^a-zA-Z0-9_\-:.]/g, '_')
113
- );
111
+ let resolvedName = $derived((name || label || autoName).replace(/[^a-zA-Z0-9_\-:.]/g, '_'));
114
112
 
115
113
  let id = $derived.by(() => {
116
114
  if (inputId) return inputId;
@@ -52,9 +52,7 @@
52
52
  }: RadioGroupProps = $props();
53
53
 
54
54
  const autoName = `radio-${crypto.randomUUID()}`;
55
- const resolvedName = $derived(
56
- (name || label || autoName).replace(/[^a-zA-Z0-9_\-:.]/g, '_')
57
- );
55
+ const resolvedName = $derived((name || label || autoName).replace(/[^a-zA-Z0-9_\-:.]/g, '_'));
58
56
 
59
57
  function handleChange(option: ComboBoxItem) {
60
58
  if (option.value === value) return;
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { clickOutside } from '../util/ClickOutside.js';
3
+ import { floating } from '../util/Floating.js';
3
4
  import type {
4
5
  ComboBoxGroup,
5
6
  ComboBoxItem,
@@ -209,6 +210,7 @@
209
210
  }
210
211
 
211
212
  let comboBoxEl: ReturnType<typeof ComboBoxMulti> | undefined = $state(undefined);
213
+ let containerElement: HTMLElement | undefined = $state(undefined);
212
214
  let textboxElement: HTMLElement | undefined = $state(undefined);
213
215
  let buttonElement: HTMLElement | undefined = $state(undefined);
214
216
  let comboBoxKeyDown: ((e: KeyboardEvent) => void) | undefined = $state(undefined);
@@ -253,7 +255,7 @@
253
255
  });
254
256
  </script>
255
257
 
256
- <div class="select-container {containerClass}">
258
+ <div class="select-container {containerClass}" bind:this={containerElement}>
257
259
  <FormGroup {label} {required} {tooltipContent} {tooltipText} {tooltipLocation}>
258
260
  {#if computedAllowSearch && open}
259
261
  <div class="select-input-wrapper">
@@ -375,26 +377,28 @@
375
377
  </button>
376
378
  {/if}
377
379
  </FormGroup>
378
- <div
379
- class="select-panel
380
- {isEmpty ? 'bottom-0' : 'top-full'}
381
- {open ? '' : 'hidden'}"
382
- >
383
- <div class="select-panel-inner" use:clickOutside={closePopover}>
384
- <ComboBoxMulti
385
- bind:this={comboBoxEl}
386
- {filterString}
387
- values={rawValue ? [rawValue] : []}
388
- {onSelection}
389
- onkeydown={comboBoxKeyDown}
390
- groupedOptions={core.filteredGroups}
391
- {open}
392
- loading={core.searching}
393
- {hideNoResults}
394
- {loadingText}
395
- />
380
+ {#if open}
381
+ <!-- Rendered in the top layer so a scrolling Dialog body cannot clip it -->
382
+ <div
383
+ class="select-panel"
384
+ use:floating={{ anchor: () => containerElement, placement: 'bottom-start', matchWidth: true }}
385
+ >
386
+ <div class="select-panel-inner" use:clickOutside={closePopover}>
387
+ <ComboBoxMulti
388
+ bind:this={comboBoxEl}
389
+ {filterString}
390
+ values={rawValue ? [rawValue] : []}
391
+ {onSelection}
392
+ onkeydown={comboBoxKeyDown}
393
+ groupedOptions={core.filteredGroups}
394
+ {open}
395
+ loading={core.searching}
396
+ {hideNoResults}
397
+ {loadingText}
398
+ />
399
+ </div>
396
400
  </div>
397
- </div>
401
+ {/if}
398
402
  {#if showError && errorText}
399
403
  <div class="text-error text-xs">{errorText}</div>
400
404
  {/if}
@@ -473,14 +477,11 @@
473
477
  right: 0.4rem;
474
478
  }
475
479
 
480
+ /* Position, width and top-layer promotion come from use:floating */
476
481
  .select-panel {
477
- position: absolute;
478
- width: 100%;
479
- left: 0;
480
482
  z-index: 70;
481
483
  border-top-left-radius: 0;
482
484
  border-top-right-radius: 0;
483
- /* absolute w-full left-0 z-20 */
484
485
  }
485
486
 
486
487
  .select-panel-inner {
@@ -493,10 +494,6 @@
493
494
  /* text-zinc-400 dark:text-neutral-500 */
494
495
  }
495
496
 
496
- .hidden {
497
- display: none;
498
- }
499
-
500
497
  .disabled {
501
498
  opacity: 0.5;
502
499
  cursor: not-allowed;