@placeholderco/placeholder-ui 2.1.1 → 2.2.1

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 (87) hide show
  1. package/dist/app.css +0 -10
  2. package/dist/editors/CodeMirror.svelte +295 -297
  3. package/dist/editors/tiptap/LinkActionMenu.svelte +6 -1
  4. package/dist/editors/tiptap/TipTap.svelte +1 -1
  5. package/dist/editors/tiptap/extensions/Alert.js +17 -17
  6. package/dist/editors/tiptap/extensions/Div.js +19 -19
  7. package/dist/editors/tiptap/extensions/Span.js +14 -14
  8. package/dist/editors/tiptap/toolbar/AlertButton.svelte +254 -253
  9. package/dist/editors/tiptap/toolbar/AlignCenterButton.svelte +11 -11
  10. package/dist/editors/tiptap/toolbar/AlignJustifyButton.svelte +11 -11
  11. package/dist/editors/tiptap/toolbar/AlignLeftButton.svelte +11 -11
  12. package/dist/editors/tiptap/toolbar/AlignRightButton.svelte +11 -11
  13. package/dist/editors/tiptap/toolbar/BoldButton.svelte +11 -11
  14. package/dist/editors/tiptap/toolbar/BulletListButton.svelte +11 -11
  15. package/dist/editors/tiptap/toolbar/CodeButton.svelte +11 -11
  16. package/dist/editors/tiptap/toolbar/ColorButton.svelte +265 -265
  17. package/dist/editors/tiptap/toolbar/DocumentButton.svelte +51 -51
  18. package/dist/editors/tiptap/toolbar/HeadingButton.svelte +21 -21
  19. package/dist/editors/tiptap/toolbar/HtmlButton.svelte +48 -52
  20. package/dist/editors/tiptap/toolbar/ImageButton.svelte +35 -39
  21. package/dist/editors/tiptap/toolbar/ItalicButton.svelte +11 -11
  22. package/dist/editors/tiptap/toolbar/LinkButton.svelte +249 -249
  23. package/dist/editors/tiptap/toolbar/OrderedListButton.svelte +11 -11
  24. package/dist/editors/tiptap/toolbar/RedoButton.svelte +11 -11
  25. package/dist/editors/tiptap/toolbar/StrikethroughButton.svelte +11 -11
  26. package/dist/editors/tiptap/toolbar/SubscriptButton.svelte +11 -11
  27. package/dist/editors/tiptap/toolbar/SuperscriptButton.svelte +11 -11
  28. package/dist/editors/tiptap/toolbar/TableButton.svelte +231 -224
  29. package/dist/editors/tiptap/toolbar/ToolbarButton.svelte +59 -65
  30. package/dist/editors/tiptap/toolbar/UnderlineButton.svelte +11 -11
  31. package/dist/editors/tiptap/toolbar/UndoButton.svelte +11 -11
  32. package/dist/form/Autocomplete.svelte +24 -22
  33. package/dist/form/AutocompleteMulti.svelte +24 -22
  34. package/dist/form/Checkbox.svelte +4 -4
  35. package/dist/form/Chips.svelte +15 -7
  36. package/dist/form/ColorPicker.svelte +5 -20
  37. package/dist/form/ComboBox.svelte +4 -4
  38. package/dist/form/ComboBoxItemBuilder.svelte +14 -32
  39. package/dist/form/ComboBoxMulti.svelte +7 -3
  40. package/dist/form/CronBuilder.svelte +32 -110
  41. package/dist/form/DatePicker.svelte +9 -28
  42. package/dist/form/DateRangePicker.svelte +46 -36
  43. package/dist/form/DateTimePicker.svelte +11 -30
  44. package/dist/form/Number.svelte +1 -3
  45. package/dist/form/RadioGroup.svelte +1 -3
  46. package/dist/form/Select.svelte +25 -28
  47. package/dist/form/SelectMulti.svelte +36 -54
  48. package/dist/form/StringArrayBuilder.svelte +1 -3
  49. package/dist/form/TextArea.svelte +5 -6
  50. package/dist/form/Textbox.svelte +10 -4
  51. package/dist/form/TimePicker.svelte +10 -4
  52. package/dist/icon/Icon.svelte +8 -4
  53. package/dist/index.d.ts +147 -146
  54. package/dist/index.js +78 -77
  55. package/dist/layout/CustomNavbar.svelte +1 -1
  56. package/dist/layout/Navbar.svelte +3 -4
  57. package/dist/layout/Navbar.svelte.d.ts +1 -2
  58. package/dist/layout/NavbarItemDisplay.svelte +1 -1
  59. package/dist/layout/NavbarItemView.svelte +3 -1
  60. package/dist/layout/Sidenav.svelte +1 -1
  61. package/dist/models/ComboBoxItem.d.ts +4 -4
  62. package/dist/models/NotifyModel.js +0 -1
  63. package/dist/theme.svelte.d.ts +1 -1
  64. package/dist/theme.svelte.js +46 -22
  65. package/dist/ui/Accordion.svelte +1 -3
  66. package/dist/ui/AccordionItem.svelte +8 -16
  67. package/dist/ui/Badge.svelte +11 -19
  68. package/dist/ui/Button.svelte +15 -17
  69. package/dist/ui/ButtonVariant.d.ts +1 -7
  70. package/dist/ui/ContextMenu.svelte +5 -11
  71. package/dist/ui/Dialog.svelte +74 -30
  72. package/dist/ui/Dialog.svelte.d.ts +8 -2
  73. package/dist/ui/Dropdown.svelte +21 -10
  74. package/dist/ui/MultiSortable.svelte +1 -3
  75. package/dist/ui/Pagination.svelte +2 -2
  76. package/dist/ui/Popover.svelte +16 -37
  77. package/dist/ui/ProgressBar.svelte +4 -10
  78. package/dist/ui/ProgressBarVariant.d.ts +2 -7
  79. package/dist/ui/ThemeSwitcher.svelte +2 -1
  80. package/dist/ui/Tooltip.svelte +1 -37
  81. package/dist/util/DateFunctions.js +1 -1
  82. package/dist/util/Floating.d.ts +70 -0
  83. package/dist/util/Floating.js +321 -0
  84. package/dist/util/NavigateTo.js +2 -2
  85. package/dist/util/Transitions.d.ts +1 -1
  86. package/dist/util/Transitions.js +1 -1
  87. package/package.json +1 -1
