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
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import '../internal/focus-ring.css'
|
|
3
3
|
import Badge from '../badge/Badge.svelte'
|
|
4
|
+
import { badgeDescription } from '../badge/description.js'
|
|
4
5
|
import Ripple from '../ripple/Ripple.svelte'
|
|
5
6
|
import type { HTMLButtonAttributes } from 'svelte/elements'
|
|
7
|
+
import { getNavigationRailContext } from './context.js'
|
|
6
8
|
import type { NavigationRailItemProps } from './types.js'
|
|
7
9
|
|
|
8
10
|
let {
|
|
@@ -15,23 +17,47 @@
|
|
|
15
17
|
element = $bindable(),
|
|
16
18
|
...attributes
|
|
17
19
|
}: NavigationRailItemProps = $props()
|
|
18
|
-
|
|
20
|
+
|
|
21
|
+
const uid = $props.id()
|
|
22
|
+
let badgeId = $derived(badge ? `${uid}-badge` : undefined)
|
|
23
|
+
let describedBy = $derived(
|
|
24
|
+
[attributes['aria-describedby'], badgeId].filter(Boolean).join(' ') || undefined,
|
|
25
|
+
)
|
|
19
26
|
|
|
20
27
|
let isLink = $derived(attributes.href != null)
|
|
28
|
+
|
|
29
|
+
const rail = getNavigationRailContext()
|
|
30
|
+
let expanded = $derived(rail?.expanded ?? false)
|
|
31
|
+
|
|
32
|
+
let classes = $derived([
|
|
33
|
+
'np-navigation-action',
|
|
34
|
+
selected && 'np-navigation-action-selected',
|
|
35
|
+
expanded && 'np-navigation-action-expanded',
|
|
36
|
+
attributes.class,
|
|
37
|
+
])
|
|
21
38
|
</script>
|
|
22
39
|
|
|
23
40
|
{#snippet content()}
|
|
24
|
-
<
|
|
25
|
-
|
|
41
|
+
<span class="np-navigation-action-indicator"><Ripple forElement={element} /></span>
|
|
42
|
+
<span class="np-navigation-action-icon">
|
|
26
43
|
{@render icon()}
|
|
27
44
|
{#if badge}
|
|
28
|
-
<
|
|
29
|
-
<Badge label={badgeLabel} aria-label={badgeAriaLabel} />
|
|
30
|
-
</div>
|
|
45
|
+
<span class="np-navigation-action-badge"><Badge label={badgeLabel} /></span>
|
|
31
46
|
{/if}
|
|
32
|
-
</
|
|
33
|
-
|
|
34
|
-
|
|
47
|
+
</span>
|
|
48
|
+
<!-- Both labels stay, so they can cross-fade while the rail morphs. The one out of use is hidden
|
|
49
|
+
and out of the flow. -->
|
|
50
|
+
<span
|
|
51
|
+
class={['np-navigation-action-label', 'np-label-below', expanded && 'np-away']}
|
|
52
|
+
aria-hidden={expanded ? 'true' : undefined}>{label}</span
|
|
53
|
+
>
|
|
54
|
+
<span
|
|
55
|
+
class={['np-navigation-action-label', 'np-label-beside', !expanded && 'np-away']}
|
|
56
|
+
aria-hidden={expanded ? undefined : 'true'}>{label}</span
|
|
57
|
+
>
|
|
58
|
+
{#if badgeId}
|
|
59
|
+
<span id={badgeId} hidden>{badgeDescription(badgeLabel, badgeAriaLabel)}</span>
|
|
60
|
+
{/if}
|
|
35
61
|
{/snippet}
|
|
36
62
|
|
|
37
63
|
{#if isLink}
|
|
@@ -39,7 +65,8 @@
|
|
|
39
65
|
{...attributes}
|
|
40
66
|
bind:this={element}
|
|
41
67
|
href={attributes.href}
|
|
42
|
-
|
|
68
|
+
aria-describedby={describedBy}
|
|
69
|
+
class={classes}
|
|
43
70
|
aria-current={selected ? 'page' : undefined}
|
|
44
71
|
tabindex={selected ? 0 : -1}
|
|
45
72
|
>
|
|
@@ -49,7 +76,8 @@
|
|
|
49
76
|
<button
|
|
50
77
|
{...attributes as HTMLButtonAttributes}
|
|
51
78
|
bind:this={element}
|
|
52
|
-
|
|
79
|
+
aria-describedby={describedBy}
|
|
80
|
+
class={classes}
|
|
53
81
|
aria-current={selected ? 'page' : undefined}
|
|
54
82
|
tabindex={selected ? 0 : -1}
|
|
55
83
|
>
|
|
@@ -58,86 +86,195 @@
|
|
|
58
86
|
{/if}
|
|
59
87
|
|
|
60
88
|
<style>
|
|
89
|
+
/* Collapsed: the indicator around the icon, the label below. Expanded: icon and label side by
|
|
90
|
+
side inside the indicator. The item spans the rail, so its target is the full width.
|
|
91
|
+
Everything is anchored to the item's start, so the parts keep their place while the rail's
|
|
92
|
+
width animates, and the measures below place the label that fades out as well. */
|
|
61
93
|
.np-navigation-action {
|
|
94
|
+
--_pad-block: 0.375rem;
|
|
95
|
+
--_pad-inline: 1.25rem;
|
|
96
|
+
--_indicator-width: 3.5rem;
|
|
97
|
+
--_indicator-height: 2rem;
|
|
98
|
+
--_row-gap: 0.25rem;
|
|
99
|
+
--_expanded-height: 3.5rem;
|
|
100
|
+
--_icon: 1.5rem;
|
|
101
|
+
--_icon-inset: 1rem;
|
|
102
|
+
--_label-gap: 0.5rem;
|
|
103
|
+
--_beside-line: 1.25rem;
|
|
104
|
+
position: relative;
|
|
105
|
+
box-sizing: border-box;
|
|
106
|
+
display: grid;
|
|
107
|
+
grid-template-columns: var(--_indicator-width);
|
|
108
|
+
grid-template-rows: var(--_indicator-height) auto;
|
|
109
|
+
row-gap: var(--_row-gap);
|
|
110
|
+
justify-items: start;
|
|
111
|
+
align-items: center;
|
|
112
|
+
width: 100%;
|
|
113
|
+
padding-block: var(--_pad-block);
|
|
114
|
+
padding-inline: var(--_pad-inline);
|
|
62
115
|
cursor: pointer;
|
|
63
116
|
font: inherit;
|
|
64
117
|
border: 0;
|
|
65
118
|
background: none;
|
|
66
|
-
align-items: center;
|
|
67
|
-
gap: 0.25rem;
|
|
68
|
-
display: flex;
|
|
69
|
-
flex-direction: column;
|
|
70
119
|
color: var(--np-color-on-surface);
|
|
71
120
|
text-decoration: none;
|
|
72
121
|
-webkit-tap-highlight-color: transparent;
|
|
73
122
|
}
|
|
123
|
+
.np-navigation-action-expanded {
|
|
124
|
+
grid-template-columns: auto auto;
|
|
125
|
+
grid-template-rows: var(--_expanded-height);
|
|
126
|
+
justify-content: start;
|
|
127
|
+
padding-block: 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
74
130
|
.np-navigation-action:focus-visible {
|
|
75
131
|
outline-style: solid;
|
|
76
132
|
outline-color: var(--np-color-secondary);
|
|
77
133
|
outline-width: 3px;
|
|
78
|
-
outline-offset:
|
|
134
|
+
outline-offset: -3px;
|
|
79
135
|
border-radius: 1rem;
|
|
80
136
|
}
|
|
137
|
+
/* The focused state layer, on top of the ring. */
|
|
138
|
+
.np-navigation-action:focus-visible :global(.np-ripple-surface)::before {
|
|
139
|
+
opacity: var(--np-ripple-focus-opacity, 0.1);
|
|
140
|
+
}
|
|
81
141
|
@media (prefers-reduced-motion: no-preference) {
|
|
82
142
|
.np-navigation-action:focus-visible {
|
|
83
|
-
animation:
|
|
143
|
+
animation: focusAnimationInset var(--np-motion-expressive-slow-effects) forwards;
|
|
84
144
|
}
|
|
85
145
|
}
|
|
86
|
-
|
|
146
|
+
|
|
147
|
+
.np-navigation-action-indicator {
|
|
148
|
+
grid-area: 1 / 1;
|
|
149
|
+
/* Top aligned, so its height can animate without moving it. */
|
|
150
|
+
align-self: start;
|
|
87
151
|
position: relative;
|
|
88
|
-
|
|
89
|
-
|
|
152
|
+
z-index: 0;
|
|
153
|
+
width: var(--_indicator-width);
|
|
154
|
+
height: var(--_indicator-height);
|
|
90
155
|
border-radius: var(--np-shape-corner-full);
|
|
91
|
-
justify-content: center;
|
|
92
|
-
width: 3.5rem;
|
|
93
|
-
height: 2rem;
|
|
94
|
-
align-items: center;
|
|
95
|
-
z-index: 1;
|
|
96
156
|
}
|
|
97
|
-
.np-navigation-action-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
color: var(--np-color-on-surface-variant);
|
|
157
|
+
.np-navigation-action-expanded .np-navigation-action-indicator {
|
|
158
|
+
grid-area: 1 / 1 / 2 / 3;
|
|
159
|
+
justify-self: stretch;
|
|
160
|
+
width: auto;
|
|
161
|
+
height: var(--_expanded-height);
|
|
103
162
|
}
|
|
104
|
-
.np-navigation-action-
|
|
105
|
-
|
|
106
|
-
--np-badge-end: 0.75rem;
|
|
163
|
+
.np-navigation-action-indicator::before {
|
|
164
|
+
content: '';
|
|
107
165
|
position: absolute;
|
|
108
166
|
inset: 0;
|
|
167
|
+
opacity: 0;
|
|
168
|
+
transform: scaleX(0.32);
|
|
169
|
+
background-color: var(
|
|
170
|
+
--np-navigation-rail-item-active-indicator-color,
|
|
171
|
+
var(--np-color-secondary-container)
|
|
172
|
+
);
|
|
173
|
+
border-radius: inherit;
|
|
174
|
+
z-index: -1;
|
|
175
|
+
}
|
|
176
|
+
.np-navigation-action-selected .np-navigation-action-indicator::before {
|
|
177
|
+
opacity: 1;
|
|
178
|
+
transform: scaleX(1);
|
|
179
|
+
}
|
|
180
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
181
|
+
.np-navigation-action-indicator::before {
|
|
182
|
+
transition:
|
|
183
|
+
transform var(--np-motion-expressive-default-spatial),
|
|
184
|
+
opacity var(--np-motion-expressive-fast-effects);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.np-navigation-action-icon {
|
|
189
|
+
grid-area: 1 / 1;
|
|
190
|
+
justify-self: center;
|
|
191
|
+
position: relative;
|
|
192
|
+
z-index: 1;
|
|
193
|
+
display: flex;
|
|
194
|
+
width: var(--_icon);
|
|
195
|
+
height: var(--_icon);
|
|
196
|
+
color: var(--np-color-on-surface-variant);
|
|
109
197
|
pointer-events: none;
|
|
110
198
|
}
|
|
199
|
+
.np-navigation-action-expanded .np-navigation-action-icon {
|
|
200
|
+
justify-self: start;
|
|
201
|
+
margin-inline-start: var(--_icon-inset);
|
|
202
|
+
}
|
|
111
203
|
.np-navigation-action-selected .np-navigation-action-icon {
|
|
112
204
|
color: var(--np-color-on-secondary-container);
|
|
113
205
|
--np-icon-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
|
114
206
|
}
|
|
115
|
-
|
|
116
|
-
|
|
207
|
+
/* The badge stays on the icon in both layouts. */
|
|
208
|
+
.np-navigation-action-badge {
|
|
117
209
|
position: absolute;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
opacity: 0;
|
|
121
|
-
transform: scaleX(0.32);
|
|
122
|
-
transition-duration: 0.2s;
|
|
123
|
-
transition-property: transform, opacity;
|
|
124
|
-
transition-timing-function: linear;
|
|
125
|
-
background-color: var(--np-color-secondary-container);
|
|
126
|
-
border-radius: 100px;
|
|
127
|
-
z-index: -1;
|
|
128
|
-
}
|
|
129
|
-
.np-navigation-action-selected .np-navigation-action-icon::before {
|
|
130
|
-
opacity: 1;
|
|
131
|
-
transform: scaleX(1);
|
|
210
|
+
inset: 0;
|
|
211
|
+
pointer-events: none;
|
|
132
212
|
}
|
|
133
213
|
|
|
214
|
+
.np-navigation-action-label {
|
|
215
|
+
font-size: 0.75rem;
|
|
216
|
+
line-height: 1rem;
|
|
217
|
+
letter-spacing: 0.031rem;
|
|
218
|
+
font-weight: var(--np-navigation-rail-item-font-weight, 500);
|
|
219
|
+
color: var(--np-color-on-surface-variant);
|
|
220
|
+
pointer-events: none;
|
|
221
|
+
}
|
|
134
222
|
.np-navigation-action-selected .np-navigation-action-label {
|
|
135
223
|
font-weight: var(--np-navigation-rail-item-selected-font-weight, 500);
|
|
136
224
|
color: var(--np-color-secondary);
|
|
137
225
|
}
|
|
138
|
-
.
|
|
139
|
-
|
|
226
|
+
/* Centred under the icon, across the width of the collapsed rail. */
|
|
227
|
+
.np-label-below {
|
|
228
|
+
grid-area: 2 / 1;
|
|
229
|
+
box-sizing: border-box;
|
|
230
|
+
width: calc(2 * var(--_pad-inline) + var(--_indicator-width));
|
|
231
|
+
margin-inline-start: calc(-1 * var(--_pad-inline));
|
|
232
|
+
padding-inline: 0.25rem;
|
|
233
|
+
text-align: center;
|
|
234
|
+
overflow-wrap: anywhere;
|
|
235
|
+
}
|
|
236
|
+
.np-label-beside {
|
|
237
|
+
grid-area: 1 / 2;
|
|
238
|
+
position: relative;
|
|
239
|
+
z-index: 1;
|
|
240
|
+
margin-inline: var(--_label-gap) 1rem;
|
|
241
|
+
font-size: 0.875rem;
|
|
242
|
+
line-height: var(--_beside-line);
|
|
243
|
+
letter-spacing: 0.006rem;
|
|
244
|
+
white-space: nowrap;
|
|
245
|
+
}
|
|
246
|
+
.np-navigation-action-selected .np-label-beside {
|
|
247
|
+
color: var(--np-color-on-secondary-container);
|
|
248
|
+
}
|
|
249
|
+
/* The label out of use leaves the flow where it would sit in the other layout. */
|
|
250
|
+
.np-navigation-action-label.np-away {
|
|
251
|
+
grid-area: auto;
|
|
140
252
|
position: absolute;
|
|
141
|
-
|
|
253
|
+
margin: 0;
|
|
254
|
+
opacity: 0;
|
|
255
|
+
visibility: hidden;
|
|
256
|
+
}
|
|
257
|
+
.np-label-below.np-away {
|
|
258
|
+
inset-block-start: calc(var(--_pad-block) + var(--_indicator-height) + var(--_row-gap));
|
|
259
|
+
inset-inline-start: 0;
|
|
260
|
+
}
|
|
261
|
+
.np-label-beside.np-away {
|
|
262
|
+
inset-block-start: calc((var(--_expanded-height) - var(--_beside-line)) / 2);
|
|
263
|
+
inset-inline-start: calc(
|
|
264
|
+
var(--_pad-inline) + var(--_icon-inset) + var(--_icon) + var(--_label-gap)
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
/* The old label fades out at once, the new one fades in as the items settle. */
|
|
268
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
269
|
+
.np-navigation-action-label {
|
|
270
|
+
transition:
|
|
271
|
+
opacity var(--np-motion-standard-fast-effects) 100ms,
|
|
272
|
+
visibility 0s;
|
|
273
|
+
}
|
|
274
|
+
.np-navigation-action-label.np-away {
|
|
275
|
+
transition:
|
|
276
|
+
opacity 100ms linear,
|
|
277
|
+
visibility 0s linear 100ms;
|
|
278
|
+
}
|
|
142
279
|
}
|
|
143
280
|
</style>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getNavigationRailContext } from './context.js'
|
|
3
|
+
import type { NavigationRailSectionProps } from './types.js'
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
label,
|
|
7
|
+
children,
|
|
8
|
+
element = $bindable(),
|
|
9
|
+
...attributes
|
|
10
|
+
}: NavigationRailSectionProps = $props()
|
|
11
|
+
|
|
12
|
+
const uid = $props.id()
|
|
13
|
+
const rail = getNavigationRailContext()
|
|
14
|
+
let expanded = $derived(rail?.expanded ?? false)
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<!-- Secondary destinations under a header. They show only in the expanded rail. -->
|
|
18
|
+
<div
|
|
19
|
+
{...attributes}
|
|
20
|
+
bind:this={element}
|
|
21
|
+
role="group"
|
|
22
|
+
aria-labelledby="{uid}-label"
|
|
23
|
+
hidden={!expanded}
|
|
24
|
+
class={['np-navigation-rail-section', attributes.class]}
|
|
25
|
+
>
|
|
26
|
+
<div id="{uid}-label" class="np-navigation-rail-section-label">{label}</div>
|
|
27
|
+
{@render children?.()}
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<style>
|
|
31
|
+
.np-navigation-rail-section {
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
}
|
|
35
|
+
.np-navigation-rail-section[hidden] {
|
|
36
|
+
display: none;
|
|
37
|
+
}
|
|
38
|
+
/* 20dp above and 8dp below, the text in line with the icons. */
|
|
39
|
+
.np-navigation-rail-section-label {
|
|
40
|
+
padding: 1.25rem 2.25rem 0.5rem;
|
|
41
|
+
font-size: 0.875rem;
|
|
42
|
+
line-height: 1.25rem;
|
|
43
|
+
font-weight: 500;
|
|
44
|
+
letter-spacing: 0.006rem;
|
|
45
|
+
white-space: nowrap;
|
|
46
|
+
color: var(--np-color-on-surface-variant);
|
|
47
|
+
}
|
|
48
|
+
/* It fades in as the destinations above settle, like the labels beside the icons. */
|
|
49
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
50
|
+
.np-navigation-rail-section {
|
|
51
|
+
transition: opacity var(--np-motion-standard-fast-effects) 100ms;
|
|
52
|
+
@starting-style {
|
|
53
|
+
opacity: 0;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
</style>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { NavigationRailSectionProps } from './types.js';
|
|
2
|
+
declare const NavigationRailSection: import("svelte").Component<NavigationRailSectionProps, {}, "element">;
|
|
3
|
+
type NavigationRailSection = ReturnType<typeof NavigationRailSection>;
|
|
4
|
+
export default NavigationRailSection;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface NavigationRailContext {
|
|
2
|
+
readonly expanded: boolean;
|
|
3
|
+
}
|
|
4
|
+
export declare const setNavigationRailContext: (context: NavigationRailContext) => NavigationRailContext;
|
|
5
|
+
/** The surrounding rail, or undefined for an item rendered on its own. */
|
|
6
|
+
export declare const getNavigationRailContext: () => NavigationRailContext | undefined;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { getContext, setContext } from 'svelte';
|
|
2
|
+
const KEY = Symbol('np-navigation-rail');
|
|
3
|
+
export const setNavigationRailContext = (context) => setContext(KEY, context);
|
|
4
|
+
/** The surrounding rail, or undefined for an item rendered on its own. */
|
|
5
|
+
export const getNavigationRailContext = () => getContext(KEY);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Cancels what runs on the elements. Collected first, so styles are flushed only once. */
|
|
2
|
+
export declare const cancel: (elements: (Element | null)[]) => void;
|
|
3
|
+
/**
|
|
4
|
+
* Switches the layout through `apply` and animates the rail and its items from where they were.
|
|
5
|
+
* Resolves when the rail has finished moving.
|
|
6
|
+
*/
|
|
7
|
+
export declare const morph: (rail: HTMLElement, apply: () => void, timing: KeyframeAnimationOptions,
|
|
8
|
+
/** Timing for the rail's own width, when it moves differently from its items. */
|
|
9
|
+
railTiming?: KeyframeAnimationOptions) => Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Grows the rail out of nothing or shrinks it away, for a rail that is hidden when collapsed.
|
|
12
|
+
* Shrinking holds the end, so the rail stays gone until it is hidden.
|
|
13
|
+
*/
|
|
14
|
+
export declare const resize: (rail: HTMLElement, grow: boolean, timing: KeyframeAnimationOptions) => Promise<void | Animation>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Morphs the rail between its collapsed and expanded layouts. CSS cannot animate a change of grid
|
|
2
|
+
// layout, so the items are measured before and after the switch and animated from the old boxes
|
|
3
|
+
// to the new ones (FLIP), along with the width of the rail itself.
|
|
4
|
+
/** Cancels what runs on the elements. Collected first, so styles are flushed only once. */
|
|
5
|
+
export const cancel = (elements) => {
|
|
6
|
+
elements.flatMap((element) => element?.getAnimations() ?? []).forEach((a) => a.cancel());
|
|
7
|
+
};
|
|
8
|
+
// The expanded rail's min-width would snap it open at once, so it is lifted while the width moves.
|
|
9
|
+
const animateWidth = (rail, from, to, timing) => rail.animate([
|
|
10
|
+
{ width: `${from}px`, minWidth: '0px' },
|
|
11
|
+
{ width: `${to}px`, minWidth: '0px' },
|
|
12
|
+
], timing);
|
|
13
|
+
const measure = ({ item, icon, indicator }) => ({
|
|
14
|
+
item: item.getBoundingClientRect(),
|
|
15
|
+
icon: icon?.getBoundingClientRect(),
|
|
16
|
+
indicator: indicator?.getBoundingClientRect(),
|
|
17
|
+
});
|
|
18
|
+
/**
|
|
19
|
+
* Animates an element back from where it sat inside its parent before. Offsets count from the
|
|
20
|
+
* parent, so a parent that moves carries the element along. `size` also animates its box.
|
|
21
|
+
*/
|
|
22
|
+
const flip = (element, [before, beforeParent], [after, afterParent], timing, size = false) => {
|
|
23
|
+
const x = before.left - beforeParent.left - (after.left - afterParent.left);
|
|
24
|
+
const y = before.top - beforeParent.top - (after.top - afterParent.top);
|
|
25
|
+
const box = (rect) => size ? { width: `${rect.width}px`, height: `${rect.height}px` } : {};
|
|
26
|
+
element.animate([
|
|
27
|
+
{ transform: `translate(${x}px, ${y}px)`, ...box(before) },
|
|
28
|
+
{ transform: 'none', ...box(after) },
|
|
29
|
+
], timing);
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Switches the layout through `apply` and animates the rail and its items from where they were.
|
|
33
|
+
* Resolves when the rail has finished moving.
|
|
34
|
+
*/
|
|
35
|
+
export const morph = async (rail, apply, timing,
|
|
36
|
+
/** Timing for the rail's own width, when it moves differently from its items. */
|
|
37
|
+
railTiming = timing) => {
|
|
38
|
+
const parts = [...rail.querySelectorAll('.np-navigation-action')].map((item) => ({
|
|
39
|
+
item,
|
|
40
|
+
icon: item.querySelector('.np-navigation-action-icon'),
|
|
41
|
+
indicator: item.querySelector('.np-navigation-action-indicator'),
|
|
42
|
+
}));
|
|
43
|
+
const firstRail = rail.getBoundingClientRect();
|
|
44
|
+
const first = parts.map(measure);
|
|
45
|
+
cancel([rail, ...parts.flatMap(({ item, icon, indicator }) => [item, icon, indicator])]);
|
|
46
|
+
apply();
|
|
47
|
+
// Measure everything before anything moves: the rail's own animation can shift its container.
|
|
48
|
+
const lastRail = rail.getBoundingClientRect();
|
|
49
|
+
const last = parts.map(measure);
|
|
50
|
+
const animation = animateWidth(rail, firstRail.width, lastRail.width, railTiming);
|
|
51
|
+
parts.forEach(({ item, icon, indicator }, i) => {
|
|
52
|
+
const before = first[i];
|
|
53
|
+
const after = last[i];
|
|
54
|
+
// An item that appears or goes with the switch, in a section shown only when expanded, fades
|
|
55
|
+
// instead.
|
|
56
|
+
if (!before.item.width || !after.item.width)
|
|
57
|
+
return;
|
|
58
|
+
flip(item, [before.item, firstRail], [after.item, lastRail], timing);
|
|
59
|
+
if (icon)
|
|
60
|
+
flip(icon, [before.icon, before.item], [after.icon, after.item], timing);
|
|
61
|
+
if (indicator) {
|
|
62
|
+
flip(indicator, [before.indicator, before.item], [after.indicator, after.item], timing, true);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
await animation.finished.catch(() => { });
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Grows the rail out of nothing or shrinks it away, for a rail that is hidden when collapsed.
|
|
69
|
+
* Shrinking holds the end, so the rail stays gone until it is hidden.
|
|
70
|
+
*/
|
|
71
|
+
export const resize = (rail, grow, timing) => {
|
|
72
|
+
cancel([rail]);
|
|
73
|
+
const width = rail.getBoundingClientRect().width;
|
|
74
|
+
return animateWidth(rail, grow ? 0 : width, grow ? width : 0, {
|
|
75
|
+
...timing,
|
|
76
|
+
fill: grow ? 'none' : 'forwards',
|
|
77
|
+
}).finished.catch(() => { });
|
|
78
|
+
};
|
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
import type { HTMLAnchorAttributes, HTMLAttributes, HTMLButtonAttributes } from 'svelte/elements';
|
|
3
3
|
export interface NavigationRailProps extends HTMLAttributes<HTMLElement> {
|
|
4
|
+
/** Shows the expanded rail: wider, with the label beside each icon. */
|
|
5
|
+
expanded?: boolean;
|
|
6
|
+
/** Opens the expanded rail over the content, behind a scrim, instead of pushing it aside. */
|
|
7
|
+
modal?: boolean;
|
|
8
|
+
/** Hides the collapsed rail. Open it with your own button that sets `expanded`. */
|
|
9
|
+
hideWhenCollapsed?: boolean;
|
|
10
|
+
/** Puts a menu button at the top that toggles `expanded`. */
|
|
11
|
+
menu?: boolean;
|
|
12
|
+
/** Name of the menu button, read by assistive technology. It shows no tooltip. */
|
|
13
|
+
menuLabel?: string;
|
|
14
|
+
/** Groups the destinations at the top of the rail or in its vertical centre. */
|
|
15
|
+
alignment?: 'top' | 'center';
|
|
16
|
+
/** A FAB above the destinations. Switch to an extended FAB while `expanded`. */
|
|
17
|
+
fab?: Snippet<[{
|
|
18
|
+
expanded: boolean;
|
|
19
|
+
}]>;
|
|
4
20
|
element?: HTMLElement;
|
|
5
21
|
}
|
|
6
22
|
export type NavigationRailItemElement = HTMLButtonElement | HTMLAnchorElement;
|
|
@@ -14,3 +30,8 @@ export interface NavigationRailItemProps extends HTMLAttributes<NavigationRailIt
|
|
|
14
30
|
type?: 'submit' | 'reset' | 'button' | (string & {}) | null;
|
|
15
31
|
element?: NavigationRailItemElement;
|
|
16
32
|
}
|
|
33
|
+
export interface NavigationRailSectionProps extends HTMLAttributes<HTMLDivElement> {
|
|
34
|
+
/** The section header, shown above the destinations and naming the group. */
|
|
35
|
+
label: string;
|
|
36
|
+
element?: HTMLDivElement;
|
|
37
|
+
}
|
|
@@ -195,7 +195,8 @@
|
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
.progress:not(.indeterminate) .track {
|
|
198
|
-
|
|
198
|
+
/* The full gap as soon as there is any progress, so a low value shows as a dot. */
|
|
199
|
+
--_g: calc(var(--_gap) * clamp(0, var(--_active) * 100, 1));
|
|
199
200
|
stroke-dasharray: calc(max(0, 100 - var(--_active) - 2 * var(--_g)) * 1px) 100px;
|
|
200
201
|
stroke-dashoffset: calc((var(--_active) + var(--_g)) * -1px);
|
|
201
202
|
}
|