@lyeve-labs/ui-kit 0.23.1 → 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/CheckboxGroup.svelte +3 -2
- package/dist/components/Dropdown.svelte +31 -23
- package/dist/components/MultiSelect.svelte +15 -3
- package/dist/components/RadioGroup.svelte +3 -2
- package/dist/components/Select.svelte +3 -2
- package/dist/components/Table.svelte +25 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/internal/choice.d.ts +9 -0
- package/dist/internal/choice.js +10 -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
|
<!--
|
|
@@ -26,12 +26,13 @@
|
|
|
26
26
|
<script lang="ts">
|
|
27
27
|
import {
|
|
28
28
|
CHOICE_GROUP,
|
|
29
|
+
CHOICE_LEGEND,
|
|
29
30
|
choiceGroupList,
|
|
30
31
|
type ChoiceOrientation,
|
|
31
32
|
type ChoiceSize,
|
|
32
33
|
type ChoiceVariant,
|
|
33
34
|
} from '../internal/choice.js';
|
|
34
|
-
import { FIELD_ERROR, FIELD_HINT,
|
|
35
|
+
import { FIELD_ERROR, FIELD_HINT, describedBy } from '../internal/field.js';
|
|
35
36
|
import Checkbox from './Checkbox.svelte';
|
|
36
37
|
|
|
37
38
|
interface Props {
|
|
@@ -123,7 +124,7 @@
|
|
|
123
124
|
so a reader moving through the page would meet the options with nothing
|
|
124
125
|
ahead of them saying what the set is for.
|
|
125
126
|
-->
|
|
126
|
-
<legend class="{
|
|
127
|
+
<legend class="{CHOICE_LEGEND} {labelHidden ? 'sr-only' : ''}">
|
|
127
128
|
{label}{#if required}<span class="text-danger ms-0.5" aria-hidden="true">*</span><span
|
|
128
129
|
class="sr-only"
|
|
129
130
|
>
|
|
@@ -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
|
<!--
|
|
@@ -27,12 +27,13 @@
|
|
|
27
27
|
<script lang="ts">
|
|
28
28
|
import {
|
|
29
29
|
CHOICE_GROUP,
|
|
30
|
+
CHOICE_LEGEND,
|
|
30
31
|
choiceGroupList,
|
|
31
32
|
type ChoiceOrientation,
|
|
32
33
|
type ChoiceSize,
|
|
33
34
|
type ChoiceVariant,
|
|
34
35
|
} from '../internal/choice.js';
|
|
35
|
-
import { FIELD_ERROR, FIELD_HINT,
|
|
36
|
+
import { FIELD_ERROR, FIELD_HINT, describedBy } from '../internal/field.js';
|
|
36
37
|
import Radio from './Radio.svelte';
|
|
37
38
|
|
|
38
39
|
interface Props {
|
|
@@ -108,7 +109,7 @@
|
|
|
108
109
|
so a reader moving through the page would meet the options with nothing
|
|
109
110
|
ahead of them saying what the set is for.
|
|
110
111
|
-->
|
|
111
|
-
<legend class="{
|
|
112
|
+
<legend class="{CHOICE_LEGEND} {labelHidden ? 'sr-only' : ''}">
|
|
112
113
|
{label}{#if required}<span class="text-danger ms-0.5" aria-hidden="true">*</span>{/if}
|
|
113
114
|
</legend>
|
|
114
115
|
|
|
@@ -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. -->
|
|
@@ -56,6 +56,23 @@
|
|
|
56
56
|
children: Snippet;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* The hover ring, as inset shadows on the cells of the hovered row: top
|
|
61
|
+
* and bottom on every cell, the start edge on the first, the end edge on
|
|
62
|
+
* the last, so the four lines meet as one rectangle and the two outer
|
|
63
|
+
* cells of the last row carry it round the frame's corner. The shadow
|
|
64
|
+
* offsets are physical, which is what a shadow is; the first cell is the
|
|
65
|
+
* start cell in either direction and the two agree in left-to-right text.
|
|
66
|
+
*/
|
|
67
|
+
const HOVER_RING = [
|
|
68
|
+
'[&_tbody_tr:hover]:bg-surface-2/60',
|
|
69
|
+
'[&_tbody_tr:hover>td]:shadow-[inset_0_1px_var(--color-brand),inset_0_-1px_var(--color-brand)]',
|
|
70
|
+
'[&_tbody_tr:hover>td:first-child]:shadow-[inset_1px_1px_var(--color-brand),inset_1px_-1px_var(--color-brand)]',
|
|
71
|
+
'[&_tbody_tr:hover>td:last-child]:shadow-[inset_-1px_1px_var(--color-brand),inset_-1px_-1px_var(--color-brand)]',
|
|
72
|
+
'[&_tbody_tr:hover>td:first-child:last-child]:shadow-[inset_1px_1px_var(--color-brand),inset_-1px_-1px_var(--color-brand)]',
|
|
73
|
+
'[&_tbody_tr]:transition-colors [&_tbody_tr]:duration-150',
|
|
74
|
+
].join(' ');
|
|
75
|
+
|
|
59
76
|
let {
|
|
60
77
|
striped = false,
|
|
61
78
|
hoverable = true,
|
|
@@ -231,6 +248,11 @@
|
|
|
231
248
|
<!--
|
|
232
249
|
border-separate with no spacing, and the row separators on the cells.
|
|
233
250
|
|
|
251
|
+
The last row's outer cells are rounded to the frame's inner radius. A
|
|
252
|
+
row takes no radius, so a ring drawn on it was a square inset in a
|
|
253
|
+
rounded frame and the frame's bottom arcs sat outside it; the ring is
|
|
254
|
+
drawn on the cells now (see HOVER_RING) and follows the corner.
|
|
255
|
+
|
|
234
256
|
The hovered row draws a ring, and a ring is a box-shadow: a browser paints
|
|
235
257
|
no box-shadow on a table row while the table collapses its borders, so the
|
|
236
258
|
same row treatment the panels use would have rendered nothing at all here.
|
|
@@ -251,9 +273,9 @@
|
|
|
251
273
|
[&_tbody_td]:px-4 [&_tbody_td]:py-3 [&_tbody_td]:text-fg [&_tbody_td]:align-middle
|
|
252
274
|
[&_tbody_td]:border-b [&_tbody_td]:border-line [&_tbody_tr:last-child_td]:border-0
|
|
253
275
|
{striped ? '[&_tbody_tr:nth-child(even)]:bg-surface-2/40' : ''}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
276
|
+
[&_tbody_tr:last-child>td:first-child]:rounded-es-[calc(var(--radius-xl)-1px)]
|
|
277
|
+
[&_tbody_tr:last-child>td:last-child]:rounded-ee-[calc(var(--radius-xl)-1px)]
|
|
278
|
+
{hoverable ? HOVER_RING : ''}"
|
|
257
279
|
>
|
|
258
280
|
{@render children()}
|
|
259
281
|
</table>
|
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';
|
|
@@ -112,6 +112,15 @@ export declare const CHOICE_ICON_PX: Record<ChoiceSize, number>;
|
|
|
112
112
|
* wrapping.
|
|
113
113
|
*/
|
|
114
114
|
export declare const CHOICE_GROUP = "flex min-w-0 flex-col gap-2";
|
|
115
|
+
/**
|
|
116
|
+
* The group's heading. A rendered legend is not a flex item: the browser lays
|
|
117
|
+
* it out as the fieldset's legend, outside the flex formatting context, so
|
|
118
|
+
* the group's gap applies between the list and the hint and never between
|
|
119
|
+
* the legend and the list. The legend carries the distance itself, the same
|
|
120
|
+
* 6px a Field puts between its label and its control. A hidden legend is
|
|
121
|
+
* absolutely positioned and the margin does nothing there.
|
|
122
|
+
*/
|
|
123
|
+
export declare const CHOICE_LEGEND = "text-sm font-medium text-fg mb-1.5";
|
|
115
124
|
/**
|
|
116
125
|
* The row or column the options sit in.
|
|
117
126
|
*
|
package/dist/internal/choice.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Not exported from the package entry point - this is an implementation detail.
|
|
16
16
|
*/
|
|
17
|
-
import { FIELD_HINT } from './field.js';
|
|
17
|
+
import { FIELD_HINT, FIELD_LABEL } from './field.js';
|
|
18
18
|
/**
|
|
19
19
|
* The box and the icon slot share one size map, so the label text starts at the
|
|
20
20
|
* same x whether or not an option carries an icon. 3.5 is 14px, 4 is 16px and 5
|
|
@@ -151,6 +151,15 @@ export const CHOICE_ICON_PX = {
|
|
|
151
151
|
* wrapping.
|
|
152
152
|
*/
|
|
153
153
|
export const CHOICE_GROUP = 'flex min-w-0 flex-col gap-2';
|
|
154
|
+
/**
|
|
155
|
+
* The group's heading. A rendered legend is not a flex item: the browser lays
|
|
156
|
+
* it out as the fieldset's legend, outside the flex formatting context, so
|
|
157
|
+
* the group's gap applies between the list and the hint and never between
|
|
158
|
+
* the legend and the list. The legend carries the distance itself, the same
|
|
159
|
+
* 6px a Field puts between its label and its control. A hidden legend is
|
|
160
|
+
* absolutely positioned and the margin does nothing there.
|
|
161
|
+
*/
|
|
162
|
+
export const CHOICE_LEGEND = `${FIELD_LABEL} mb-1.5`;
|
|
154
163
|
/**
|
|
155
164
|
* The row or column the options sit in.
|
|
156
165
|
*
|
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