@@ -5,11 +5,5 @@
5
5
  * - `primary` → `--ui-primary` (dark brand colour)
6
6
  * - `accent` → `--ui-accent` (light brand colour)
7
7
  * - `tertiary` → `--ui-tertiary` (supporting colour)
8
- *
9
- * The `midnight`, `pistachio` and `turquoise` names are legacy aliases for
10
- * `primary`, `accent` and `tertiary` respectively. They keep working but new code
11
- * should use the semantic names.
12
8
  */
13
- export type ButtonVariant = 'white' | 'secondary' | 'secondary-subtle' | 'secondary-selected' | 'danger' | 'danger-subtle' | 'auto-subtle' | 'auto-outline' | 'primary' | 'primary-outline' | 'primary-subtle' | 'accent' | 'accent-outline' | 'accent-subtle' | 'tertiary' | 'tertiary-subtle' | 'shaded' | LegacyButtonVariant;
14
- /** @deprecated Use `primary`, `accent` and `tertiary` instead. */
15
- export type LegacyButtonVariant = 'pistachio' | 'pistachio-outline' | 'pistachio-subtle' | 'midnight' | 'midnight-outline' | 'midnight-subtle' | 'turquoise' | 'turquoise-subtle';
9
+ export type ButtonVariant = 'white' | 'secondary' | 'secondary-subtle' | 'secondary-selected' | 'danger' | 'danger-subtle' | 'auto-subtle' | 'auto-outline' | 'primary' | 'primary-outline' | 'primary-subtle' | 'accent' | 'accent-outline' | 'accent-subtle' | 'tertiary' | 'tertiary-subtle' | 'shaded';
@@ -38,6 +38,7 @@
38
38
 
39
39
  <script lang="ts">
