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.
Files changed (130) hide show
  1. package/dist/animation.d.ts +26 -1
  2. package/dist/animation.js +50 -2
  3. package/dist/app-bar/AppBar.svelte +71 -7
  4. package/dist/app-bar/types.d.ts +6 -0
  5. package/dist/autocomplete/AutoComplete.svelte +6 -31
  6. package/dist/autocomplete/AutoComplete.svelte.d.ts +1 -1
  7. package/dist/badge/Badge.svelte +12 -4
  8. package/dist/badge/description.d.ts +4 -0
  9. package/dist/badge/description.js +4 -0
  10. package/dist/button/Button.svelte +55 -20
  11. package/dist/button/ButtonGroup.svelte +139 -19
  12. package/dist/button/ButtonGroup.svelte.d.ts +1 -1
  13. package/dist/button/IconButton.svelte +49 -18
  14. package/dist/button/SegmentedButton.svelte +44 -4
  15. package/dist/button/SplitButton.svelte +77 -3
  16. package/dist/button/SplitButton.svelte.d.ts +1 -1
  17. package/dist/button/buttonGroup.d.ts +0 -4
  18. package/dist/button/buttonGroup.js +0 -9
  19. package/dist/button/groupContext.d.ts +10 -0
  20. package/dist/button/groupContext.js +4 -0
  21. package/dist/button/types.d.ts +19 -0
  22. package/dist/checkbox/Checkbox.svelte +5 -0
  23. package/dist/chip/ChipSet.svelte +28 -2
  24. package/dist/chip/FilterChip.svelte +32 -4
  25. package/dist/chip/InputChip.svelte +127 -26
  26. package/dist/chip/SuggestionChip.svelte +6 -22
  27. package/dist/chip/SuggestionChip.svelte.d.ts +1 -2
  28. package/dist/chip/types.d.ts +8 -1
  29. package/dist/date-picker/Calendar.svelte +4 -2
  30. package/dist/date-picker/DatePickerDialog.svelte +1 -0
  31. package/dist/date-picker/DatePickerDialog.svelte.d.ts +1 -1
  32. package/dist/date-picker/DateRangePicker.svelte +1 -0
  33. package/dist/date-picker/DateRangePicker.svelte.d.ts +1 -1
  34. package/dist/date-picker/DockedDatePicker.svelte +12 -5
  35. package/dist/date-picker/DockedDatePicker.svelte.d.ts +1 -1
  36. package/dist/date-picker/DockedDateTimePicker.svelte +12 -5
  37. package/dist/date-picker/DockedDateTimePicker.svelte.d.ts +1 -1
  38. package/dist/date-picker/YearGrid.svelte +4 -2
  39. package/dist/dialog/Dialog.svelte +171 -33
  40. package/dist/dialog/Dialog.svelte.d.ts +2 -1
  41. package/dist/dialog/types.d.ts +6 -0
  42. package/dist/divider/Divider.svelte +36 -10
  43. package/dist/divider/types.d.ts +1 -0
  44. package/dist/fab/ExtendedFab.svelte +26 -60
  45. package/dist/fab/Fab.svelte +13 -48
  46. package/dist/fab/FabMenu.svelte +102 -44
  47. package/dist/fab/FabMenu.svelte.d.ts +2 -1
  48. package/dist/fab/FabMenuItem.svelte +8 -0
  49. package/dist/fab/FabMenuItem.svelte.d.ts +4 -0
  50. package/dist/fab/index.d.ts +1 -0
  51. package/dist/fab/index.js +1 -0
  52. package/dist/fab/types.d.ts +6 -4
  53. package/dist/icons/MenuIcon.svelte +13 -0
  54. package/dist/icons/MenuIcon.svelte.d.ts +7 -0
  55. package/dist/icons/MenuOpenIcon.svelte +15 -0
  56. package/dist/icons/MenuOpenIcon.svelte.d.ts +7 -0
  57. package/dist/icons/index.d.ts +2 -0
  58. package/dist/icons/index.js +2 -0
  59. package/dist/internal/exit.css +47 -0
  60. package/dist/internal/fieldTypes.d.ts +6 -0
  61. package/dist/internal/focus-ring.css +4 -0
  62. package/dist/internal/interest.css +3 -1
  63. package/dist/keyboard-nav.d.ts +12 -1
  64. package/dist/keyboard-nav.js +104 -14
  65. package/dist/list/Item.svelte +52 -6
  66. package/dist/list/List.svelte +35 -2
  67. package/dist/list/ListItem.svelte +16 -2
  68. package/dist/list/context.d.ts +6 -0
  69. package/dist/list/context.js +4 -0
  70. package/dist/list/types.d.ts +7 -0
  71. package/dist/media.d.ts +1 -0
  72. package/dist/media.js +1 -0
  73. package/dist/menu/Menu.svelte +70 -18
  74. package/dist/menu/Menu.svelte.d.ts +2 -1
  75. package/dist/menu/MenuItem.svelte +9 -3
  76. package/dist/menu/types.d.ts +7 -1
  77. package/dist/navigation-bar/NavigationBar.svelte +9 -3
  78. package/dist/navigation-bar/NavigationBarItem.svelte +29 -13
  79. package/dist/navigation-drawer/NavigationDrawer.svelte +96 -20
  80. package/dist/navigation-drawer/NavigationDrawer.svelte.d.ts +2 -1
  81. package/dist/navigation-drawer/NavigationDrawerItem.svelte +8 -3
  82. package/dist/navigation-drawer/NavigationDrawerSection.svelte +49 -0
  83. package/dist/navigation-drawer/NavigationDrawerSection.svelte.d.ts +4 -0
  84. package/dist/navigation-drawer/index.d.ts +1 -0
  85. package/dist/navigation-drawer/index.js +1 -0
  86. package/dist/navigation-drawer/types.d.ts +12 -0
  87. package/dist/navigation-rail/NavigationRail.svelte +367 -7
  88. package/dist/navigation-rail/NavigationRail.svelte.d.ts +1 -1
  89. package/dist/navigation-rail/NavigationRailItem.svelte +190 -53
  90. package/dist/navigation-rail/NavigationRailSection.svelte +57 -0
  91. package/dist/navigation-rail/NavigationRailSection.svelte.d.ts +4 -0
  92. package/dist/navigation-rail/context.d.ts +6 -0
  93. package/dist/navigation-rail/context.js +5 -0
  94. package/dist/navigation-rail/index.d.ts +1 -0
  95. package/dist/navigation-rail/index.js +1 -0
  96. package/dist/navigation-rail/morph.d.ts +14 -0
  97. package/dist/navigation-rail/morph.js +78 -0
  98. package/dist/navigation-rail/types.d.ts +21 -0
  99. package/dist/progress/CircularProgress.svelte +2 -1
  100. package/dist/progress/LinearProgress.svelte +48 -20
  101. package/dist/progress/types.d.ts +1 -0
  102. package/dist/radio/Radio.svelte +7 -1
  103. package/dist/search/Search.svelte +245 -58
  104. package/dist/search/types.d.ts +4 -0
  105. package/dist/select/NativeSelect.svelte +20 -12
  106. package/dist/select/Option.svelte +1 -2
  107. package/dist/select/Select.svelte +59 -140
  108. package/dist/select/VirtualList.svelte +16 -1
  109. package/dist/select/VirtualList.svelte.d.ts +6 -2
  110. package/dist/sheet/Sheet.svelte +223 -5
  111. package/dist/sheet/Sheet.svelte.d.ts +3 -1
  112. package/dist/sheet/types.d.ts +10 -0
  113. package/dist/slider/Slider.svelte +1 -27
  114. package/dist/snackbar/Snackbar.svelte +3 -0
  115. package/dist/snackbar/Snackbar.svelte.d.ts +2 -1
  116. package/dist/switch/Switch.svelte +51 -11
  117. package/dist/tabs/Tab.svelte +28 -14
  118. package/dist/tabs/Tabs.svelte +28 -4
  119. package/dist/tabs/types.d.ts +3 -0
  120. package/dist/text-field/TextField.svelte +49 -13
  121. package/dist/time-picker/DockedTimePicker.svelte +6 -1
  122. package/dist/time-picker/DockedTimePicker.svelte.d.ts +1 -1
  123. package/dist/time-picker/TimePickerDialog.svelte +1 -0
  124. package/dist/time-picker/TimePickerDialog.svelte.d.ts +1 -1
  125. package/dist/toolbar/Toolbar.svelte +21 -11
  126. package/dist/toolbar/types.d.ts +1 -0
  127. package/dist/tooltip/RichTooltip.svelte.d.ts +1 -1
  128. package/dist/tooltip/Tooltip.svelte +3 -1
  129. package/dist/tooltip/Tooltip.svelte.d.ts +1 -1
  130. package/package.json +1 -1
