noph-ui 0.51.2 → 0.52.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.
- package/dist/animation.d.ts +26 -1
- package/dist/animation.js +50 -2
- package/dist/app-bar/AppBar.svelte +71 -7
- package/dist/app-bar/types.d.ts +6 -0
- package/dist/autocomplete/AutoComplete.svelte +6 -31
- package/dist/autocomplete/AutoComplete.svelte.d.ts +1 -1
- package/dist/badge/Badge.svelte +12 -4
- package/dist/badge/description.d.ts +4 -0
- package/dist/badge/description.js +4 -0
- package/dist/button/Button.svelte +55 -20
- package/dist/button/ButtonGroup.svelte +139 -19
- package/dist/button/ButtonGroup.svelte.d.ts +1 -1
- package/dist/button/IconButton.svelte +49 -18
- package/dist/button/SegmentedButton.svelte +44 -4
- package/dist/button/SplitButton.svelte +77 -3
- package/dist/button/SplitButton.svelte.d.ts +1 -1
- package/dist/button/buttonGroup.d.ts +0 -4
- package/dist/button/buttonGroup.js +0 -9
- package/dist/button/groupContext.d.ts +10 -0
- package/dist/button/groupContext.js +4 -0
- package/dist/button/types.d.ts +19 -0
- package/dist/checkbox/Checkbox.svelte +5 -0
- package/dist/chip/ChipSet.svelte +28 -2
- package/dist/chip/FilterChip.svelte +32 -4
- package/dist/chip/InputChip.svelte +127 -26
- package/dist/chip/SuggestionChip.svelte +6 -22
- package/dist/chip/SuggestionChip.svelte.d.ts +1 -2
- package/dist/chip/types.d.ts +8 -1
- package/dist/date-picker/Calendar.svelte +4 -2
- package/dist/date-picker/DatePickerDialog.svelte +1 -0
- package/dist/date-picker/DatePickerDialog.svelte.d.ts +1 -1
- package/dist/date-picker/DateRangePicker.svelte +1 -0
- package/dist/date-picker/DateRangePicker.svelte.d.ts +1 -1
- package/dist/date-picker/DockedDatePicker.svelte +12 -5
- package/dist/date-picker/DockedDatePicker.svelte.d.ts +1 -1
- package/dist/date-picker/DockedDateTimePicker.svelte +12 -5
- package/dist/date-picker/DockedDateTimePicker.svelte.d.ts +1 -1
- package/dist/date-picker/YearGrid.svelte +4 -2
- package/dist/dialog/Dialog.svelte +171 -33
- package/dist/dialog/Dialog.svelte.d.ts +2 -1
- package/dist/dialog/types.d.ts +6 -0
- package/dist/divider/Divider.svelte +36 -10
- package/dist/divider/types.d.ts +1 -0
- package/dist/fab/ExtendedFab.svelte +26 -60
- package/dist/fab/Fab.svelte +13 -48
- package/dist/fab/FabMenu.svelte +102 -44
- package/dist/fab/FabMenu.svelte.d.ts +2 -1
- package/dist/fab/FabMenuItem.svelte +8 -0
- package/dist/fab/FabMenuItem.svelte.d.ts +4 -0
- package/dist/fab/index.d.ts +1 -0
- package/dist/fab/index.js +1 -0
- package/dist/fab/types.d.ts +6 -4
- package/dist/icons/MenuIcon.svelte +13 -0
- package/dist/icons/MenuIcon.svelte.d.ts +7 -0
- package/dist/icons/MenuOpenIcon.svelte +15 -0
- package/dist/icons/MenuOpenIcon.svelte.d.ts +7 -0
- package/dist/icons/index.d.ts +2 -0
- package/dist/icons/index.js +2 -0
- package/dist/internal/exit.css +47 -0
- package/dist/internal/fieldTypes.d.ts +6 -0
- package/dist/internal/focus-ring.css +4 -0
- package/dist/internal/interest.css +3 -1
- package/dist/keyboard-nav.d.ts +12 -1
- package/dist/keyboard-nav.js +104 -14
- package/dist/list/Item.svelte +52 -6
- package/dist/list/List.svelte +35 -2
- package/dist/list/ListItem.svelte +16 -2
- package/dist/list/context.d.ts +6 -0
- package/dist/list/context.js +4 -0
- package/dist/list/types.d.ts +7 -0
- package/dist/media.d.ts +1 -0
- package/dist/media.js +1 -0
- package/dist/menu/Menu.svelte +70 -18
- package/dist/menu/Menu.svelte.d.ts +2 -1
- package/dist/menu/MenuItem.svelte +9 -3
- package/dist/menu/types.d.ts +7 -1
- package/dist/navigation-bar/NavigationBar.svelte +9 -3
- package/dist/navigation-bar/NavigationBarItem.svelte +29 -13
- package/dist/navigation-drawer/NavigationDrawer.svelte +96 -20
- package/dist/navigation-drawer/NavigationDrawer.svelte.d.ts +2 -1
- package/dist/navigation-drawer/NavigationDrawerItem.svelte +8 -3
- package/dist/navigation-drawer/NavigationDrawerSection.svelte +49 -0
- package/dist/navigation-drawer/NavigationDrawerSection.svelte.d.ts +4 -0
- package/dist/navigation-drawer/index.d.ts +1 -0
- package/dist/navigation-drawer/index.js +1 -0
- package/dist/navigation-drawer/types.d.ts +12 -0
- package/dist/navigation-rail/NavigationRail.svelte +367 -7
- package/dist/navigation-rail/NavigationRail.svelte.d.ts +1 -1
- package/dist/navigation-rail/NavigationRailItem.svelte +190 -53
- package/dist/navigation-rail/NavigationRailSection.svelte +57 -0
- package/dist/navigation-rail/NavigationRailSection.svelte.d.ts +4 -0
- package/dist/navigation-rail/context.d.ts +6 -0
- package/dist/navigation-rail/context.js +5 -0
- package/dist/navigation-rail/index.d.ts +1 -0
- package/dist/navigation-rail/index.js +1 -0
- package/dist/navigation-rail/morph.d.ts +14 -0
- package/dist/navigation-rail/morph.js +78 -0
- package/dist/navigation-rail/types.d.ts +21 -0
- package/dist/progress/CircularProgress.svelte +2 -1
- package/dist/progress/LinearProgress.svelte +48 -20
- package/dist/progress/types.d.ts +1 -0
- package/dist/radio/Radio.svelte +7 -1
- package/dist/search/Search.svelte +245 -58
- package/dist/search/types.d.ts +4 -0
- package/dist/select/NativeSelect.svelte +20 -12
- package/dist/select/Option.svelte +1 -2
- package/dist/select/Select.svelte +59 -140
- package/dist/select/VirtualList.svelte +16 -1
- package/dist/select/VirtualList.svelte.d.ts +6 -2
- package/dist/sheet/Sheet.svelte +223 -5
- package/dist/sheet/Sheet.svelte.d.ts +3 -1
- package/dist/sheet/types.d.ts +10 -0
- package/dist/slider/Slider.svelte +1 -27
- package/dist/snackbar/Snackbar.svelte +3 -0
- package/dist/snackbar/Snackbar.svelte.d.ts +2 -1
- package/dist/switch/Switch.svelte +51 -11
- package/dist/tabs/Tab.svelte +28 -14
- package/dist/tabs/Tabs.svelte +28 -4
- package/dist/tabs/types.d.ts +3 -0
- package/dist/text-field/TextField.svelte +49 -13
- package/dist/time-picker/DockedTimePicker.svelte +6 -1
- package/dist/time-picker/DockedTimePicker.svelte.d.ts +1 -1
- package/dist/time-picker/TimePickerDialog.svelte +1 -0
- package/dist/time-picker/TimePickerDialog.svelte.d.ts +1 -1
- package/dist/toolbar/Toolbar.svelte +21 -11
- package/dist/toolbar/types.d.ts +1 -0
- package/dist/tooltip/RichTooltip.svelte.d.ts +1 -1
- package/dist/tooltip/Tooltip.svelte +3 -1
- package/dist/tooltip/Tooltip.svelte.d.ts +1 -1
- package/package.json +1 -1
package/dist/animation.d.ts
CHANGED
|
@@ -1,2 +1,27 @@
|
|
|
1
|
+
import type { Attachment } from 'svelte/attachments';
|
|
1
2
|
export declare const afterTwoFrames: (fn: () => void) => () => void;
|
|
2
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Calls `fn` once the element's animations have finished, two frames from now so that transitions
|
|
5
|
+
* just started are counted. `subtree` includes those of its descendants. Endless animations, such
|
|
6
|
+
* as a spinner's, are not waited for.
|
|
7
|
+
*/
|
|
8
|
+
export declare const afterExit: (element: Element | undefined, fn: () => void, { subtree }?: {
|
|
9
|
+
subtree?: boolean;
|
|
10
|
+
}) => () => void;
|
|
11
|
+
export declare const parseMotion: (token: string) => {
|
|
12
|
+
duration: number;
|
|
13
|
+
easing: string;
|
|
14
|
+
};
|
|
15
|
+
/** Reads a motion token such as `--np-motion-standard-fast-spatial` off an element's styles. */
|
|
16
|
+
export declare const motionOf: (element: Element, token: string) => {
|
|
17
|
+
duration: number;
|
|
18
|
+
easing: string;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Keeps a dialog or popover on screen while it animates out, in browsers that drop it from the top
|
|
22
|
+
* layer the moment it closes because they cannot transition `overlay`, such as Safari. From the
|
|
23
|
+
* start of closing until its exit animations end it carries `data-np-exiting`, and the rules in
|
|
24
|
+
* `internal/exit.css` keep it shown meanwhile, over the page but not blocking it. `beforetoggle`
|
|
25
|
+
* fires before every way of closing, from Escape and light dismiss to `close()`.
|
|
26
|
+
*/
|
|
27
|
+
export declare const exitAnimation: Attachment<HTMLElement>;
|
package/dist/animation.js
CHANGED
|
@@ -8,7 +8,12 @@ export const afterTwoFrames = (fn) => {
|
|
|
8
8
|
cancelAnimationFrame(inner);
|
|
9
9
|
};
|
|
10
10
|
};
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Calls `fn` once the element's animations have finished, two frames from now so that transitions
|
|
13
|
+
* just started are counted. `subtree` includes those of its descendants. Endless animations, such
|
|
14
|
+
* as a spinner's, are not waited for.
|
|
15
|
+
*/
|
|
16
|
+
export const afterExit = (element, fn, { subtree = false } = {}) => {
|
|
12
17
|
if (!element) {
|
|
13
18
|
fn();
|
|
14
19
|
return () => { };
|
|
@@ -19,7 +24,9 @@ export const afterExit = (element, fn) => {
|
|
|
19
24
|
fn();
|
|
20
25
|
};
|
|
21
26
|
const cancelFrames = afterTwoFrames(() => {
|
|
22
|
-
const animations = element
|
|
27
|
+
const animations = element
|
|
28
|
+
.getAnimations({ subtree })
|
|
29
|
+
.filter((animation) => animation.effect?.getTiming().iterations !== Infinity);
|
|
23
30
|
if (animations.length === 0) {
|
|
24
31
|
finish();
|
|
25
32
|
return;
|
|
@@ -31,3 +38,44 @@ export const afterExit = (element, fn) => {
|
|
|
31
38
|
cancelFrames();
|
|
32
39
|
};
|
|
33
40
|
};
|
|
41
|
+
export const parseMotion = (token) => {
|
|
42
|
+
const [time, ...rest] = token.trim().split(/\s+/);
|
|
43
|
+
const value = Number.parseFloat(time);
|
|
44
|
+
const easing = rest.join(' ');
|
|
45
|
+
return {
|
|
46
|
+
duration: Number.isNaN(value) ? 0 : time.endsWith('ms') ? value : value * 1000,
|
|
47
|
+
easing: easing || 'linear',
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
/** Reads a motion token such as `--np-motion-standard-fast-spatial` off an element's styles. */
|
|
51
|
+
export const motionOf = (element, token) => parseMotion(getComputedStyle(element).getPropertyValue(token));
|
|
52
|
+
/**
|
|
53
|
+
* Keeps a dialog or popover on screen while it animates out, in browsers that drop it from the top
|
|
54
|
+
* layer the moment it closes because they cannot transition `overlay`, such as Safari. From the
|
|
55
|
+
* start of closing until its exit animations end it carries `data-np-exiting`, and the rules in
|
|
56
|
+
* `internal/exit.css` keep it shown meanwhile, over the page but not blocking it. `beforetoggle`
|
|
57
|
+
* fires before every way of closing, from Escape and light dismiss to `close()`.
|
|
58
|
+
*/
|
|
59
|
+
export const exitAnimation = (element) => {
|
|
60
|
+
if (CSS.supports('overlay: auto'))
|
|
61
|
+
return;
|
|
62
|
+
let cancel = () => { };
|
|
63
|
+
const done = () => {
|
|
64
|
+
cancel();
|
|
65
|
+
delete element.dataset.npExiting;
|
|
66
|
+
};
|
|
67
|
+
const onBeforeToggle = (event) => {
|
|
68
|
+
done();
|
|
69
|
+
if (event.newState !== 'closed')
|
|
70
|
+
return;
|
|
71
|
+
if (matchMedia('(prefers-reduced-motion: reduce)').matches)
|
|
72
|
+
return;
|
|
73
|
+
element.dataset.npExiting = '';
|
|
74
|
+
cancel = afterExit(element, done, { subtree: true });
|
|
75
|
+
};
|
|
76
|
+
element.addEventListener('beforetoggle', onBeforeToggle);
|
|
77
|
+
return () => {
|
|
78
|
+
element.removeEventListener('beforetoggle', onBeforeToggle);
|
|
79
|
+
done();
|
|
80
|
+
};
|
|
81
|
+
};
|
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
let {
|
|
5
5
|
variant = 'small',
|
|
6
6
|
headline,
|
|
7
|
+
headlineLevel,
|
|
7
8
|
subtitle,
|
|
9
|
+
alignment = 'start',
|
|
10
|
+
image,
|
|
8
11
|
leading,
|
|
9
12
|
search,
|
|
10
13
|
trailing,
|
|
@@ -22,7 +25,15 @@
|
|
|
22
25
|
|
|
23
26
|
{#snippet titles(inline: boolean)}
|
|
24
27
|
<div class={['np-app-bar-titles', inline && 'np-app-bar-titles-inline']}>
|
|
25
|
-
|
|
28
|
+
{#if image && !inline}
|
|
29
|
+
<div class="np-app-bar-image">{@render image()}</div>
|
|
30
|
+
{/if}
|
|
31
|
+
<!-- The copy shown while collapsed is hidden from assistive technology, so only the other one
|
|
32
|
+
is the heading. -->
|
|
33
|
+
<svelte:element
|
|
34
|
+
this={headlineLevel && !inline ? `h${headlineLevel}` : 'div'}
|
|
35
|
+
class="np-app-bar-headline">{headline}</svelte:element
|
|
36
|
+
>
|
|
26
37
|
{#if subtitle}
|
|
27
38
|
<div class="np-app-bar-subtitle">{subtitle}</div>
|
|
28
39
|
{/if}
|
|
@@ -36,6 +47,7 @@
|
|
|
36
47
|
'np-app-bar',
|
|
37
48
|
`np-app-bar-${variant}`,
|
|
38
49
|
collapses && 'np-app-bar-collapsible',
|
|
50
|
+
alignment === 'center' && 'np-app-bar-center',
|
|
39
51
|
scroller === 'nearest' && 'np-app-bar-scroller-nearest',
|
|
40
52
|
attributes.class,
|
|
41
53
|
]}
|
|
@@ -48,6 +60,9 @@
|
|
|
48
60
|
<div class="np-app-bar-search-field">{@render search?.()}</div>
|
|
49
61
|
{:else if twoLine}
|
|
50
62
|
<div class="np-app-bar-inline" aria-hidden="true">{@render titles(true)}</div>
|
|
63
|
+
{:else if image}
|
|
64
|
+
<!-- In a small app bar the image takes the headline's place. -->
|
|
65
|
+
<div class="np-app-bar-titles">{@render image()}</div>
|
|
51
66
|
{:else}
|
|
52
67
|
{@render titles(false)}
|
|
53
68
|
{/if}
|
|
@@ -82,8 +97,10 @@
|
|
|
82
97
|
--_timeline: scroll(nearest block);
|
|
83
98
|
}
|
|
84
99
|
|
|
100
|
+
/* The bar is the open search's stacking context, so it rises to the search's own level, above
|
|
101
|
+
other bars and a page header. */
|
|
85
102
|
.np-app-bar:has(:global(.np-search-expanded)) {
|
|
86
|
-
z-index:
|
|
103
|
+
z-index: var(--np-search-z-index, 24);
|
|
87
104
|
}
|
|
88
105
|
|
|
89
106
|
.np-app-bar-row {
|
|
@@ -105,6 +122,11 @@
|
|
|
105
122
|
.np-app-bar-trailing {
|
|
106
123
|
margin-inline-start: auto;
|
|
107
124
|
}
|
|
125
|
+
/* The leading button is on surface, the trailing ones on surface variant. In a search app bar
|
|
126
|
+
they are all on surface variant. */
|
|
127
|
+
.np-app-bar:not(.np-app-bar-search) .np-app-bar-leading {
|
|
128
|
+
--np-icon-button-icon-color: var(--np-color-on-surface);
|
|
129
|
+
}
|
|
108
130
|
|
|
109
131
|
.np-app-bar-titles {
|
|
110
132
|
display: flex;
|
|
@@ -125,6 +147,7 @@
|
|
|
125
147
|
}
|
|
126
148
|
|
|
127
149
|
.np-app-bar-headline {
|
|
150
|
+
margin: 0;
|
|
128
151
|
font-size: var(--_headline-size);
|
|
129
152
|
line-height: var(--_headline-line-height);
|
|
130
153
|
font-weight: 400;
|
|
@@ -152,12 +175,20 @@
|
|
|
152
175
|
--_subtitle-line-height: 1rem;
|
|
153
176
|
}
|
|
154
177
|
|
|
178
|
+
/* The search fills the space between the leading and trailing elements up to 312dp, then
|
|
179
|
+
grows only to half of it, centred. The bar sits 8dp from the elements beside it, so the
|
|
180
|
+
margin a search keeps for its open view stays out of the row. */
|
|
155
181
|
.np-app-bar-search-field {
|
|
156
182
|
flex: 1;
|
|
157
183
|
min-width: 0;
|
|
158
184
|
display: flex;
|
|
159
185
|
align-items: center;
|
|
160
|
-
|
|
186
|
+
justify-content: center;
|
|
187
|
+
container-type: inline-size;
|
|
188
|
+
--np-search-pane-margin: 0;
|
|
189
|
+
}
|
|
190
|
+
.np-app-bar-search-field :global(.np-search:not(.np-search-full-screen.np-search-expanded)) {
|
|
191
|
+
width: min(100%, max(19.5rem, 50cqi));
|
|
161
192
|
}
|
|
162
193
|
.np-app-bar-search-field :global(.np-search:not(.np-search-expanded) .np-search-bar) {
|
|
163
194
|
background-color: var(--np-search-container-color, var(--np-color-surface-container));
|
|
@@ -179,7 +210,7 @@
|
|
|
179
210
|
--_headline-line-height: 2.75rem;
|
|
180
211
|
--_subtitle-size: 1rem;
|
|
181
212
|
--_subtitle-line-height: 1.5rem;
|
|
182
|
-
--_second-row-height:
|
|
213
|
+
--_second-row-height: 3.5rem;
|
|
183
214
|
}
|
|
184
215
|
|
|
185
216
|
.np-app-bar-second-row {
|
|
@@ -190,11 +221,44 @@
|
|
|
190
221
|
height: auto;
|
|
191
222
|
min-height: var(--_second-row-height);
|
|
192
223
|
padding-inline: 1rem;
|
|
193
|
-
padding-block-end:
|
|
224
|
+
padding-block-end: 0.75rem;
|
|
194
225
|
overflow: hidden;
|
|
195
226
|
}
|
|
196
|
-
.np-app-bar-
|
|
197
|
-
|
|
227
|
+
.np-app-bar-image {
|
|
228
|
+
display: flex;
|
|
229
|
+
margin-block-end: 0.5rem;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/* Centred, a small app bar puts its headline in the middle of the bar whatever sits beside it. */
|
|
233
|
+
.np-app-bar-center .np-app-bar-row {
|
|
234
|
+
display: grid;
|
|
235
|
+
grid-template-columns: minmax(max-content, 1fr) auto minmax(max-content, 1fr);
|
|
236
|
+
}
|
|
237
|
+
.np-app-bar-center .np-app-bar-leading {
|
|
238
|
+
grid-column: 1;
|
|
239
|
+
}
|
|
240
|
+
.np-app-bar-center .np-app-bar-row > .np-app-bar-titles,
|
|
241
|
+
.np-app-bar-center .np-app-bar-inline,
|
|
242
|
+
.np-app-bar-center .np-app-bar-search-field {
|
|
243
|
+
grid-column: 2;
|
|
244
|
+
padding-inline: 0;
|
|
245
|
+
}
|
|
246
|
+
.np-app-bar-center.np-app-bar-search .np-app-bar-row {
|
|
247
|
+
grid-template-columns: auto 1fr auto;
|
|
248
|
+
}
|
|
249
|
+
.np-app-bar-center .np-app-bar-trailing {
|
|
250
|
+
grid-column: 3;
|
|
251
|
+
justify-self: end;
|
|
252
|
+
}
|
|
253
|
+
.np-app-bar-center .np-app-bar-titles {
|
|
254
|
+
align-items: center;
|
|
255
|
+
text-align: center;
|
|
256
|
+
}
|
|
257
|
+
.np-app-bar-center .np-app-bar-second-row {
|
|
258
|
+
justify-content: center;
|
|
259
|
+
}
|
|
260
|
+
.np-app-bar-center .np-app-bar-search-field :global(.np-search-input) {
|
|
261
|
+
text-align: center;
|
|
198
262
|
}
|
|
199
263
|
|
|
200
264
|
.np-app-bar-inline {
|
package/dist/app-bar/types.d.ts
CHANGED
|
@@ -3,7 +3,13 @@ import type { HTMLAttributes } from 'svelte/elements';
|
|
|
3
3
|
export interface AppBarProps extends HTMLAttributes<HTMLElement> {
|
|
4
4
|
variant?: 'search' | 'small' | 'medium' | 'large';
|
|
5
5
|
headline?: string;
|
|
6
|
+
/** Renders the headline as a heading of this level. Without it the headline is plain text. */
|
|
7
|
+
headlineLevel?: 1 | 2 | 3 | 4 | 5 | 6;
|
|
6
8
|
subtitle?: string;
|
|
9
|
+
/** Headline and subtitle at the leading edge or centered. */
|
|
10
|
+
alignment?: 'start' | 'center';
|
|
11
|
+
/** An image or logo. It replaces the headline of a small app bar and sits above it otherwise. */
|
|
12
|
+
image?: Snippet;
|
|
7
13
|
leading?: Snippet;
|
|
8
14
|
search?: Snippet;
|
|
9
15
|
trailing?: Snippet;
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
let useVirtualList = $derived(displayOptions.length > virtualThreshold)
|
|
39
39
|
let widthProp = $derived(clampMenuWidth || useVirtualList ? 'width' : 'min-width')
|
|
40
40
|
let menuElement = $state<HTMLDivElement>()
|
|
41
|
+
let virtualList = $state<VirtualList<AutoCompleteOption>>()
|
|
41
42
|
let finalPopulated = $derived(populated)
|
|
42
43
|
let activeIndex = $state(NO_INDEX)
|
|
43
44
|
|
|
@@ -81,25 +82,7 @@
|
|
|
81
82
|
optEl.scrollIntoView({ block: 'nearest' })
|
|
82
83
|
return
|
|
83
84
|
}
|
|
84
|
-
if (useVirtualList
|
|
85
|
-
const viewport = menuElement.querySelector(
|
|
86
|
-
'svelte-virtual-list-viewport',
|
|
87
|
-
) as HTMLElement | null
|
|
88
|
-
if (!viewport) return
|
|
89
|
-
let rowHeight = 48
|
|
90
|
-
const firstRow = viewport.querySelector('[id^="' + uid + '-opt-"]') as HTMLElement | null
|
|
91
|
-
if (firstRow) {
|
|
92
|
-
rowHeight = firstRow.offsetHeight || rowHeight
|
|
93
|
-
}
|
|
94
|
-
const top = activeIndex * rowHeight
|
|
95
|
-
const bottom = top + rowHeight
|
|
96
|
-
const { scrollTop, clientHeight } = viewport
|
|
97
|
-
if (top < scrollTop) {
|
|
98
|
-
viewport.scrollTop = top
|
|
99
|
-
} else if (bottom > scrollTop + clientHeight) {
|
|
100
|
-
viewport.scrollTop = bottom - clientHeight
|
|
101
|
-
}
|
|
102
|
-
}
|
|
85
|
+
if (useVirtualList) virtualList?.scrollToIndex(activeIndex)
|
|
103
86
|
})
|
|
104
87
|
</script>
|
|
105
88
|
|
|
@@ -169,13 +152,9 @@
|
|
|
169
152
|
event.preventDefault()
|
|
170
153
|
return
|
|
171
154
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
event.
|
|
175
|
-
return
|
|
176
|
-
}
|
|
177
|
-
if (event.key === 'End') {
|
|
178
|
-
setActive(displayOptions.length - 1)
|
|
155
|
+
// Home and End move the caret, unless the arrows have already moved into the options.
|
|
156
|
+
if ((event.key === 'Home' || event.key === 'End') && open && activeIndex >= 0) {
|
|
157
|
+
setActive(event.key === 'Home' ? 0 : displayOptions.length - 1)
|
|
179
158
|
event.preventDefault()
|
|
180
159
|
return
|
|
181
160
|
}
|
|
@@ -227,11 +206,7 @@
|
|
|
227
206
|
bind:element={menuElement}
|
|
228
207
|
>
|
|
229
208
|
{#if useVirtualList}
|
|
230
|
-
<VirtualList
|
|
231
|
-
height="250px"
|
|
232
|
-
itemHeight={displayOptions.some(({ supportingText }) => supportingText) ? 68 : 48}
|
|
233
|
-
items={displayOptions}
|
|
234
|
-
>
|
|
209
|
+
<VirtualList bind:this={virtualList} height="250px" items={displayOptions}>
|
|
235
210
|
{#snippet row(option, index)}
|
|
236
211
|
{@render item(option, index)}
|
|
237
212
|
{/snippet}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { AutoCompleteProps } from './types.js';
|
|
2
|
-
declare const AutoComplete: import("svelte").Component<AutoCompleteProps, {}, "
|
|
2
|
+
declare const AutoComplete: import("svelte").Component<AutoCompleteProps, {}, "open" | "element" | "value">;
|
|
3
3
|
type AutoComplete = ReturnType<typeof AutoComplete>;
|
|
4
4
|
export default AutoComplete;
|
package/dist/badge/Badge.svelte
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { badgeText } from './description.js'
|
|
2
3
|
import type { BadgeProps } from './types.js'
|
|
3
4
|
|
|
4
5
|
let {
|
|
@@ -23,7 +24,7 @@
|
|
|
23
24
|
>
|
|
24
25
|
{#if label !== undefined}
|
|
25
26
|
<div class="np-badge-label">
|
|
26
|
-
{label}
|
|
27
|
+
{badgeText(label)}
|
|
27
28
|
</div>
|
|
28
29
|
{/if}
|
|
29
30
|
</div>
|
|
@@ -35,15 +36,21 @@
|
|
|
35
36
|
background-color: var(--np-color-error);
|
|
36
37
|
border-radius: var(--np-shape-corner-full);
|
|
37
38
|
position: var(--np-badge-position, absolute);
|
|
38
|
-
top: var(--np-badge-top,
|
|
39
|
-
inset-inline-start: var(--np-badge-start,
|
|
40
|
-
inset-inline-end: var(--np-badge-end, auto);
|
|
39
|
+
top: var(--np-badge-top, var(--_top));
|
|
40
|
+
inset-inline-start: var(--np-badge-start, var(--_start));
|
|
41
41
|
}
|
|
42
|
+
/* Anchored inside the icon at its top trailing corner. A large badge keeps its leading edge and
|
|
43
|
+
grows toward the trailing side. */
|
|
42
44
|
.np-badge-container-label {
|
|
45
|
+
--_top: -0.125rem;
|
|
46
|
+
--_start: calc(100% - 0.75rem);
|
|
47
|
+
box-sizing: border-box;
|
|
43
48
|
height: 1rem;
|
|
44
49
|
min-width: 1rem;
|
|
45
50
|
}
|
|
46
51
|
.np-badge-container-no-label {
|
|
52
|
+
--_top: 0;
|
|
53
|
+
--_start: calc(100% - 0.375rem);
|
|
47
54
|
width: 0.375rem;
|
|
48
55
|
height: 0.375rem;
|
|
49
56
|
}
|
|
@@ -52,6 +59,7 @@
|
|
|
52
59
|
padding-inline: 0.25rem;
|
|
53
60
|
font-weight: 500;
|
|
54
61
|
font-size: 0.6875rem;
|
|
62
|
+
white-space: nowrap;
|
|
55
63
|
display: flex;
|
|
56
64
|
align-items: center;
|
|
57
65
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** What a navigation destination reads after its name for its badge: the given label, else the count, else "New notification". */
|
|
2
|
+
export declare const badgeDescription: (label: string | number | undefined, ariaLabel?: string) => string;
|
|
3
|
+
/** Shows counts above 999 as "999+", so a large badge keeps to four characters. */
|
|
4
|
+
export declare const badgeText: (label: string | number) => string;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** What a navigation destination reads after its name for its badge: the given label, else the count, else "New notification". */
|
|
2
|
+
export const badgeDescription = (label, ariaLabel) => ariaLabel ?? (label !== undefined ? String(label) : 'New notification');
|
|
3
|
+
/** Shows counts above 999 as "999+", so a large badge keeps to four characters. */
|
|
4
|
+
export const badgeText = (label) => typeof label === 'number' && label > 999 ? '999+' : String(label);
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import Ripple from '../ripple/Ripple.svelte'
|
|
6
6
|
import Tooltip from '../tooltip/Tooltip.svelte'
|
|
7
7
|
import type { HTMLButtonAttributes } from 'svelte/elements'
|
|
8
|
+
import { getButtonGroupContext } from './groupContext.js'
|
|
8
9
|
import type { ButtonProps } from './types.js'
|
|
9
10
|
|
|
10
11
|
let {
|
|
@@ -17,8 +18,8 @@
|
|
|
17
18
|
disabled = false,
|
|
18
19
|
loading = false,
|
|
19
20
|
loadingAriaLabel,
|
|
20
|
-
size
|
|
21
|
-
shape
|
|
21
|
+
size: sizeProp,
|
|
22
|
+
shape: shapeProp,
|
|
22
23
|
toggle = false,
|
|
23
24
|
selected = $bindable(false),
|
|
24
25
|
...attributes
|
|
@@ -29,6 +30,24 @@
|
|
|
29
30
|
let isLink = $derived(attributes.href != null && !disabled && !loading)
|
|
30
31
|
|
|
31
32
|
let tooltipId = $derived(title && !disabled && !loading ? uid : undefined)
|
|
33
|
+
// A visible label names the button, the tooltip only describes it. Without one, the tooltip
|
|
34
|
+
// text is the name, and describing the button with it again would read it twice.
|
|
35
|
+
let tooltipNames = $derived(!!tooltipId && !children && !attributes['aria-label'])
|
|
36
|
+
|
|
37
|
+
// Inside a ButtonGroup, the group sets the size and shape its buttons leave open, and with
|
|
38
|
+
// `selection` it decides what is selected.
|
|
39
|
+
const group = getButtonGroupContext()
|
|
40
|
+
let size = $derived(sizeProp ?? group?.size ?? 's')
|
|
41
|
+
let shape = $derived(shapeProp ?? group?.shape ?? 'round')
|
|
42
|
+
let groupValue = $derived(
|
|
43
|
+
group?.selection && attributes.value != null ? String(attributes.value) : undefined,
|
|
44
|
+
)
|
|
45
|
+
let isToggle = $derived(toggle || groupValue !== undefined)
|
|
46
|
+
let isSelected = $derived(groupValue !== undefined ? group!.isSelected(groupValue) : selected)
|
|
47
|
+
// A selected toggle swaps its resting shape: round turns square, square turns round.
|
|
48
|
+
let shapeClass = $derived(
|
|
49
|
+
isSelected ? (shape === 'square' ? 'round' : 'square') : loading ? 'square' : shape,
|
|
50
|
+
)
|
|
32
51
|
|
|
33
52
|
const morph = pressMorph()
|
|
34
53
|
|
|
@@ -41,6 +60,7 @@
|
|
|
41
60
|
{#snippet content()}
|
|
42
61
|
{#if !disabled && !loading}
|
|
43
62
|
<Ripple forElement={element} />
|
|
63
|
+
<span class="np-touch"></span>
|
|
44
64
|
{/if}
|
|
45
65
|
{#if loading}
|
|
46
66
|
<div class="circular-progress">
|
|
@@ -68,21 +88,21 @@
|
|
|
68
88
|
<a
|
|
69
89
|
{...attributes}
|
|
70
90
|
onclick={(event) => {
|
|
71
|
-
if (!
|
|
91
|
+
if (!isToggle) {
|
|
72
92
|
handlePress()
|
|
73
93
|
}
|
|
74
94
|
attributes.onclick?.(event)
|
|
75
95
|
}}
|
|
76
|
-
aria-describedby={tooltipId
|
|
96
|
+
aria-describedby={(!tooltipNames && tooltipId) || attributes['aria-describedby']}
|
|
77
97
|
interestfor={tooltipId ?? attributes['interestfor']}
|
|
78
|
-
aria-label={
|
|
98
|
+
aria-label={attributes['aria-label'] ?? (tooltipNames ? title : undefined)}
|
|
79
99
|
bind:this={element}
|
|
80
100
|
class={[
|
|
81
101
|
'np-button',
|
|
82
102
|
size,
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
103
|
+
shapeClass,
|
|
104
|
+
isToggle && 'toggle',
|
|
105
|
+
isSelected && 'selected',
|
|
86
106
|
'enabled',
|
|
87
107
|
variant,
|
|
88
108
|
morph.pressed && 'pressed',
|
|
@@ -94,16 +114,18 @@
|
|
|
94
114
|
{:else}
|
|
95
115
|
<button
|
|
96
116
|
{...attributes as HTMLButtonAttributes}
|
|
97
|
-
aria-describedby={tooltipId
|
|
117
|
+
aria-describedby={(!tooltipNames && tooltipId) || attributes['aria-describedby']}
|
|
98
118
|
interestfor={tooltipId ?? attributes['interestfor']}
|
|
99
|
-
aria-label={
|
|
119
|
+
aria-label={attributes['aria-label'] ?? (tooltipNames ? title : undefined)}
|
|
100
120
|
disabled={disabled || loading}
|
|
101
|
-
aria-pressed={
|
|
121
|
+
aria-pressed={isToggle ? isSelected : undefined}
|
|
102
122
|
aria-busy={loading}
|
|
103
123
|
type={(attributes['type'] as 'button' | 'submit' | 'reset' | 'button') ?? undefined}
|
|
104
124
|
bind:this={element}
|
|
105
125
|
onclick={(event) => {
|
|
106
|
-
if (
|
|
126
|
+
if (groupValue !== undefined) {
|
|
127
|
+
group!.toggle(groupValue)
|
|
128
|
+
} else if (toggle) {
|
|
107
129
|
selected = !selected
|
|
108
130
|
} else {
|
|
109
131
|
handlePress()
|
|
@@ -113,9 +135,9 @@
|
|
|
113
135
|
class={[
|
|
114
136
|
'np-button',
|
|
115
137
|
size,
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
138
|
+
shapeClass,
|
|
139
|
+
isToggle && 'toggle',
|
|
140
|
+
isSelected && 'selected',
|
|
119
141
|
loading && 'np-loading',
|
|
120
142
|
disabled || loading ? `${variant}-disabled disabled` : `${variant} enabled`,
|
|
121
143
|
morph.pressed && 'pressed',
|
|
@@ -131,10 +153,10 @@
|
|
|
131
153
|
{/if}
|
|
132
154
|
|
|
133
155
|
<style>
|
|
156
|
+
/* The label is never truncated or wrapped, the button grows to fit it instead. */
|
|
134
157
|
.children-wrapper {
|
|
135
|
-
|
|
136
|
-
overflow: var(--_button-label-overflow,
|
|
137
|
-
text-overflow: var(--_button-label-text-overflow, ellipsis);
|
|
158
|
+
min-width: 0;
|
|
159
|
+
overflow: var(--_button-label-overflow, visible);
|
|
138
160
|
text-wrap: nowrap;
|
|
139
161
|
}
|
|
140
162
|
.circular-progress {
|
|
@@ -162,8 +184,10 @@
|
|
|
162
184
|
display: inline-flex;
|
|
163
185
|
user-select: none;
|
|
164
186
|
align-items: center;
|
|
165
|
-
|
|
166
|
-
|
|
187
|
+
/* Icon and label stay grouped and centered, also in a stretched button. */
|
|
188
|
+
justify-content: center;
|
|
189
|
+
text-align: center;
|
|
190
|
+
min-width: var(--_button-min-width, max-content);
|
|
167
191
|
font-weight: 500;
|
|
168
192
|
text-decoration: none;
|
|
169
193
|
--np-icon-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
|
@@ -276,6 +300,17 @@
|
|
|
276
300
|
outline-width: 3px;
|
|
277
301
|
outline-offset: 2px;
|
|
278
302
|
}
|
|
303
|
+
.enabled:focus-visible :global(.np-ripple-surface)::before {
|
|
304
|
+
opacity: var(--np-ripple-focus-opacity, 0.1);
|
|
305
|
+
}
|
|
306
|
+
/* Extra small and small buttons keep a 48px tall target around them. */
|
|
307
|
+
.np-touch {
|
|
308
|
+
position: absolute;
|
|
309
|
+
inset-inline: 0;
|
|
310
|
+
top: 50%;
|
|
311
|
+
translate: 0 -50%;
|
|
312
|
+
height: max(3rem, 100%);
|
|
313
|
+
}
|
|
279
314
|
@media (prefers-reduced-motion: no-preference) {
|
|
280
315
|
.enabled:focus-visible {
|
|
281
316
|
animation: focusAnimation var(--np-motion-expressive-slow-effects) forwards;
|