40
40
  import ContextMenuList from './ContextMenuList.svelte';
41
+ import { topLayer } from '../util/Floating.js';
41
42
 
42
43
  let {
43
44
  open = $bindable(false),
@@ -54,16 +55,6 @@
54
55
  let posY = $state(0);
55
56
  let placed = $state(false);
56
57
 
57
- /** Move the menu to <body> so it can't be clipped by overflow/transform ancestors. */
58
- function portal(node: HTMLElement) {
59
- document.body.appendChild(node);
60
- return {
61
- destroy() {
62
- node.remove();
63
- }
64
- };
65
- }
66
-
67
58
  function close() {
68
59
  if (!open) return;
69
60
  open = false;
@@ -127,8 +118,11 @@
127
118
 
128
119
  {#if open}
129
120
  <!-- svelte-ignore a11y_no_static_element_interactions -->
121
+ <!-- Promoted to the top layer so overflow/transform ancestors cannot clip it. Unlike a
122
+ body portal, the menu stays inside its subtree, so it remains usable inside a
123
+ modal Dialog (everything outside an open modal is inert). -->
130
124
  <div
131
- use:portal
125
+ use:topLayer
132
126
  bind:this={menuEl}
133
127
  class="pui-ctxmenu {classes}"
134
128
  style="left: {posX}px; top: {posY}px; visibility: {placed ? 'visible' : 'hidden'};"
@@ -25,10 +25,16 @@
25
25
  title?: string | undefined;
26
26
  /** Render without the Paper wrapper component */
27
27
  noPaper?: boolean;
28
- /** Prevent content from overflowing the dialog bounds */
28
+ /**
29
+ * Let content overflow the dialog bounds instead of scrolling inside it.
30
+ * The body scrolls by default; dropdowns, pickers and tooltips render in
31
+ * the browser's top layer, so they are never clipped by that scroll.
32
+ */
33
+ allowOverflow?: boolean;
34
+ /** @deprecated Scrolling is now the default; use `allowOverflow` to opt out. */
29
35
  preventOverflow?: boolean;
30
36
  /** Size preset for the dialog width */
31
- size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
37
+ size?: 'md' | 'lg' | 'xl' | 'full';
32
38
  /** Additional CSS classes */
33
39
  class?: string;
34
40
  /** Footer content (typically action buttons) */
@@ -42,6 +48,7 @@
42
48
  allowImplicitClose = true,
43
49
  title = undefined,
44
50
  noPaper = false,
51
+ allowOverflow = false,
45
52
  preventOverflow = false,
46
53
  size = 'md',
47
54
  class: classes = '',
@@ -50,6 +57,12 @@
50
57
  }: DialogProps = $props();
51
58
 
52
59
  const uid = $props.id();
60
+
61
+ // preventOverflow is the legacy opt-in for the behaviour that is now default,
62
+ // so it wins over allowOverflow when both are set
63
+ const overflowClass = $derived(
64
+ allowOverflow && !preventOverflow ? 'overflow-visible' : 'overflow-y-auto'
65
+ );
53
66
  const titleId = `${uid}-title`;
54
67
 
55
68
  let dialogElement: HTMLDialogElement | undefined = $state(undefined);
@@ -129,12 +142,12 @@
129
142
  {#snippet DialogInner()}
130
143
  {#if title}
131
144
  <div class="dialog-title">
132
- <span id={titleId} class="font-bold">{title}</span>
145
+ <span id={titleId} class="dialog-title-text">{title}</span>
133
146
  {#if allowImplicitClose}
134
147
  <ActionIcon
135
148
  onclick={() => (show = false)}
136
149
  variant="secondary-subtle"
137
- class="flex !p-[0.3rem] mr-1 !leading-0"
150
+ class="dialog-close"
138
151
  svg={iconX}
139
152
  size="1rem"
140
153
  ariaLabel="Close dialog"
@@ -142,7 +155,7 @@
142
155
  {/if}
143
156
  </div>
144
157
  {/if}
145
- <div class="dialog-body {preventOverflow ? 'overflow-y-auto' : 'overflow-visible'}">
158
+ <div class="dialog-body {overflowClass}">
146
159
  {@render children?.()}
147
160
  </div>
148
161
  {#if footer}
@@ -169,8 +182,9 @@
169
182
  {#if noPaper}
170
183
  {@render DialogInner()}
171
184
  {:else}
172
- <div class="dialog-paper {preventOverflow ? 'overflow-y-auto' : 'overflow-visible'}">
173
- <Paper containerClass={classes}>
185
+ <div class="dialog-paper">
186
+ <!-- Dialog owns the section padding so header and footer dividers span the card -->
187
+ <Paper containerClass={classes} noPadding noGap>
174
188
  {@render DialogInner()}
175
189
  </Paper>
176
190
  </div>
@@ -247,7 +261,7 @@
247
261
  background-color: transparent;
248
262
  overflow: visible;
249
263
  margin: auto;
250
- max-width: min(calc(100vw - 2rem), calc(100% - 6px));
264
+ max-width: calc(100% - 2rem);
251
265
  /* Cap to the visible viewport with equal margins; the flex chain below
252
266
  (dialog > inner > paper > body, each min-height: 0) passes this limit
253
267
  down so the body shrinks instead of spilling past the bottom edge.
@@ -281,36 +295,77 @@
281
295
  min-height: 0;
282
296
  }
283
297
 
284
- /* Paper sits inside the height chain; .paper-body already has an inline
285
- min-height: 0, so only .paper needs the shrink link */
286
- .dialog :global(.paper) {
298
+ /* Paper sits inside the height chain. Without a gap its body is a plain
299
+ block, so make it a shrinkable flex column here; the sections below rely
300
+ on that to give the body the leftover height */
301
+ .dialog :global(.paper),
302
+ .dialog :global(.paper-body) {
303
+ display: flex;
304
+ flex-direction: column;
287
305
  min-height: 0;
288
306
  }
289
307
 
308
+ /* Section padding: 1rem inset, 0.75rem vertical for the header and footer,
309
+ 1rem for the body. Dividers run edge to edge */
290
310
  .dialog-title {
291
311
  display: flex;
312
+ align-items: center;
292
313
  justify-content: space-between;
293
- margin-bottom: 0.5rem;
294
- padding-bottom: 0.25rem;
314
+ gap: 0.75rem;
315
+ padding: 0.75rem 1rem;
295
316
  border-bottom: 1px solid var(--border-color);
317
+ flex-shrink: 0;
318
+ }
319
+
320
+ .dialog-title-text {
321
+ font-size: 1.0625rem;
296
322
  font-weight: 600;
323
+ line-height: 1.4;
324
+ min-width: 0;
325
+ overflow-wrap: anywhere;
326
+ }
327
+
328
+ /* Keep the close button from setting the header height: its hit area
329
+ overhangs the text line and the right inset instead */
330
+ .dialog-title :global(button.dialog-close) {
297
331
  flex-shrink: 0;
332
+ padding: 0.25rem;
333
+ margin: -0.25rem -0.5rem -0.25rem 0;
334
+ line-height: 0;
298
335
  }
299
336
 
337
+ /* The only scroll container. Padding lives inside it so focus rings and
338
+ negative-margin rows are not clipped, and the scrollbar hugs the edge */
300
339
  .dialog-body {
301
340
  display: flex;
302
341
  flex-direction: column;
303
342
  min-width: 0;
304
343
  min-height: 0;
305
344
  flex-shrink: 1;
345
+ padding: 1rem;
346
+ }
347
+
348
+ /* The body padding is the spacing; don't let the first and last elements
349
+ (or a wrapper's first and last) add their own margins on top of it */
350
+ .dialog-body > :global(:first-child),
351
+ .dialog-body > :global(:first-child > :first-child) {
352
+ margin-top: 0;
353
+ }
354
+
355
+ .dialog-body > :global(:last-child),
356
+ .dialog-body > :global(:last-child > :last-child) {
357
+ margin-bottom: 0;
306
358
  }
307
359
 
360
+ /* Actions align right. A secondary action can sit on the left with
361
+ `margin-right: auto` */
308
362
  .dialog-footer {
309
363
  display: flex;
310
- justify-content: space-between;
311
- padding-top: 0.5rem;
312
- border-top: 1px solid var(--border-color);
364
+ align-items: center;
365
+ justify-content: flex-end;
313
366
  gap: 0.5rem;
367
+ padding: 0.75rem 1rem;
368
+ border-top: 1px solid var(--border-color);
314
369
  flex-shrink: 0;
315
370
  }
316
371
 
@@ -322,33 +377,22 @@
322
377
  overflow-y: auto;
323
378
  }
324
379
 
325
- .dialog-no-paper {
326
- padding: 1rem;
327
- }
328
-
329
380
  .dialog-paper,
330
381
  .dialog-no-paper {
331
382
  display: flex;
332
383
  flex-direction: column;
333
- box-sizing: border-box;
334
- border-radius: 0.25rem;
335
- background: #f1f3f5;
336
384
  min-height: 0;
337
385
  }
338
386
 
339
- :global(.dark .dialog-paper),
340
- :global(.dark .dialog-no-paper) {
341
- background-color: rgb(33, 34, 38);
387
+ .dialog-no-paper {
388
+ border-radius: 0.25rem;
389
+ background-color: var(--paper-body-bg);
342
390
  }
343
391
 
344
392
  dialog::backdrop {
345
393
  background-color: #0008;
346
394
  }
347
395
 
348
- .dialog.sm {
349
- width: 20rem;
350
- }
351
-
352
396
  .dialog.md {
353
397
  width: 30rem;
354
398
  }
@@ -18,10 +18,16 @@ export interface DialogProps {
18
18
  title?: string | undefined;
19
19
  /** Render without the Paper wrapper component */
20
20
  noPaper?: boolean;
21
- /** Prevent content from overflowing the dialog bounds */
21
+ /**
22
+ * Let content overflow the dialog bounds instead of scrolling inside it.
23
+ * The body scrolls by default; dropdowns, pickers and tooltips render in
24
+ * the browser's top layer, so they are never clipped by that scroll.
25
+ */
26
+ allowOverflow?: boolean;
27
+ /** @deprecated Scrolling is now the default; use `allowOverflow` to opt out. */
22
28
  preventOverflow?: boolean;
23
29
  /** Size preset for the dialog width */
24
- size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
30
+ size?: 'md' | 'lg' | 'xl' | 'full';
25
31
  /** Additional CSS classes */
26
32
  class?: string;
27
33
  /** Footer content (typically action buttons) */
@@ -5,6 +5,8 @@
5
5
  import type { Hyperlink } from '../models/Hyperlink.js';
6
6
  import LinkCollection from '../display/LinkCollection.svelte';
7
7
  import { iconChevronDown } from '../icon/index.js';
8
+ import { floating } from '../util/Floating.js';
9
+ import { clickOutside } from '../util/ClickOutside.js';
8
10
 
9
11
  export interface DropdownProps {
10
12
  /** Whether the dropdown is expanded (bindable) */
@@ -32,16 +34,32 @@
32
34
  class: classes = '',
33
35
  alignToButton = 'left'
34
36
  }: DropdownProps = $props();
37
+
38
+ let triggerElement: HTMLElement | undefined = $state(undefined);
35
39
  </script>
36
40
 
37
- <div class="dropdown {classes} {alignToButton}">
41
+ <!-- The floating content is still a DOM descendant of this wrapper (only its rendering
42
+ moves to the top layer), so a contains() check covers both the button and the menu -->
43
+ <div
44
+ class="dropdown {classes} {alignToButton}"
45
+ bind:this={triggerElement}
46
+ use:clickOutside={() => (show = false)}
47
+ >
38
48
  <Button class="ps-2" {variant} svg={buttonSvg} onclick={() => (show = !show)}>
39
49
  {label}
40
50
  </Button>
41
51
  {#if show}
52
+ <!-- Rendered in the top layer so a scrolling Dialog body cannot clip it -->
42
53
  <!-- svelte-ignore a11y_click_events_have_key_events -->
43
54
  <!-- svelte-ignore a11y_no_static_element_interactions -->
44
- <div class="dropdown-content" onclick={(e) => e.stopPropagation()}>
55
+ <div
56
+ class="dropdown-content"
57
+ use:floating={{
58
+ anchor: () => triggerElement,
59
+ placement: alignToButton === 'right' ? 'bottom-end' : 'bottom-start'
60
+ }}
61
+ onclick={(e) => e.stopPropagation()}
62
+ >
45
63
  {#if links.length > 0 && Array.isArray(links[0])}
46
64
  {#each links as group}
47
65
  <div class="dropdown-group">
@@ -60,10 +78,8 @@
60
78
  position: relative;
61
79
  }
62
80
 
81
+ /* Position and top-layer promotion come from use:floating */
63
82
  .dropdown-content {
64
- position: absolute;
65
- top: 100%;
66
- left: 0;
67
83
  background-color: var(--paper-body-bg);
68
84
  min-width: 160px;
69
85
  box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
@@ -94,11 +110,6 @@
94
110
  background-color: var(--border-color);
95
111
  }
96
112
 
97
- .dropdown.right .dropdown-content {
98
- left: auto;
99
- right: 0;
100
- }
101
-
102
113
  :global(.dark) {
103
114
  .dropdown-content :global(a) {
104
115
  color: var(--ui-accent);
@@ -114,9 +114,7 @@
114
114
  if (!accepts(ctx)) return;
115
115
  const host = container;
116
116
  if (!host) return;
117
- const itemEls = Array.from(
118
- host.querySelectorAll<HTMLElement>(':scope > .pui-msortable-item')
119
- );
117
+ const itemEls = Array.from(host.querySelectorAll<HTMLElement>(':scope > .pui-msortable-item'));
120
118
  if (itemEls.length === 0) {
121
119
  dropIndex = null;
122
120
  return;
@@ -48,7 +48,7 @@
48
48
  // For maxPageButtons, we need to fit: first, last, and some middle pages + ellipsis
49
49
  // Reserve 2 slots for first and last pages, leaving maxPageButtons - 2 for middle + ellipsis
50
50
  const middleSlots = maxPageButtons - 2;
51
-
51
+
52
52
  // Calculate how many pages we can show around the selected page
53
53
  const sidePages = Math.floor((middleSlots - 1) / 2); // -1 to account for potential ellipsis
54
54
 
@@ -134,7 +134,7 @@
134
134
  display: flex;
135
135
  justify-content: center;
136
136
  }
137
-
137
+
138
138
  :global(.dark) .pagination-container :global(.paginate-button.secondary-selected) {
139
139
  color: black;
140
140
  }
@@ -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}
@@ -135,20 +132,17 @@
135
132
  }
136
133
 
137
134
  /* Variant colors */
138
- .primary,
139
- .midnight {
135
+ .primary {
140
136
  background-color: var(--ui-primary);
141
137
  color: #fff;
142
138
  }
143
139
 
144
- .accent,
145
- .pistachio {
140
+ .accent {
146
141
  background-color: var(--ui-accent);
147
142
  color: var(--ui-primary);
148
143
  }
149
144
 
150
- .tertiary,
151
- .turquoise {
145
+ .tertiary {
152
146
  background-color: var(--ui-tertiary);
153
147
  color: var(--ui-primary);
154
148
  }
@@ -1,7 +1,2 @@
1
- /**
2
- * Colour variants for ProgressBar. `midnight`, `pistachio` and `turquoise` are
3
- * legacy aliases for `primary`, `accent` and `tertiary`.
4
- */
5
- export type ProgressBarVariant = 'primary' | 'accent' | 'tertiary' | 'danger' | 'secondary' | LegacyProgressBarVariant;
6
- /** @deprecated Use `primary`, `accent` and `tertiary` instead. */
7
- export type LegacyProgressBarVariant = 'midnight' | 'pistachio' | 'turquoise';
1
+ /** Colour variants for ProgressBar, named after the `--ui-*` tokens. */
2
+ export type ProgressBarVariant = 'primary' | 'accent' | 'tertiary' | 'danger' | 'secondary';
@@ -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();