@@ -1,11 +1,17 @@
1
1
  <script lang="ts">
2
+ import '../internal/exit.css'
3
+ import { exitAnimation } from '../animation.js'
4
+ import IconButton from '../button/IconButton.svelte'
2
5
  import Divider from '../divider/Divider.svelte'
6
+ import CloseIcon from '../icons/CloseIcon.svelte'
3
7
  import { syncOpenEffect } from '../popover.svelte.js'
4
8
  import type { DialogProps } from './types.js'
5
9
 
6
10
  let {
7
11
  element = $bindable(),
8
12
  open = $bindable(),
13
+ variant = 'basic',
14
+ closeLabel = 'Close',
9
15
  quick = false,
10
16
  children,
11
17
  headline,
@@ -13,13 +19,18 @@
13
19
  icon,
14
20
  supportingText,
15
21
  actions,
22
+ actionBar,
16
23
  divider,
24
+ closedby = 'any',
25
+ role,
17
26
  ontoggle,
27
+ oncancel,
18
28
  'aria-labelledby': ariaLabelledby,
19
29
  ...attributes
20
30
  }: DialogProps = $props()
21
31
 
22
32
  const uid = $props.id()
33
+ let fullScreen = $derived(variant === 'full-screen')
23
34
 
24
35
  export const show = () => {
25
36
  if (element && !element.open) element.showModal()
@@ -37,50 +48,90 @@
37
48
  )
