@lyeve-labs/ui-kit 0.23.2 → 0.23.3
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.
- package/dist/components/Autocomplete.svelte +9 -3
- package/dist/components/Dropdown.svelte +31 -23
- package/dist/components/MultiSelect.svelte +15 -3
- package/dist/components/Select.svelte +3 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/internal/panel.d.ts +65 -12
- package/dist/internal/panel.js +136 -13
- package/package.json +1 -1
|
@@ -42,7 +42,13 @@
|
|
|
42
42
|
} from '../internal/field.js';
|
|
43
43
|
import { applyFilter, type FilterInput } from '../internal/filter.js';
|
|
44
44
|
import { createListbox } from '../internal/listbox.svelte.js';
|
|
45
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
PANEL_EMPTY,
|
|
47
|
+
PANEL_LIST,
|
|
48
|
+
PANEL_SURFACE,
|
|
49
|
+
panelOption,
|
|
50
|
+
placePanel,
|
|
51
|
+
} from '../internal/panel.js';
|
|
46
52
|
|
|
47
53
|
interface Props {
|
|
48
54
|
/** The chosen option's value. Empty for none. */
|
|
@@ -228,8 +234,8 @@
|
|
|
228
234
|
{/if}
|
|
229
235
|
|
|
230
236
|
{#if box.open}
|
|
231
|
-
<div class="{PANEL_SURFACE} w-full">
|
|
232
|
-
<div class={PANEL_LIST} use:panel {...box.listAttrs}>
|
|
237
|
+
<div use:placePanel class="{PANEL_SURFACE} w-full">
|
|
238
|
+
<div class={PANEL_LIST} data-panel-list use:panel {...box.listAttrs}>
|
|
233
239
|
{#each rows as option, index (option.value)}
|
|
234
240
|
{@const isSelected = option.value === value}
|
|
235
241
|
<!--
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* and Escape and Tab both close and hand focus back to the trigger.
|
|
13
13
|
*/
|
|
14
14
|
import type { Component, Snippet } from 'svelte';
|
|
15
|
+
import { PANEL_LIST, PANEL_SURFACE, placePanel } from '../internal/panel.js';
|
|
15
16
|
|
|
16
17
|
interface DropdownItem {
|
|
17
18
|
label: string;
|
|
@@ -242,33 +243,40 @@
|
|
|
242
243
|
{@render trigger({ open, toggle })}
|
|
243
244
|
|
|
244
245
|
{#if open}
|
|
246
|
+
<!--
|
|
247
|
+
The surface and the list come from the shared panel module, so a menu of
|
|
248
|
+
eighty items scrolls inside the same cap as a listbox and opens upward
|
|
249
|
+
when the room under the trigger has run out. The rows keep their own
|
|
250
|
+
classes: a danger row hovers in its own tint, which the shared option
|
|
251
|
+
row has no state for.
|
|
252
|
+
-->
|
|
245
253
|
<div
|
|
246
254
|
bind:this={menuEl}
|
|
247
255
|
role="menu"
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
{align === 'right' ? 'end-0' : 'start-0'}"
|
|
256
|
+
use:placePanel
|
|
257
|
+
class="{PANEL_SURFACE} min-w-36 {align === 'right' ? 'end-0' : 'start-0'}"
|
|
251
258
|
>
|
|
252
|
-
{
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
259
|
+
<div class={PANEL_LIST} data-panel-list>
|
|
260
|
+
{#each items as item, index}
|
|
261
|
+
<button
|
|
262
|
+
type="button"
|
|
263
|
+
role="menuitem"
|
|
264
|
+
disabled={item.disabled}
|
|
265
|
+
tabindex={index === active ? 0 : -1}
|
|
266
|
+
onclick={() => handleItemClick(item)}
|
|
267
|
+
class="flex w-full items-center gap-2.5 rounded-lg px-2 py-2 text-sm transition-colors
|
|
268
|
+
duration-150 outline-none focus-visible:ring-2 focus-visible:ring-inset
|
|
269
|
+
focus-visible:ring-brand disabled:cursor-not-allowed disabled:opacity-40
|
|
270
|
+
{item.variant === 'danger' ? 'text-danger hover:bg-danger/10' : 'hover:bg-surface-2'}"
|
|
271
|
+
>
|
|
272
|
+
{#if item.icon}
|
|
273
|
+
{@const Icon = item.icon}
|
|
274
|
+
<Icon size={14} class="shrink-0" />
|
|
275
|
+
{/if}
|
|
276
|
+
{item.label}
|
|
277
|
+
</button>
|
|
278
|
+
{/each}
|
|
279
|
+
</div>
|
|
272
280
|
</div>
|
|
273
281
|
{/if}
|
|
274
282
|
</div>
|
|
@@ -18,7 +18,13 @@
|
|
|
18
18
|
describedBy,
|
|
19
19
|
} from '../internal/field.js';
|
|
20
20
|
import { createListbox } from '../internal/listbox.svelte.js';
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
PANEL_EMPTY,
|
|
23
|
+
PANEL_LIST,
|
|
24
|
+
PANEL_SURFACE,
|
|
25
|
+
panelOption,
|
|
26
|
+
placePanel,
|
|
27
|
+
} from '../internal/panel.js';
|
|
22
28
|
import type { ListOption } from './Autocomplete.svelte';
|
|
23
29
|
|
|
24
30
|
interface Props {
|
|
@@ -246,7 +252,7 @@
|
|
|
246
252
|
</div>
|
|
247
253
|
|
|
248
254
|
{#if box.open}
|
|
249
|
-
<div class="{PANEL_SURFACE} w-full">
|
|
255
|
+
<div use:placePanel class="{PANEL_SURFACE} w-full">
|
|
250
256
|
{#if searchable}
|
|
251
257
|
<div class="border-b border-line p-2">
|
|
252
258
|
<input
|
|
@@ -268,7 +274,13 @@
|
|
|
268
274
|
</div>
|
|
269
275
|
{/if}
|
|
270
276
|
|
|
271
|
-
<div
|
|
277
|
+
<div
|
|
278
|
+
class={PANEL_LIST}
|
|
279
|
+
data-panel-list
|
|
280
|
+
use:panel
|
|
281
|
+
{...box.listAttrs}
|
|
282
|
+
aria-multiselectable="true"
|
|
283
|
+
>
|
|
272
284
|
{#each rows as option, index (option.value)}
|
|
273
285
|
{@const isSelected = chosen.has(option.value)}
|
|
274
286
|
<!--
|
|
@@ -82,6 +82,7 @@
|
|
|
82
82
|
PANEL_LIST,
|
|
83
83
|
PANEL_SURFACE,
|
|
84
84
|
panelOption,
|
|
85
|
+
placePanel,
|
|
85
86
|
} from '../internal/panel.js';
|
|
86
87
|
|
|
87
88
|
interface Props {
|
|
@@ -407,7 +408,7 @@
|
|
|
407
408
|
<input type="hidden" {name} {disabled} value={value ?? ''} />
|
|
408
409
|
|
|
409
410
|
{#if box.open}
|
|
410
|
-
<div class="{PANEL_SURFACE} w-full">
|
|
411
|
+
<div use:placePanel class="{PANEL_SURFACE} w-full">
|
|
411
412
|
{#if searchable}
|
|
412
413
|
<div class="border-b border-line p-2">
|
|
413
414
|
<!--
|
|
@@ -437,7 +438,7 @@
|
|
|
437
438
|
</div>
|
|
438
439
|
{/if}
|
|
439
440
|
|
|
440
|
-
<div class={PANEL_LIST} use:panel {...box.listAttrs}>
|
|
441
|
+
<div class={PANEL_LIST} data-panel-list use:panel {...box.listAttrs}>
|
|
441
442
|
{#each blocks as block, position (position)}
|
|
442
443
|
{#if block.group !== undefined}
|
|
443
444
|
<!-- Named once, on the group. The heading repeats it on screen. -->
|
package/dist/index.d.ts
CHANGED
|
@@ -89,4 +89,4 @@ export type { ConfirmOptions } from './components/dialog/dialog-manager.svelte.j
|
|
|
89
89
|
export type { DialogOptions, DialogEntry, DialogSize } from './components/dialog/types.js';
|
|
90
90
|
export { cn, type ClassValue } from './utils/cn.js';
|
|
91
91
|
export { getTheme, getThemePreference, nextThemePreference, resolveTheme, setTheme, setThemePreference, systemTheme, themeBootScript, toggleTheme, watchSystemTheme, THEME_PREFERENCES, type Theme, type ThemePreference, } from './utils/theme.js';
|
|
92
|
-
export declare const VERSION = "0.23.
|
|
92
|
+
export declare const VERSION = "0.23.3";
|
package/dist/index.js
CHANGED
|
@@ -96,4 +96,4 @@ export { getTheme, getThemePreference, nextThemePreference, resolveTheme, setThe
|
|
|
96
96
|
// ── Version ────────────────────────────────────────────────────────────────
|
|
97
97
|
// Generated from package.json by `pnpm version:sync`. Bump package.json, never
|
|
98
98
|
// this line; the build and the test suite fail when the two disagree.
|
|
99
|
-
export const VERSION = '0.23.
|
|
99
|
+
export const VERSION = '0.23.3';
|
package/dist/internal/panel.d.ts
CHANGED
|
@@ -26,10 +26,15 @@
|
|
|
26
26
|
* Width stays at the call site: a menu sized to its trigger and a calendar
|
|
27
27
|
* sized to seven columns are different requirements. Everything else is here.
|
|
28
28
|
*
|
|
29
|
-
*
|
|
29
|
+
* Two rules hold these together. No row carries two utilities for the same
|
|
30
30
|
* property. Colour and background sit on the surface, and a row states only
|
|
31
31
|
* the override its state earns, because two utilities for one property resolve
|
|
32
32
|
* in the order Tailwind emits them and not in the order they were written.
|
|
33
|
+
* And a row is inset and rounded, never square and full width: the surface is
|
|
34
|
+
* rounded-xl, so a ring or tint that reaches its edge is cut at the corners on
|
|
35
|
+
* the first and last row. The list pads the rows in by px-1 and each row takes
|
|
36
|
+
* rounded-lg, so what a row paints stays inside the frame with corners of its
|
|
37
|
+
* own.
|
|
33
38
|
*
|
|
34
39
|
* Not exported from the package entry point - this is an implementation detail.
|
|
35
40
|
*/
|
|
@@ -40,24 +45,32 @@
|
|
|
40
45
|
* needs a boundary that clears 3:1, which line does not. It carries the
|
|
41
46
|
* resting text colour so a row can override it with a single utility.
|
|
42
47
|
*
|
|
43
|
-
* overflow-hidden
|
|
44
|
-
*
|
|
45
|
-
*
|
|
48
|
+
* overflow-hidden is a backstop, not the shape: the rows sit inset with
|
|
49
|
+
* corners of their own, so nothing they paint reaches the rounded frame. It
|
|
50
|
+
* still clips a caller's stray full-width child, and it keeps the search
|
|
51
|
+
* field's corners inside the surface's.
|
|
52
|
+
*
|
|
53
|
+
* No vertical offset and no side. Both belong to placePanel, which measures
|
|
54
|
+
* the room and puts the panel above the anchor when there is none below.
|
|
46
55
|
*/
|
|
47
|
-
export declare const PANEL_SURFACE = "absolute z-dropdown
|
|
56
|
+
export declare const PANEL_SURFACE = "absolute z-dropdown overflow-hidden rounded-xl border border-line-strong bg-surface text-fg shadow-2xl";
|
|
48
57
|
/**
|
|
49
58
|
* The scrolling region inside it.
|
|
50
59
|
*
|
|
51
60
|
* The cap is a token, not max-h-60, so a fifth panel cannot pick a different
|
|
52
61
|
* one. overscroll-contain stops a wheel that has reached the end of the list
|
|
53
|
-
* from carrying on into the page behind the open panel.
|
|
62
|
+
* from carrying on into the page behind the open panel. px-1 insets the rows
|
|
63
|
+
* from the frame, so a row's ring and hover tint sit inside the surface's
|
|
64
|
+
* rounded corners instead of being cut by them on the first and last row.
|
|
54
65
|
*/
|
|
55
|
-
export declare const PANEL_LIST = "max-h-panel-max overflow-y-auto overscroll-contain py-1";
|
|
66
|
+
export declare const PANEL_LIST = "max-h-panel-max overflow-y-auto overscroll-contain px-1 py-1";
|
|
56
67
|
/**
|
|
57
68
|
* One row at rest.
|
|
58
69
|
*
|
|
59
70
|
* No background and no text colour of its own: it inherits both from the
|
|
60
71
|
* surface, which leaves each state below a single utility to override.
|
|
72
|
+
* rounded-lg, one step inside the surface's rounded-xl, so the hover tint and
|
|
73
|
+
* the active ring follow the row's own corners.
|
|
61
74
|
*/
|
|
62
75
|
export declare const PANEL_OPTION: string;
|
|
63
76
|
/**
|
|
@@ -65,7 +78,7 @@ export declare const PANEL_OPTION: string;
|
|
|
65
78
|
*
|
|
66
79
|
* A tint alone reads 1.09:1 against the panel, which is not a visible state,
|
|
67
80
|
* so the active row also carries an inset brand ring. Inset because the row
|
|
68
|
-
*
|
|
81
|
+
* fills the list's inner width and an outset ring would run into the frame.
|
|
69
82
|
*/
|
|
70
83
|
export declare const PANEL_OPTION_ACTIVE = "bg-surface-2 ring-1 ring-inset ring-brand";
|
|
71
84
|
/**
|
|
@@ -83,10 +96,18 @@ export declare const PANEL_OPTION_SELECTED = "font-medium text-brand";
|
|
|
83
96
|
* by emitted order, so the row could take the tint and read as choosable.
|
|
84
97
|
*/
|
|
85
98
|
export declare const PANEL_OPTION_DISABLED = "pointer-events-none opacity-40";
|
|
86
|
-
/**
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
99
|
+
/**
|
|
100
|
+
* The line shown when a filter matched nothing.
|
|
101
|
+
*
|
|
102
|
+
* px-2, the same as a row: the list's px-1 supplies the rest, so the text
|
|
103
|
+
* stays flush with row text.
|
|
104
|
+
*/
|
|
105
|
+
export declare const PANEL_EMPTY = "px-2 py-2 text-sm text-faint";
|
|
106
|
+
/**
|
|
107
|
+
* A group heading inside the list. Not focusable, so it takes no row classes,
|
|
108
|
+
* only the row's horizontal padding so it lines up with the labels below it.
|
|
109
|
+
*/
|
|
110
|
+
export declare const PANEL_GROUP_LABEL = "px-2 pb-1 pt-3 text-xs font-medium uppercase text-faint";
|
|
90
111
|
/**
|
|
91
112
|
* Composes the option classes for a row's state.
|
|
92
113
|
*
|
|
@@ -102,3 +123,35 @@ export declare function panelOption(state: {
|
|
|
102
123
|
selected: boolean;
|
|
103
124
|
disabled: boolean;
|
|
104
125
|
}): string;
|
|
126
|
+
/** Marks the scrolling region inside a surface, so placePanel can find it. */
|
|
127
|
+
export declare const PANEL_LIST_ATTR = "data-panel-list";
|
|
128
|
+
/** The classes that put the surface under its anchor. */
|
|
129
|
+
export declare const PANEL_BELOW = "top-full mt-1";
|
|
130
|
+
/** The classes that put the surface over its anchor. */
|
|
131
|
+
export declare const PANEL_ABOVE = "bottom-full mb-1";
|
|
132
|
+
/**
|
|
133
|
+
* Svelte action for a panel surface. Puts it on the side of its anchor that
|
|
134
|
+
* has room, and stops its list running past whatever clips it.
|
|
135
|
+
*
|
|
136
|
+
* <div use:placePanel class="{PANEL_SURFACE} w-full">
|
|
137
|
+
* <div class={PANEL_LIST} data-panel-list>
|
|
138
|
+
*
|
|
139
|
+
* The anchor is the surface's offset parent, the relative wrapper every
|
|
140
|
+
* control puts around its trigger; a DOM without layout has no offset parent,
|
|
141
|
+
* so the parent element stands in. Below is the default, because a list under
|
|
142
|
+
* its field is where a native select puts one. The panel goes above only when
|
|
143
|
+
* its natural height does not fit below and there is more room above, so a
|
|
144
|
+
* long list near the bottom of a modal opens upward instead of into the space
|
|
145
|
+
* under the modal's scroll edge that nobody can reach without scrolling
|
|
146
|
+
* first. Whichever side wins, the list's max-height is the smaller of the
|
|
147
|
+
* token cap and the room on that side, written as a CSS min so the token stays
|
|
148
|
+
* the cap without this code knowing its value.
|
|
149
|
+
*
|
|
150
|
+
* Measured on mount, on resize and on scroll of the clipping ancestor, since
|
|
151
|
+
* each moves the anchor relative to the edge. Scroll does not bubble, so the
|
|
152
|
+
* list's own scrolling never re-measures. A server render has no window and
|
|
153
|
+
* does nothing.
|
|
154
|
+
*/
|
|
155
|
+
export declare function placePanel(node: HTMLElement): {
|
|
156
|
+
destroy(): void;
|
|
157
|
+
};
|
package/dist/internal/panel.js
CHANGED
|
@@ -26,10 +26,15 @@
|
|
|
26
26
|
* Width stays at the call site: a menu sized to its trigger and a calendar
|
|
27
27
|
* sized to seven columns are different requirements. Everything else is here.
|
|
28
28
|
*
|
|
29
|
-
*
|
|
29
|
+
* Two rules hold these together. No row carries two utilities for the same
|
|
30
30
|
* property. Colour and background sit on the surface, and a row states only
|
|
31
31
|
* the override its state earns, because two utilities for one property resolve
|
|
32
32
|
* in the order Tailwind emits them and not in the order they were written.
|
|
33
|
+
* And a row is inset and rounded, never square and full width: the surface is
|
|
34
|
+
* rounded-xl, so a ring or tint that reaches its edge is cut at the corners on
|
|
35
|
+
* the first and last row. The list pads the rows in by px-1 and each row takes
|
|
36
|
+
* rounded-lg, so what a row paints stays inside the frame with corners of its
|
|
37
|
+
* own.
|
|
33
38
|
*
|
|
34
39
|
* Not exported from the package entry point - this is an implementation detail.
|
|
35
40
|
*/
|
|
@@ -40,26 +45,34 @@
|
|
|
40
45
|
* needs a boundary that clears 3:1, which line does not. It carries the
|
|
41
46
|
* resting text colour so a row can override it with a single utility.
|
|
42
47
|
*
|
|
43
|
-
* overflow-hidden
|
|
44
|
-
*
|
|
45
|
-
*
|
|
48
|
+
* overflow-hidden is a backstop, not the shape: the rows sit inset with
|
|
49
|
+
* corners of their own, so nothing they paint reaches the rounded frame. It
|
|
50
|
+
* still clips a caller's stray full-width child, and it keeps the search
|
|
51
|
+
* field's corners inside the surface's.
|
|
52
|
+
*
|
|
53
|
+
* No vertical offset and no side. Both belong to placePanel, which measures
|
|
54
|
+
* the room and puts the panel above the anchor when there is none below.
|
|
46
55
|
*/
|
|
47
|
-
export const PANEL_SURFACE = 'absolute z-dropdown
|
|
56
|
+
export const PANEL_SURFACE = 'absolute z-dropdown overflow-hidden rounded-xl border border-line-strong bg-surface text-fg shadow-2xl';
|
|
48
57
|
/**
|
|
49
58
|
* The scrolling region inside it.
|
|
50
59
|
*
|
|
51
60
|
* The cap is a token, not max-h-60, so a fifth panel cannot pick a different
|
|
52
61
|
* one. overscroll-contain stops a wheel that has reached the end of the list
|
|
53
|
-
* from carrying on into the page behind the open panel.
|
|
62
|
+
* from carrying on into the page behind the open panel. px-1 insets the rows
|
|
63
|
+
* from the frame, so a row's ring and hover tint sit inside the surface's
|
|
64
|
+
* rounded corners instead of being cut by them on the first and last row.
|
|
54
65
|
*/
|
|
55
|
-
export const PANEL_LIST = 'max-h-panel-max overflow-y-auto overscroll-contain py-1';
|
|
66
|
+
export const PANEL_LIST = 'max-h-panel-max overflow-y-auto overscroll-contain px-1 py-1';
|
|
56
67
|
/**
|
|
57
68
|
* One row at rest.
|
|
58
69
|
*
|
|
59
70
|
* No background and no text colour of its own: it inherits both from the
|
|
60
71
|
* surface, which leaves each state below a single utility to override.
|
|
72
|
+
* rounded-lg, one step inside the surface's rounded-xl, so the hover tint and
|
|
73
|
+
* the active ring follow the row's own corners.
|
|
61
74
|
*/
|
|
62
|
-
export const PANEL_OPTION = 'flex w-full items-center gap-2.5 px-
|
|
75
|
+
export const PANEL_OPTION = 'flex w-full items-center gap-2.5 rounded-lg px-2 py-2 text-start text-sm ' +
|
|
63
76
|
'transition-colors duration-150 outline-none hover:bg-surface-2 ' +
|
|
64
77
|
'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand';
|
|
65
78
|
/**
|
|
@@ -67,7 +80,7 @@ export const PANEL_OPTION = 'flex w-full items-center gap-2.5 px-3 py-2 text-sta
|
|
|
67
80
|
*
|
|
68
81
|
* A tint alone reads 1.09:1 against the panel, which is not a visible state,
|
|
69
82
|
* so the active row also carries an inset brand ring. Inset because the row
|
|
70
|
-
*
|
|
83
|
+
* fills the list's inner width and an outset ring would run into the frame.
|
|
71
84
|
*/
|
|
72
85
|
export const PANEL_OPTION_ACTIVE = 'bg-surface-2 ring-1 ring-inset ring-brand';
|
|
73
86
|
/**
|
|
@@ -85,10 +98,18 @@ export const PANEL_OPTION_SELECTED = 'font-medium text-brand';
|
|
|
85
98
|
* by emitted order, so the row could take the tint and read as choosable.
|
|
86
99
|
*/
|
|
87
100
|
export const PANEL_OPTION_DISABLED = 'pointer-events-none opacity-40';
|
|
88
|
-
/**
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
101
|
+
/**
|
|
102
|
+
* The line shown when a filter matched nothing.
|
|
103
|
+
*
|
|
104
|
+
* px-2, the same as a row: the list's px-1 supplies the rest, so the text
|
|
105
|
+
* stays flush with row text.
|
|
106
|
+
*/
|
|
107
|
+
export const PANEL_EMPTY = 'px-2 py-2 text-sm text-faint';
|
|
108
|
+
/**
|
|
109
|
+
* A group heading inside the list. Not focusable, so it takes no row classes,
|
|
110
|
+
* only the row's horizontal padding so it lines up with the labels below it.
|
|
111
|
+
*/
|
|
112
|
+
export const PANEL_GROUP_LABEL = 'px-2 pb-1 pt-3 text-xs font-medium uppercase text-faint';
|
|
92
113
|
/**
|
|
93
114
|
* Composes the option classes for a row's state.
|
|
94
115
|
*
|
|
@@ -111,3 +132,105 @@ export function panelOption(state) {
|
|
|
111
132
|
}
|
|
112
133
|
return parts.join(' ');
|
|
113
134
|
}
|
|
135
|
+
/** Marks the scrolling region inside a surface, so placePanel can find it. */
|
|
136
|
+
export const PANEL_LIST_ATTR = 'data-panel-list';
|
|
137
|
+
/** The classes that put the surface under its anchor. */
|
|
138
|
+
export const PANEL_BELOW = 'top-full mt-1';
|
|
139
|
+
/** The classes that put the surface over its anchor. */
|
|
140
|
+
export const PANEL_ABOVE = 'bottom-full mb-1';
|
|
141
|
+
/**
|
|
142
|
+
* Breathing room between the panel and the edge it is measured against, so a
|
|
143
|
+
* panel that fits exactly does not sit on the edge of the window or the modal.
|
|
144
|
+
*/
|
|
145
|
+
const PANEL_EDGE_GAP = 8;
|
|
146
|
+
/**
|
|
147
|
+
* The nearest ancestor that clips or scrolls its content.
|
|
148
|
+
*
|
|
149
|
+
* A modal body, a drawer body and a scrolling card all crop what floats past
|
|
150
|
+
* them, so the room a panel has is the room inside that ancestor and not the
|
|
151
|
+
* room in the window. The walk stops short of body and html: an open overlay
|
|
152
|
+
* sets overflow hidden on the body to lock the page, and the body's rect is
|
|
153
|
+
* the document's, not the viewport's. A DOM that computes no style reports an
|
|
154
|
+
* empty string, and an overflow nobody stated is visible.
|
|
155
|
+
*/
|
|
156
|
+
function clippingAncestor(node) {
|
|
157
|
+
let cursor = node.parentElement;
|
|
158
|
+
while (cursor !== null && cursor !== document.body && cursor !== document.documentElement) {
|
|
159
|
+
const overflow = getComputedStyle(cursor).overflowY;
|
|
160
|
+
if (overflow !== '' && overflow !== 'visible')
|
|
161
|
+
return cursor;
|
|
162
|
+
cursor = cursor.parentElement;
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
function swap(node, remove, add) {
|
|
167
|
+
node.classList.remove(...remove.split(' '));
|
|
168
|
+
node.classList.add(...add.split(' '));
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Svelte action for a panel surface. Puts it on the side of its anchor that
|
|
172
|
+
* has room, and stops its list running past whatever clips it.
|
|
173
|
+
*
|
|
174
|
+
* <div use:placePanel class="{PANEL_SURFACE} w-full">
|
|
175
|
+
* <div class={PANEL_LIST} data-panel-list>
|
|
176
|
+
*
|
|
177
|
+
* The anchor is the surface's offset parent, the relative wrapper every
|
|
178
|
+
* control puts around its trigger; a DOM without layout has no offset parent,
|
|
179
|
+
* so the parent element stands in. Below is the default, because a list under
|
|
180
|
+
* its field is where a native select puts one. The panel goes above only when
|
|
181
|
+
* its natural height does not fit below and there is more room above, so a
|
|
182
|
+
* long list near the bottom of a modal opens upward instead of into the space
|
|
183
|
+
* under the modal's scroll edge that nobody can reach without scrolling
|
|
184
|
+
* first. Whichever side wins, the list's max-height is the smaller of the
|
|
185
|
+
* token cap and the room on that side, written as a CSS min so the token stays
|
|
186
|
+
* the cap without this code knowing its value.
|
|
187
|
+
*
|
|
188
|
+
* Measured on mount, on resize and on scroll of the clipping ancestor, since
|
|
189
|
+
* each moves the anchor relative to the edge. Scroll does not bubble, so the
|
|
190
|
+
* list's own scrolling never re-measures. A server render has no window and
|
|
191
|
+
* does nothing.
|
|
192
|
+
*/
|
|
193
|
+
export function placePanel(node) {
|
|
194
|
+
if (typeof window === 'undefined')
|
|
195
|
+
return { destroy() { } };
|
|
196
|
+
const anchor = node.offsetParent instanceof HTMLElement ? node.offsetParent : node.parentElement;
|
|
197
|
+
const clip = anchor === null ? null : clippingAncestor(anchor);
|
|
198
|
+
const list = node.querySelector(`[${PANEL_LIST_ATTR}]`);
|
|
199
|
+
function measure() {
|
|
200
|
+
if (anchor === null)
|
|
201
|
+
return;
|
|
202
|
+
// The list is read at its class-capped height, not at whatever an earlier
|
|
203
|
+
// measurement pinned it to, or a panel shrunk to fit a small room would
|
|
204
|
+
// read as fitting anywhere.
|
|
205
|
+
if (list !== null)
|
|
206
|
+
list.style.maxHeight = '';
|
|
207
|
+
const rect = anchor.getBoundingClientRect();
|
|
208
|
+
const clipTop = clip === null ? 0 : clip.getBoundingClientRect().top;
|
|
209
|
+
const clipBottom = clip === null ? window.innerHeight : clip.getBoundingClientRect().bottom;
|
|
210
|
+
const below = clipBottom - rect.bottom - PANEL_EDGE_GAP;
|
|
211
|
+
const above = rect.top - clipTop - PANEL_EDGE_GAP;
|
|
212
|
+
const natural = node.scrollHeight;
|
|
213
|
+
const flip = natural > below && above > below;
|
|
214
|
+
if (flip)
|
|
215
|
+
swap(node, PANEL_BELOW, PANEL_ABOVE);
|
|
216
|
+
else
|
|
217
|
+
swap(node, PANEL_ABOVE, PANEL_BELOW);
|
|
218
|
+
if (list === null)
|
|
219
|
+
return;
|
|
220
|
+
// What the surface adds around the list, a search field for instance, is
|
|
221
|
+
// taken out of the room so the whole panel fits and not only its list.
|
|
222
|
+
const chrome = Math.max(0, node.offsetHeight - list.offsetHeight);
|
|
223
|
+
const room = Math.max(0, Math.round((flip ? above : below) - chrome));
|
|
224
|
+
list.style.maxHeight = `min(var(--spacing-panel-max), ${room}px)`;
|
|
225
|
+
}
|
|
226
|
+
measure();
|
|
227
|
+
const scroller = clip ?? window;
|
|
228
|
+
window.addEventListener('resize', measure, { passive: true });
|
|
229
|
+
scroller.addEventListener('scroll', measure, { passive: true });
|
|
230
|
+
return {
|
|
231
|
+
destroy() {
|
|
232
|
+
window.removeEventListener('resize', measure);
|
|
233
|
+
scroller.removeEventListener('scroll', measure);
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|
package/package.json
CHANGED