38
49
  </script>
39
50
 
51
+ {#snippet title()}
52
+ <svelte:element this={`h${headlineLevel}`} id="{uid}-dialog-headline" class="np-dialog-headline">
53
+ {headline}
54
+ </svelte:element>
55
+ {/snippet}
56
+
57
+ <!-- A basic dialog is an alert dialog on the web, as M3 asks; one that holds a task, like a
58
+ full-screen dialog or a picker, is a plain dialog. -->
40
59
  <dialog
41
- bind:this={element}
42
60
  {...attributes}
43
- closedby="any"
61
+ bind:this={element}
62
+ {@attach !quick && exitAnimation}
63
+ {closedby}
64
+ role={role ?? (fullScreen ? 'dialog' : 'alertdialog')}
44
65
  aria-labelledby={ariaLabelledby ?? (headline ? `${uid}-dialog-headline` : undefined)}
45
66
  aria-describedby={supportingText ? `${uid}-dialog-supporting-text` : undefined}
46
- class={['np-dialog-container', !quick && 'np-animate', attributes.class]}
67
+ class={[
68
+ 'np-dialog-container',
69
+ fullScreen && 'np-dialog-full-screen',
70
+ !quick && 'np-animate np-exit-scrim',
71
+ attributes.class,
72
+ ]}
47
73
  ontoggle={(event) => {
48
74
  open = event.newState === 'open'
49
75
  ontoggle?.(event)
50
76
  }}
77
+ oncancel={(event) => {
78
+ // Browsers without closedby still close on Escape.
79
+ if (closedby === 'none') event.preventDefault()
80
+ oncancel?.(event)
81
+ }}
51
82
  onclick={(event) => {
52
83
  attributes.onclick?.(event)
53
- if (event.target === element) close()
84
+ if (event.target === element && closedby === 'any') close()
54
85
  }}
55
86
  >
56
87
  <div class="np-dialog">
57
- {#if icon}
58
- <div class="np-dialog-icon">
59
- {@render icon()}
88
+ {#if fullScreen}
89
+ <div class="np-dialog-header">
90
+ <IconButton type="button" aria-label={closeLabel} onclick={close}>
91
+ <CloseIcon />
92
+ </IconButton>
93
+ {#if headline}
94
+ {@render title()}
95
+ {/if}
96
+ {#if actions}
97
+ <div class="np-dialog-header-actions">{@render actions()}</div>
98
+ {/if}
60
99
  </div>
100
+ {#if divider}
101
+ <Divider />
102
+ {/if}
103
+ {:else}
104
+ {#if icon}
105
+ <div class="np-dialog-icon">
106
+ {@render icon()}
107
+ </div>
108
+ {/if}
109
+ {#if headline}
110
+ <div class={['np-dialog-title', icon && 'np-dialog-title-centered']}>{@render title()}</div>
111
+ {/if}
112
+ {#if divider && (supportingText || children)}
113
+ <Divider style="margin-top: 1rem" --np-divider-color="var(--np-color-outline)" />
114
+ {/if}
61
115
  {/if}
62
- {#if headline}
63
- <svelte:element
64
- this={`h${headlineLevel}`}
65
- id="{uid}-dialog-headline"
66
- class="np-dialog-headline"
67
- style={icon ? 'text-align: center' : ''}
68
- >
69
- {headline}
70
- </svelte:element>
71
- {/if}
72
- {#if supportingText}
73
- <p id="{uid}-dialog-supporting-text" class="np-dialog-supporting-text">{supportingText}</p>
74
- {/if}
75
- {#if divider}
76
- <Divider style="margin-top: 1rem" --np-divider-color="var(--np-color-outline)" />
77
- {/if}
78
- {#if children}
116
+ <!-- Headline and actions stay put; everything between them scrolls. -->
117
+ {#if supportingText || children}
79
118
  <div class="np-dialog-scroller">
80
- {@render children()}
119
+ {#if supportingText}
120
+ <p id="{uid}-dialog-supporting-text" class="np-dialog-supporting-text">
121
+ {supportingText}
122
+ </p>
123
+ {/if}
124
+ {@render children?.()}
81
125
  </div>
82
126
  {/if}
83
- {#if actions}
127
+ {#if fullScreen}
128
+ {#if actionBar}
129
+ <div class="np-dialog-action-bar">{@render actionBar()}</div>
130
+ {/if}
131
+ {:else if actions}
132
+ {#if divider && (supportingText || children)}
133
+ <Divider style="margin-bottom: 1rem" --np-divider-color="var(--np-color-outline)" />
134
+ {/if}
84
135
  <div class="np-dialog-actions">
85
136
  {@render actions()}
86
137
  </div>
@@ -117,7 +168,9 @@
117
168
  padding: var(--np-dialog-padding, 1.5rem);
118
169
  border-radius: var(--np-dialog-container-shape, var(--np-shape-corner-extra-large));
119
170
  box-shadow: var(--np-dialog-elevation, var(--np-elevation-3));
120
- max-height: var(--np-dialog-max-height, calc(100dvh - 3rem));
171
+ /* Its padding included, so it fits the window inside the container's own inset. */
172
+ box-sizing: border-box;
173
+ max-height: var(--np-dialog-max-height, calc(100dvh - 4rem));
121
174
  scrollbar-color: var(--np-color-on-surface-variant) transparent;
122
175
  scrollbar-width: thin;
123
176
  position: relative;
@@ -138,33 +191,118 @@
138
191
  opacity: 0.32;
139
192
  }
140
193
 
194
+ /* It fades in, and closes like a menu: a short fade while it shrinks a little, its scrim
195
+ fading along. */
141
196
  .np-animate {
197
+ --_exit: var(--np-motion-expressive-default-effects);
198
+ --np-exit-scrim-motion: var(--_exit);
142
199
  transition:
143
- opacity var(--np-motion-expressive-slow-effects),
144
- display var(--np-motion-expressive-slow-effects) allow-discrete,
145
- overlay var(--np-motion-expressive-slow-effects) allow-discrete;
200
+ opacity var(--_exit),
201
+ scale var(--_exit),
202
+ display var(--_exit) allow-discrete,
203
+ overlay var(--_exit) allow-discrete;
146
204
  opacity: 0;
147
205
  }
148
206
  .np-animate[open] {
207
+ --_enter: var(--np-motion-expressive-slow-effects);
208
+ transition:
209
+ opacity var(--_enter),
210
+ display var(--_enter) allow-discrete,
211
+ overlay var(--_enter) allow-discrete;
149
212
  opacity: 1;
150
213
  @starting-style {
151
214
  opacity: 0;
152
215
  }
153
216
  }
217
+ @media (prefers-reduced-motion: no-preference) {
218
+ .np-animate:not([open]) {
219
+ scale: 0.95;
220
+ }
221
+ }
154
222
  .np-animate::backdrop {
155
223
  transition:
156
- opacity var(--np-motion-expressive-slow-effects),
157
- display var(--np-motion-expressive-slow-effects) allow-discrete,
158
- overlay var(--np-motion-expressive-slow-effects) allow-discrete;
224
+ opacity var(--np-motion-expressive-default-effects),
225
+ display var(--np-motion-expressive-default-effects) allow-discrete,
226
+ overlay var(--np-motion-expressive-default-effects) allow-discrete;
159
227
  opacity: 0;
160
228
  }
161
229
  .np-animate[open]::backdrop {
230
+ transition:
231
+ opacity var(--np-motion-expressive-slow-effects),
232
+ display var(--np-motion-expressive-slow-effects) allow-discrete,
233
+ overlay var(--np-motion-expressive-slow-effects) allow-discrete;
162
234
  opacity: 0.32;
163
235
  @starting-style {
164
236
  opacity: 0;
165
237
  }
166
238
  }
167
239
 
240
+ .np-dialog-title-centered {
241
+ text-align: center;
242
+ }
243
+
244
+ /* Full-screen: a 56dp header with the close button, headline and confirming action, the
245
+ content 24dp from the sides and an optional 56dp action bar. It fills a compact window and
246
+ floats, at most 560dp wide, on larger ones. */
247
+ .np-dialog-full-screen {
248
+ width: var(--np-dialog-container-width, 35rem);
249
+ }
250
+ .np-dialog-full-screen .np-dialog {
251
+ padding: 0;
252
+ }
253
+ .np-dialog-header {
254
+ flex: none;
255
+ display: flex;
256
+ align-items: center;
257
+ gap: 0.5rem;
258
+ min-height: 3.5rem;
259
+ padding-inline: 0.5rem 0.75rem;
260
+ --np-icon-button-icon-color: var(--np-color-on-surface);
261
+ }
262
+ .np-dialog-header .np-dialog-headline {
263
+ flex: 1;
264
+ min-width: 0;
265
+ margin: 0;
266
+ font-size: 1.375rem;
267
+ line-height: 1.75rem;
268
+ overflow: hidden;
269
+ text-overflow: ellipsis;
270
+ white-space: nowrap;
271
+ }
272
+ .np-dialog-header-actions {
273
+ display: flex;
274
+ gap: 0.5rem;
275
+ margin-inline-start: auto;
276
+ }
277
+ .np-dialog-full-screen .np-dialog-scroller {
278
+ padding: 1.5rem 1.5rem;
279
+ }
280
+ .np-dialog-action-bar {
281
+ flex: none;
282
+ display: flex;
283
+ align-items: center;
284
+ justify-content: flex-end;
285
+ gap: 0.5rem;
286
+ min-height: 3.5rem;
287
+ padding-inline: 1.5rem;
288
+ }
289
+ @media (width < 600px) {
290
+ .np-dialog-full-screen {
291
+ width: 100%;
292
+ height: 100%;
293
+ min-width: 0;
294
+ max-height: none;
295
+ margin: 0;
296
+ padding: 0;
297
+ }
298
+ .np-dialog-full-screen .np-dialog {
299
+ height: 100%;
300
+ max-height: none;
301
+ border-radius: 0;
302
+ box-shadow: none;
303
+ }
304
+ }
305
+
168
306
  .np-dialog-icon {
169
307
  color: var(--np-color-secondary);
170
308
  display: flex;
@@ -1,7 +1,8 @@
1
+ import '../internal/exit.css';
1
2
  import type { DialogProps } from './types.js';
2
3
  declare const Dialog: import("svelte").Component<DialogProps, {
3
4
  show: () => void;
4
5
  close: () => void;
5
- }, "element" | "open">;
6
+ }, "open" | "element">;
6
7
  type Dialog = ReturnType<typeof Dialog>;
7
8
  export default Dialog;
@@ -1,6 +1,12 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { HTMLDialogAttributes } from 'svelte/elements';
3
3
  export interface DialogProps extends Omit<HTMLDialogAttributes, 'open'> {
4
+ /** A basic dialog floats over a scrim; a full-screen one fills a compact window. */
5
+ variant?: 'basic' | 'full-screen';
6
+ /** Names the close button of a full-screen dialog. */
7
+ closeLabel?: string;
8
+ /** The 56dp bar at the bottom of a full-screen dialog. */
9
+ actionBar?: Snippet;
4
10
  icon?: Snippet;
5
11
  headline?: string;
6
12
  headlineLevel?: 1 | 2 | 3 | 4 | 5 | 6;
@@ -1,31 +1,57 @@
1
1
  <script lang="ts">
2
2
  import type { DividerProps } from './types.js'
3
3
 
4
- let { variant = 'full', element = $bindable(), ...attributes }: DividerProps = $props()
4
+ let {
5
+ variant = 'full',
6
+ orientation = 'horizontal',
7
+ element = $bindable(),
8
+ ...attributes
9
+ }: DividerProps = $props()
5
10
  </script>
6
11
 
7
12
  <template
8
13
  {...attributes}
9
14
  bind:this={element}
10
15
  role="separator"
11
- class={['np-divider', variant, attributes.class]}
16
+ aria-orientation={orientation === 'vertical' ? 'vertical' : undefined}
17
+ class={[
18
+ 'np-divider',
19
+ variant,
20
+ orientation === 'vertical' && 'np-divider-vertical',
21
+ attributes.class,
22
+ ]}
12
23
  ></template>
13
24
 
14
25
  <style>
15
- .inset {
26
+ .np-divider {
27
+ display: flex;
28
+ flex-shrink: 0;
29
+ background-color: var(--np-divider-color, var(--np-color-outline-variant));
30
+ }
31
+ .np-divider:not(.np-divider-vertical) {
32
+ height: 1px;
33
+ }
34
+ .np-divider:not(.np-divider-vertical).full {
35
+ width: 100%;
36
+ }
37
+ .np-divider:not(.np-divider-vertical).inset {
16
38
  width: calc(100% - 1rem);
17
39
  margin-inline-start: 1rem;
18
40
  }
19
- .inset-middle {
41
+ .np-divider:not(.np-divider-vertical).inset-middle {
20
42
  width: calc(100% - 2rem);
21
43
  margin-inline: 1rem;
22
44
  }
23
- .full {
24
- width: 100%;
45
+
46
+ /* Takes the height of a flex or grid row. */
47
+ .np-divider-vertical {
48
+ width: 1px;
49
+ align-self: stretch;
25
50
  }
26
- .np-divider {
27
- height: 1px;
28
- display: flex;
29
- background-color: var(--np-divider-color, var(--np-color-outline-variant));
51
+ .np-divider-vertical.inset {
52
+ margin-block-start: 1rem;
53
+ }
54
+ .np-divider-vertical.inset-middle {
55
+ margin-block: 1rem;
30
56
  }
31
57
  </style>
@@ -1,5 +1,6 @@
1
1
  import type { HTMLAttributes } from 'svelte/elements';
2
2
  export interface DividerProps extends Omit<HTMLAttributes<HTMLElement>, 'role'> {
3
3
  variant?: 'full' | 'inset' | 'inset-middle';
4
+ orientation?: 'horizontal' | 'vertical';
4
5
  element?: HTMLElement;
5
6
  }
@@ -1,6 +1,5 @@
1
1
  <script lang="ts">
2
2
  import '../internal/focus-ring.css'
3
- import { pressMorph } from '../press.svelte.js'
4
3
  import Ripple from '../ripple/Ripple.svelte'
5
4
  import Tooltip from '../tooltip/Tooltip.svelte'
6
5
  import type { HTMLButtonAttributes } from 'svelte/elements'
@@ -9,9 +8,8 @@
9
8
  let {
10
9
  variant = 'primary-container',
11
10
  size = 's',
12
- shape = 'round',
11
+ shape = 'square',
13
12
  lowered = false,
14
- disabled = false,
15
13
  collapsed = false,
16
14
  icon,
17
15
  label,
@@ -21,16 +19,10 @@
21
19
 
22
20
  const uid = $props.id()
23
21
 
24
- let isLink = $derived(attributes.href != null && !disabled)
25
- let tooltipId = $derived(collapsed && label && !disabled ? uid : undefined)
22
+ let isLink = $derived(attributes.href != null)
23
+ let tooltipId = $derived(collapsed && label ? uid : undefined)
26
24
 
27
- const morph = pressMorph()
28
- morph.supersededBy(() => shape)
29
-
30
- const handlePress = () => {
31
- if (disabled) return
32
- morph.press()
33
- }
25
+ let tooltipNames = $derived(!!tooltipId && !attributes['aria-label'])
34
26
 
35
27
  let classes = $derived([
36
28
  'np-extended-fab',
@@ -40,15 +32,12 @@
40
32
  shape,
41
33
  lowered && 'np-fab-lowered',
42
34
  collapsed && 'np-fab-collapsed',
43
- morph.pressed && 'pressed',
44
35
  attributes.class,
45
36
  ])
46
37
  </script>
47
38
 
48
39
  {#snippet content()}
49
- {#if !disabled}
50
- <Ripple forElement={element} />
51
- {/if}
40
+ <Ripple forElement={element} />
52
41
  {#if icon}
53
42
  <span class="np-fab-icon">{@render icon()}</span>
54
43
  {/if}
@@ -60,14 +49,10 @@
60
49
  {#if isLink}
61
50
  <a
62
51
  {...attributes}
63
- onclick={(event) => {
64
- handlePress()
65
- attributes.onclick?.(event)
66
- }}
67
52
  bind:this={element}
68
- aria-describedby={tooltipId ?? attributes['aria-describedby']}
53
+ aria-describedby={(!tooltipNames && tooltipId) || attributes['aria-describedby']}
69
54
  interestfor={tooltipId ?? attributes['interestfor']}
70
- aria-label={collapsed ? label || attributes['aria-label'] : attributes['aria-label']}
55
+ aria-label={attributes['aria-label'] ?? (collapsed ? label : undefined)}
71
56
  class={classes}
72
57
  >
73
58
  {@render content()}
@@ -75,16 +60,11 @@
75
60
  {:else}
76
61
  <button
77
62
  {...attributes as HTMLButtonAttributes}
78
- onclick={(event) => {
79
- handlePress()
80
- attributes.onclick?.(event)
81
- }}
82
63
  bind:this={element}
83
- aria-describedby={tooltipId ?? attributes['aria-describedby']}
64
+ aria-describedby={(!tooltipNames && tooltipId) || attributes['aria-describedby']}
84
65
  interestfor={tooltipId ?? attributes['interestfor']}
85
- aria-label={collapsed ? label || attributes['aria-label'] : attributes['aria-label']}
66
+ aria-label={attributes['aria-label'] ?? (collapsed ? label : undefined)}
86
67
  type={(attributes['type'] as HTMLButtonAttributes['type']) ?? 'button'}
87
- {disabled}
88
68
  class={classes}
89
69
  >
90
70
  {@render content()}
@@ -129,19 +109,10 @@
129
109
  .square {
130
110
  border-radius: var(--np-fab-shape, var(--_square-radius));
131
111
  }
132
- @media (prefers-reduced-motion: no-preference) {
133
- .round:not(:disabled):is(:active, .pressed) {
134
- border-radius: var(--np-fab-pressed-shape, var(--_square-radius));
135
- }
136
- .square:not(:disabled):is(:active, .pressed) {
137
- border-radius: var(--np-fab-pressed-shape, calc(var(--_height) / 2));
138
- }
139
- }
140
-
141
112
  .s {
142
113
  --_height: 3.5rem;
143
114
  --_square-radius: 1rem;
144
- --_gap: 0.75rem;
115
+ --_gap: 0.5rem;
145
116
  --_padding: 1rem;
146
117
  font-size: 1rem;
147
118
  --np-icon-size: 1.5rem;
@@ -149,8 +120,9 @@
149
120
  .m {
150
121
  --_height: 5rem;
151
122
  --_square-radius: 1.25rem;
152
- --_gap: 1rem;
153
- --_padding: 1.5rem;
123
+ --_gap: 0.75rem;
124
+ --_padding: 1.625rem;
125
+ font-weight: 400;
154
126
  font-size: 1.375rem;
155
127
  --np-icon-size: 1.75rem;
156
128
  }
@@ -158,15 +130,17 @@
158
130
  --_height: 6rem;
159
131
  --_square-radius: 1.75rem;
160
132
  --_gap: 1rem;
161
- --_padding: 2rem;
133
+ --_padding: 1.75rem;
134
+ font-weight: 400;
162
135
  font-size: 1.5rem;
163
136
  --np-icon-size: 2.25rem;
164
137
  }
165
138
 
139
+ /* Collapsed, the icon keeps its place at the start: the padding centres it in the square, and
140
+ only the gap and the label shrink, so the icon does not drift while the FAB changes width. */
166
141
  .np-fab-collapsed {
167
142
  width: var(--_height);
168
- padding-inline: 0;
169
- justify-content: center;
143
+ padding-inline: calc((var(--_height) - var(--np-icon-size)) / 2);
170
144
  gap: 0;
171
145
  }
172
146
  .np-fab-collapsed .np-fab-label {
@@ -175,25 +149,24 @@
175
149
  }
176
150
 
177
151
  .np-fab-label {
178
- max-width: 20rem;
152
+ max-width: max-content;
179
153
  overflow: hidden;
180
- text-overflow: ellipsis;
181
154
  }
182
155
 
183
156
  @media (prefers-reduced-motion: no-preference) {
184
157
  .np-extended-fab {
185
158
  transition:
186
- width var(--np-motion-expressive-default-spatial),
187
- gap var(--np-motion-expressive-default-spatial),
188
- padding-inline var(--np-motion-expressive-default-spatial),
189
- border-radius var(--np-motion-expressive-default-spatial),
159
+ width var(--np-fab-motion-spatial, var(--np-motion-expressive-default-spatial)),
160
+ gap var(--np-fab-motion-spatial, var(--np-motion-expressive-default-spatial)),
161
+ padding-inline var(--np-fab-motion-spatial, var(--np-motion-expressive-default-spatial)),
162
+ border-radius var(--np-fab-motion-spatial, var(--np-motion-expressive-default-spatial)),
190
163
  box-shadow var(--np-motion-expressive-fast-effects),
191
164
  background-color var(--np-motion-expressive-fast-effects);
192
165
  }
193
166
  .np-fab-label {
194
167
  transition:
195
168
  opacity var(--np-motion-expressive-fast-effects),
196
- max-width var(--np-motion-expressive-default-spatial);
169
+ max-width var(--np-fab-motion-spatial, var(--np-motion-expressive-default-spatial));
197
170
  }
198
171
  }
199
172
 
@@ -227,21 +200,14 @@
227
200
  }
228
201
 
229
202
  @media (hover: hover) {
230
- .np-extended-fab:not(:disabled):hover {
203
+ .np-extended-fab:hover {
231
204
  box-shadow: var(--np-fab-elevation, var(--np-elevation-4));
232
205
  }
233
- .np-fab-lowered:not(:disabled):hover {
206
+ .np-fab-lowered:hover {
234
207
  box-shadow: var(--np-fab-elevation, var(--np-elevation-2));
235
208
  }
236
209
  }
237
210
 
238
- .np-extended-fab:disabled {
239
- cursor: default;
240
- box-shadow: none;
241
- background-color: color-mix(in srgb, var(--np-color-on-surface) 12%, transparent);
242
- color: color-mix(in srgb, var(--np-color-on-surface) 38%, transparent);
243
- }
244
-
245
211
  .np-fab-icon {
246
212
  display: flex;
247
213
  flex: none;