noph-ui 0.51.2 → 0.53.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 (187) hide show
  1. package/dist/animation.d.ts +14 -1
  2. package/dist/animation.js +55 -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 +53 -30
  11. package/dist/button/ButtonGroup.svelte +139 -19
  12. package/dist/button/ButtonGroup.svelte.d.ts +1 -1
  13. package/dist/button/IconButton.svelte +51 -33
  14. package/dist/button/SegmentedButton.svelte +52 -15
  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/card/Card.svelte +3 -19
  23. package/dist/card/types.d.ts +12 -2
  24. package/dist/carousel/Carousel.svelte +77 -14
  25. package/dist/carousel/CarouselItem.svelte +26 -14
  26. package/dist/carousel/carouselMorph.js +1 -1
  27. package/dist/carousel/carouselStrategy.js +25 -27
  28. package/dist/checkbox/Checkbox.svelte +6 -8
  29. package/dist/chip/ChipSet.svelte +28 -2
  30. package/dist/chip/FilterChip.svelte +33 -17
  31. package/dist/chip/InputChip.svelte +127 -26
  32. package/dist/chip/SuggestionChip.svelte +6 -22
  33. package/dist/chip/SuggestionChip.svelte.d.ts +1 -2
  34. package/dist/chip/types.d.ts +8 -1
  35. package/dist/date-picker/Calendar.svelte +34 -31
  36. package/dist/date-picker/Calendar.svelte.d.ts +2 -0
  37. package/dist/date-picker/CalendarHeader.svelte +84 -62
  38. package/dist/date-picker/CalendarHeader.svelte.d.ts +3 -0
  39. package/dist/date-picker/DatePickerDialog.svelte +135 -75
  40. package/dist/date-picker/DatePickerDialog.svelte.d.ts +4 -3
  41. package/dist/date-picker/DateRangePicker.svelte +227 -78
  42. package/dist/date-picker/DateRangePicker.svelte.d.ts +3 -3
  43. package/dist/date-picker/DockedDatePicker.svelte +76 -457
  44. package/dist/date-picker/DockedDatePicker.svelte.d.ts +3 -3
  45. package/dist/date-picker/DockedDateTimePicker.svelte +128 -518
  46. package/dist/date-picker/DockedDateTimePicker.svelte.d.ts +3 -3
  47. package/dist/date-picker/DockedPickerField.svelte +221 -0
  48. package/dist/date-picker/DockedPickerField.svelte.d.ts +9 -0
  49. package/dist/date-picker/MonthPanel.svelte +291 -0
  50. package/dist/date-picker/MonthPanel.svelte.d.ts +12 -0
  51. package/dist/date-picker/SelectionList.svelte +7 -6
  52. package/dist/date-picker/SelectionList.svelte.d.ts +1 -0
  53. package/dist/date-picker/TimeColumn.svelte +2 -6
  54. package/dist/date-picker/TimeColumn.svelte.d.ts +1 -0
  55. package/dist/date-picker/YearGrid.svelte +6 -13
  56. package/dist/date-picker/YearGrid.svelte.d.ts +1 -0
  57. package/dist/date-picker/dateUtils.d.ts +4 -0
  58. package/dist/date-picker/dateUtils.js +15 -88
  59. package/dist/date-picker/exitVisibility.svelte.js +13 -6
  60. package/dist/date-picker/formatters.d.ts +16 -0
  61. package/dist/date-picker/formatters.js +48 -15
  62. package/dist/date-picker/internalTypes.d.ts +59 -1
  63. package/dist/date-picker/shortcuts.d.ts +11 -0
  64. package/dist/date-picker/shortcuts.js +40 -0
  65. package/dist/date-picker/timeUtils.d.ts +8 -3
  66. package/dist/date-picker/timeUtils.js +33 -51
  67. package/dist/date-picker/types.d.ts +22 -0
  68. package/dist/dialog/Dialog.svelte +171 -33
  69. package/dist/dialog/Dialog.svelte.d.ts +2 -1
  70. package/dist/dialog/types.d.ts +6 -0
  71. package/dist/divider/Divider.svelte +36 -10
  72. package/dist/divider/types.d.ts +1 -0
  73. package/dist/fab/ExtendedFab.svelte +26 -60
  74. package/dist/fab/Fab.svelte +13 -48
  75. package/dist/fab/FabMenu.svelte +102 -44
  76. package/dist/fab/FabMenu.svelte.d.ts +2 -1
  77. package/dist/fab/FabMenuItem.svelte +8 -0
  78. package/dist/fab/FabMenuItem.svelte.d.ts +4 -0
  79. package/dist/fab/index.d.ts +1 -0
  80. package/dist/fab/index.js +1 -0
  81. package/dist/fab/types.d.ts +6 -4
  82. package/dist/icons/Icon.svelte +1 -4
  83. package/dist/icons/MenuIcon.svelte +13 -0
  84. package/dist/icons/MenuIcon.svelte.d.ts +7 -0
  85. package/dist/icons/MenuOpenIcon.svelte +15 -0
  86. package/dist/icons/MenuOpenIcon.svelte.d.ts +7 -0
  87. package/dist/icons/index.d.ts +2 -0
  88. package/dist/icons/index.js +2 -0
  89. package/dist/internal/exit.css +33 -0
  90. package/dist/internal/field.css +337 -0
  91. package/dist/internal/fieldTypes.d.ts +6 -0
  92. package/dist/internal/focus-ring.css +25 -6
  93. package/dist/internal/interest.css +3 -1
  94. package/dist/internal/pop-in.css +19 -0
  95. package/dist/internal/visually-hidden.css +11 -0
  96. package/dist/keyboard-nav.d.ts +12 -1
  97. package/dist/keyboard-nav.js +104 -14
  98. package/dist/list/Item.svelte +84 -24
  99. package/dist/list/List.svelte +35 -2
  100. package/dist/list/ListItem.svelte +16 -2
  101. package/dist/list/context.d.ts +6 -0
  102. package/dist/list/context.js +4 -0
  103. package/dist/list/types.d.ts +7 -0
  104. package/dist/media.d.ts +1 -0
  105. package/dist/media.js +1 -0
  106. package/dist/menu/Menu.svelte +53 -20
  107. package/dist/menu/Menu.svelte.d.ts +3 -1
  108. package/dist/menu/MenuItem.svelte +9 -3
  109. package/dist/menu/types.d.ts +7 -1
  110. package/dist/navigation-bar/NavigationBar.svelte +9 -3
  111. package/dist/navigation-bar/NavigationBarItem.svelte +31 -39
  112. package/dist/navigation-drawer/NavigationDrawer.svelte +96 -20
  113. package/dist/navigation-drawer/NavigationDrawer.svelte.d.ts +2 -1
  114. package/dist/navigation-drawer/NavigationDrawerItem.svelte +10 -12
  115. package/dist/navigation-drawer/NavigationDrawerSection.svelte +49 -0
  116. package/dist/navigation-drawer/NavigationDrawerSection.svelte.d.ts +4 -0
  117. package/dist/navigation-drawer/index.d.ts +1 -0
  118. package/dist/navigation-drawer/index.js +1 -0
  119. package/dist/navigation-drawer/types.d.ts +12 -0
  120. package/dist/navigation-rail/NavigationRail.svelte +367 -7
  121. package/dist/navigation-rail/NavigationRail.svelte.d.ts +1 -1
  122. package/dist/navigation-rail/NavigationRailItem.svelte +185 -59
  123. package/dist/navigation-rail/NavigationRailSection.svelte +57 -0
  124. package/dist/navigation-rail/NavigationRailSection.svelte.d.ts +4 -0
  125. package/dist/navigation-rail/context.d.ts +6 -0
  126. package/dist/navigation-rail/context.js +5 -0
  127. package/dist/navigation-rail/index.d.ts +1 -0
  128. package/dist/navigation-rail/index.js +1 -0
  129. package/dist/navigation-rail/morph.d.ts +14 -0
  130. package/dist/navigation-rail/morph.js +78 -0
  131. package/dist/navigation-rail/types.d.ts +21 -0
  132. package/dist/press.svelte.d.ts +0 -1
  133. package/dist/press.svelte.js +0 -11
  134. package/dist/progress/CircularProgress.svelte +28 -82
  135. package/dist/progress/CircularProgress.svelte.d.ts +1 -0
  136. package/dist/progress/LinearProgress.svelte +67 -171
  137. package/dist/progress/LinearProgress.svelte.d.ts +1 -0
  138. package/dist/progress/four-color.css +34 -0
  139. package/dist/progress/types.d.ts +1 -0
  140. package/dist/radio/Radio.svelte +13 -10
  141. package/dist/ripple/Ripple.svelte +8 -16
  142. package/dist/search/Search.svelte +238 -58
  143. package/dist/search/Search.svelte.d.ts +1 -0
  144. package/dist/search/types.d.ts +4 -0
  145. package/dist/select/Check.svelte +8 -31
  146. package/dist/select/Check.svelte.d.ts +1 -1
  147. package/dist/select/NativeSelect.svelte +113 -374
  148. package/dist/select/NativeSelect.svelte.d.ts +1 -0
  149. package/dist/select/Option.svelte +2 -11
  150. package/dist/select/Select.svelte +170 -858
  151. package/dist/select/Select.svelte.d.ts +1 -0
  152. package/dist/select/VirtualList.svelte +16 -2
  153. package/dist/select/VirtualList.svelte.d.ts +6 -2
  154. package/dist/sheet/Sheet.svelte +229 -22
  155. package/dist/sheet/Sheet.svelte.d.ts +3 -1
  156. package/dist/sheet/types.d.ts +10 -0
  157. package/dist/slider/Slider.svelte +14 -64
  158. package/dist/slider/Slider.svelte.d.ts +1 -0
  159. package/dist/snackbar/Snackbar.svelte +3 -0
  160. package/dist/snackbar/Snackbar.svelte.d.ts +2 -1
  161. package/dist/switch/Switch.svelte +53 -19
  162. package/dist/tabs/Tab.svelte +30 -18
  163. package/dist/tabs/Tabs.svelte +28 -4
  164. package/dist/tabs/types.d.ts +3 -0
  165. package/dist/text-field/TextField.svelte +124 -616
  166. package/dist/text-field/TextField.svelte.d.ts +1 -0
  167. package/dist/time-picker/ClockDial.svelte +8 -25
  168. package/dist/time-picker/ClockDial.svelte.d.ts +2 -0
  169. package/dist/time-picker/DockedTimePicker.svelte +102 -239
  170. package/dist/time-picker/DockedTimePicker.svelte.d.ts +3 -3
  171. package/dist/time-picker/PeriodSelector.svelte +8 -7
  172. package/dist/time-picker/PeriodSelector.svelte.d.ts +1 -0
  173. package/dist/time-picker/TimeInputs.svelte +13 -2
  174. package/dist/time-picker/TimePickerDialog.svelte +33 -26
  175. package/dist/time-picker/TimePickerDialog.svelte.d.ts +3 -3
  176. package/dist/time-picker/TimePickerPanel.svelte +26 -4
  177. package/dist/time-picker/TimeSelectors.svelte +4 -9
  178. package/dist/time-picker/TimeSelectors.svelte.d.ts +1 -0
  179. package/dist/time-picker/internalTypes.d.ts +2 -0
  180. package/dist/time-picker/timePickerState.svelte.js +3 -9
  181. package/dist/toolbar/Toolbar.svelte +21 -11
  182. package/dist/toolbar/types.d.ts +1 -0
  183. package/dist/tooltip/RichTooltip.svelte +2 -23
  184. package/dist/tooltip/RichTooltip.svelte.d.ts +2 -1
  185. package/dist/tooltip/Tooltip.svelte +5 -25
  186. package/dist/tooltip/Tooltip.svelte.d.ts +2 -1
  187. package/package.json +12 -12
@@ -21,12 +21,3 @@ export const expandedWidths = (widths, index, expandedRatio, compressionLimit) =
21
21
  }
22
22
  return next;
23
23
  };
24
- export const parseMotion = (token) => {
25
- const [time, ...rest] = token.trim().split(/\s+/);
26
- const value = Number.parseFloat(time);
27
- const easing = rest.join(' ');
28
- return {
29
- duration: Number.isNaN(value) ? 0 : time.endsWith('ms') ? value : value * 1000,
30
- easing: easing || 'linear',
31
- };
32
- };
@@ -0,0 +1,10 @@
1
+ /** What a `ButtonGroup` decides for its buttons: their size and shape, and with `selection` which are selected. */
2
+ export interface ButtonGroupContext {
3
+ readonly selection: 'single' | 'multiple' | undefined;
4
+ readonly size: 'xs' | 's' | 'm' | 'l' | 'xl' | undefined;
5
+ readonly shape: 'round' | 'square' | undefined;
6
+ isSelected: (value: string) => boolean;
7
+ toggle: (value: string) => void;
8
+ }
9
+ export declare const setButtonGroupContext: (context: ButtonGroupContext) => ButtonGroupContext;
10
+ export declare const getButtonGroupContext: () => ButtonGroupContext | undefined;
@@ -0,0 +1,4 @@
1
+ import { getContext, setContext } from 'svelte';
2
+ const key = Symbol('np-button-group');
3
+ export const setButtonGroupContext = (context) => setContext(key, context);
4
+ export const getButtonGroupContext = () => getContext(key);
@@ -25,12 +25,31 @@ export interface IconButtonProps extends BaseButtonProps {
25
25
  }
26
26
  export interface ButtonGroupProps extends HTMLAttributes<HTMLDivElement> {
27
27
  variant?: 'standard' | 'connected';
28
+ /** Size of the buttons that don't set their own. */
29
+ size?: BaseButtonProps['size'];
30
+ /** Shape of the buttons that don't set their own. */
31
+ shape?: BaseButtonProps['shape'];
32
+ /**
33
+ * Lets the group manage selection: its buttons with a `value` become toggles, and `value`
34
+ * holds the selected one (`single`) or the selected ones (`multiple`).
35
+ */
36
+ selection?: 'single' | 'multiple';
37
+ value?: string | string[] | null;
38
+ /** With `selection`, keeps at least one button selected and makes a form require one. */
39
+ required?: boolean;
40
+ /** With `selection`, submits the selected values under this name, like a radio or checkbox group. */
41
+ name?: string;
42
+ form?: string;
43
+ /** Arrow keys move between the buttons, which share one tab stop. Off, Tab visits each. */
44
+ arrowKeys?: boolean;
28
45
  expandedRatio?: number;
29
46
  compressionLimit?: number;
30
47
  element?: HTMLElement;
31
48
  }
32
49
  export interface SplitButtonProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onclick'> {
33
50
  label?: string;
51
+ /** Shows only `icon` on the leading button. `label` then names it and becomes its tooltip. */
52
+ iconOnly?: boolean;
34
53
  icon?: Snippet;
35
54
  menu?: Snippet<[string]>;
36
55
  variant?: Exclude<ButtonProps['variant'], 'text'>;
@@ -66,6 +66,7 @@
66
66
  aria-disabled={disabled}
67
67
  class={[
68
68
  'np-card-container',
69
+ 'np-focus-ring',
69
70
  `np-card-${variant}`,
70
71
  disabled && 'np-card-disabled',
71
72
  attributes.class,
@@ -82,6 +83,7 @@
82
83
  {disabled}
83
84
  class={[
84
85
  'np-card-container',
86
+ 'np-focus-ring',
85
87
  `np-card-${variant}`,
86
88
  disabled && 'np-card-disabled',
87
89
  attributes.class,
@@ -98,6 +100,7 @@
98
100
  role="link"
99
101
  class={[
100
102
  'np-card-container',
103
+ 'np-focus-ring',
101
104
  `np-card-${variant}`,
102
105
  disabled && 'np-card-disabled',
103
106
  attributes.class,
@@ -111,7 +114,6 @@
111
114
  .np-card-elevated {
112
115
  border-width: 0;
113
116
  box-shadow: var(--np-elevation-1);
114
- color: var(--np-color-on-surface);
115
117
  --background-color: var(
116
118
  --np-elevated-card-container-color,
117
119
  var(--np-color-surface-container-low)
@@ -123,7 +125,6 @@
123
125
  }
124
126
  .np-card-filled {
125
127
  border-width: 0;
126
- color: var(--np-color-on-surface);
127
128
  --background-color: var(
128
129
  --np-filled-card-container-color,
129
130
  var(--np-color-surface-container-highest)
@@ -134,7 +135,6 @@
134
135
  --background-color: var(--np-color-surface-variant);
135
136
  }
136
137
  .np-card-outlined {
137
- color: var(--np-color-on-surface);
138
138
  border: 1px solid var(--np-color-outline-variant);
139
139
  --background-color: var(--np-outlined-card-container-color, var(--np-color-surface));
140
140
  --border-radius: var(--np-outlined-card-container-shape, var(--np-shape-corner-medium));
@@ -159,21 +159,6 @@
159
159
  cursor: pointer;
160
160
  }
161
161
 
162
- .np-card-container:focus-visible :global(.np-ripple-surface)::before {
163
- opacity: var(--np-ripple-focus-opacity, 0.1);
164
- }
165
-
166
- .np-card-container:focus-visible {
167
- outline-style: solid;
168
- outline-color: var(--np-color-secondary);
169
- outline-width: 3px;
170
- outline-offset: 2px;
171
- }
172
- @media (prefers-reduced-motion: no-preference) {
173
- .np-card-container:focus-visible {
174
- animation: focusAnimation var(--np-motion-expressive-slow-effects) forwards;
175
- }
176
- }
177
162
  .np-card-disabled {
178
163
  opacity: 0.38;
179
164
  cursor: unset;
@@ -199,7 +184,6 @@
199
184
  flex: 1 0 auto;
200
185
  min-width: calc(100% - 32px);
201
186
  margin: 1rem;
202
- grid-gap: 0.5rem;
203
187
  gap: 0.5rem;
204
188
  }
205
189
 
@@ -1,7 +1,7 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { ElementKindProps } from '../internal/elementTag.js';
3
3
  export type CardElement = HTMLDivElement | HTMLButtonElement | HTMLAnchorElement;
4
- export interface CardProps extends ElementKindProps<CardElement> {
4
+ interface CardBaseProps extends ElementKindProps<CardElement> {
5
5
  variant?: 'elevated' | 'filled' | 'outlined';
6
6
  image?: string | null;
7
7
  /** Describes `image` for assistive technology. Leave it out for a decorative image. */
@@ -10,5 +10,15 @@ export interface CardProps extends ElementKindProps<CardElement> {
10
10
  headline?: string | null;
11
11
  subhead?: string | null;
12
12
  supportingText?: string | null;
13
- action?: Snippet<[]>;
14
13
  }
14
+ export type CardProps = CardBaseProps & ({
15
+ action?: undefined;
16
+ } | {
17
+ action?: Snippet<[]>;
18
+ type?: 'text' | null;
19
+ href?: never;
20
+ onclick?: never;
21
+ command?: never;
22
+ popovertarget?: never;
23
+ });
24
+ export {};
@@ -62,13 +62,47 @@
62
62
  : undefined,
63
63
  )
64
64
 
65
- let arrowHandler = $derived(arrowKeyNav('.np-carousel-item', axis, { wrap: false }))
65
+ const ITEMS = '.np-carousel-item:is(button, a[href]):not(.np-carousel-item-disabled)'
66
+ const TABBABLE =
67
+ 'a[href], button, input:not([type="hidden"]), select, textarea, summary, [tabindex], [contenteditable]'
68
+
69
+ let arrowHandler = $derived(arrowKeyNav(ITEMS, axis, { wrap: false }))
70
+
71
+ const tabbable = (node: HTMLElement) =>
72
+ node.tabIndex >= 0 &&
73
+ !node.matches(':disabled') &&
74
+ !node.closest('[inert]') &&
75
+ node.checkVisibility({ visibilityProperty: true })
76
+
77
+ const leave = (event: KeyboardEvent & { currentTarget: EventTarget & HTMLDivElement }) => {
78
+ const { key, target, currentTarget: carousel } = event
79
+ if (vertical || (key !== 'ArrowUp' && key !== 'ArrowDown')) return
80
+ if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return
81
+ if (!(target instanceof HTMLElement) || !target.matches(ITEMS)) return
82
+
83
+ // A modal dialog makes the rest of the page inert without an [inert] attribute.
84
+ const modal = carousel.closest('dialog')
85
+ const scope = modal?.matches(':modal') ? modal : document
86
+ const nodes = [...scope.querySelectorAll<HTMLElement>(TABBABLE)]
87
+ const after = (node: HTMLElement) =>
88
+ !!(carousel.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_FOLLOWING)
89
+ const candidates =
90
+ key === 'ArrowDown' ? nodes.filter(after) : nodes.filter((node) => !after(node)).reverse()
91
+ for (const node of candidates) {
92
+ if (carousel.contains(node) || node.contains(carousel) || !tabbable(node)) continue
93
+ node.focus()
94
+ if (document.activeElement !== node) continue
95
+ event.preventDefault()
96
+ return
97
+ }
98
+ }
66
99
 
67
100
  const handleKeydown = (
68
101
  event: KeyboardEvent & { currentTarget: EventTarget & HTMLDivElement },
69
102
  ) => {
70
103
  userKeydown?.(event)
71
104
  if (!event.defaultPrevented) arrowHandler(event)
105
+ if (!event.defaultPrevented) leave(event)
72
106
  }
73
107
 
74
108
  let places = $derived(new Map(items.map((item, index) => [item, index + 1])))
@@ -81,7 +115,10 @@
81
115
  const notch =
82
116
  place > 0 ? (snapPoints?.children[place - 1] as HTMLElement | undefined) : undefined
83
117
  if (notch) notch.scrollIntoView({ block: 'start', inline: 'start', behavior })
84
- else item.scrollIntoView({ block: 'nearest', inline: 'nearest', behavior })
118
+ else {
119
+ const inline = alignment === 'center' && !vertical ? 'center' : 'nearest'
120
+ item.scrollIntoView({ block: 'nearest', inline, behavior })
121
+ }
85
122
  }
86
123
 
87
124
  const carouselContext: CarouselContext = {
@@ -113,6 +150,7 @@
113
150
  `np-carousel-${variant}`,
114
151
  `np-carousel-${axis}`,
115
152
  snapping && 'np-carousel-snap',
153
+ alignment === 'center' && !morphing && 'np-carousel-center',
116
154
  cropping && 'np-carousel-cropping',
117
155
  attributes.class,
118
156
  ]}
@@ -302,29 +340,20 @@
302
340
 
303
341
  .np-carousel-cropping.np-carousel-horizontal .np-carousel-track :global(.np-carousel-item) {
304
342
  animation-timeline: view(inline), view(inline);
305
- clip-path: inset(
306
- 0 var(--_crop-end) 0 var(--_crop-start) round
307
- var(--np-carousel-item-container-shape, var(--np-shape-corner-extra-large))
308
- );
343
+ clip-path: inset(0 var(--_crop-end) 0 var(--_crop-start) round var(--_carousel-item-corner));
309
344
  --np-carousel-parallax: var(--_shift) 0;
310
345
  }
311
346
 
312
347
  .np-carousel-cropping.np-carousel-horizontal
313
348
  .np-carousel-track:dir(rtl)
314
349
  :global(.np-carousel-item) {
315
- clip-path: inset(
316
- 0 var(--_crop-start) 0 var(--_crop-end) round
317
- var(--np-carousel-item-container-shape, var(--np-shape-corner-extra-large))
318
- );
350
+ clip-path: inset(0 var(--_crop-start) 0 var(--_crop-end) round var(--_carousel-item-corner));
319
351
  --_shift: calc(var(--_visible, 0.3333) * (var(--_in) - var(--_out)) * 100%);
320
352
  }
321
353
 
322
354
  .np-carousel-cropping.np-carousel-vertical .np-carousel-track :global(.np-carousel-item) {
323
355
  animation-timeline: view(block), view(block);
324
- clip-path: inset(
325
- var(--_crop-start) 0 var(--_crop-end) 0 round
326
- var(--np-carousel-item-container-shape, var(--np-shape-corner-extra-large))
327
- );
356
+ clip-path: inset(var(--_crop-start) 0 var(--_crop-end) 0 round var(--_carousel-item-corner));
328
357
  --np-carousel-parallax: 0 var(--_shift);
329
358
  }
330
359
 
@@ -332,6 +361,20 @@
332
361
  inset-inline-start: var(--_crop-start);
333
362
  inset-inline-end: var(--_crop-end);
334
363
  }
364
+
365
+ .np-carousel-cropping.np-carousel-horizontal
366
+ .np-carousel-track
367
+ :global(.np-carousel-item::after) {
368
+ inset-inline-start: var(--_crop-start);
369
+ inset-inline-end: var(--_crop-end);
370
+ }
371
+
372
+ .np-carousel-cropping.np-carousel-vertical
373
+ .np-carousel-track
374
+ :global(.np-carousel-item::after) {
375
+ inset-block-start: var(--_crop-start);
376
+ inset-block-end: var(--_crop-end);
377
+ }
335
378
  }
336
379
 
337
380
  @supports (animation-timeline: scroll(nearest inline)) {
@@ -346,6 +389,14 @@
346
389
  :global(.np-carousel-morphing-block) .np-carousel-track :global(.np-carousel-item) {
347
390
  animation-timeline: scroll(nearest block);
348
391
  }
392
+
393
+ :global(.np-carousel-morphing-inline) .np-carousel-track :global(.np-carousel-item::after) {
394
+ inset-inline: var(--np-carousel-clip);
395
+ }
396
+
397
+ :global(.np-carousel-morphing-block) .np-carousel-track :global(.np-carousel-item::after) {
398
+ inset-block: var(--np-carousel-clip);
399
+ }
349
400
  }
350
401
 
351
402
  .np-carousel-snap-points > span {
@@ -374,6 +425,18 @@
374
425
  scroll-snap-align: start;
375
426
  }
376
427
 
428
+ .np-carousel-snap.np-carousel-center .np-carousel-track :global(.np-carousel-item) {
429
+ scroll-snap-align: center;
430
+ }
431
+
432
+ .np-carousel-center.np-carousel-horizontal .np-carousel-scroller {
433
+ scroll-padding-inline: var(--_pad);
434
+ }
435
+
436
+ .np-carousel-center.np-carousel-vertical .np-carousel-scroller {
437
+ scroll-padding-block: var(--_pad);
438
+ }
439
+
377
440
  :global(.np-carousel-morphing) .np-carousel-track :global(.np-carousel-item) {
378
441
  scroll-snap-align: none;
379
442
  }
@@ -16,7 +16,6 @@
16
16
  disabled = false,
17
17
  children,
18
18
  element = $bindable(),
19
- onpointerdown,
20
19
  style,
21
20
  ...attributes
22
21
  }: CarouselItemProps = $props()
@@ -39,20 +38,20 @@
39
38
  .join(';'),
40
39
  )
41
40
 
41
+ let text = $derived(attributes['aria-label'] ?? label ?? imageAlt)
42
42
  let accessibleName = $derived(
43
- attributes['aria-label'] ??
44
- (tag !== 'div' && label && position > 0
45
- ? carouselContext.itemLabel(label, position, total)
46
- : undefined),
43
+ tag !== 'div' && text && position > 0
44
+ ? carouselContext.itemLabel(text, position, total)
45
+ : (attributes['aria-label'] ?? undefined),
47
46
  )
48
47
 
49
48
  let labelHidden = $derived(accessibleName !== undefined)
50
49
 
51
- const handlePointerdown = (
52
- event: PointerEvent & { currentTarget: EventTarget & CarouselItemElement },
50
+ const handleClick = (
51
+ event: MouseEvent & { currentTarget: EventTarget & CarouselItemElement },
53
52
  ) => {
54
- onpointerdown?.(event)
55
- if (!disabled && tag !== 'div') press.press()
53
+ if (!disabled) press.press()
54
+ attributes.onclick?.(event)
56
55
  }
57
56
  </script>
58
57
 
@@ -100,7 +99,7 @@
100
99
  {disabled}
101
100
  aria-disabled={disabled}
102
101
  aria-label={accessibleName}
103
- onpointerdown={handlePointerdown}
102
+ onclick={handleClick}
104
103
  style={itemStyle}
105
104
  class={[
106
105
  'np-carousel-item',
@@ -120,7 +119,7 @@
120
119
  aria-disabled={disabled}
121
120
  aria-label={accessibleName}
122
121
  tabindex={disabled ? -1 : undefined}
123
- onpointerdown={handlePointerdown}
122
+ onclick={handleClick}
124
123
  style={itemStyle}
125
124
  class={[
126
125
  'np-carousel-item',
@@ -135,6 +134,12 @@
135
134
  {/if}
136
135
 
137
136
  <style>
137
+ @property --_carousel-item-corner {
138
+ syntax: '<length-percentage>';
139
+ inherits: false;
140
+ initial-value: 0px;
141
+ }
142
+
138
143
  .np-carousel-item {
139
144
  position: relative;
140
145
  box-sizing: border-box;
@@ -154,7 +159,11 @@
154
159
  inline-size: var(--np-carousel-item-width, 12.5rem);
155
160
  block-size: var(--np-carousel-item-height, auto);
156
161
  align-self: stretch;
157
- border-radius: var(--np-carousel-item-container-shape, var(--np-shape-corner-extra-large));
162
+ --_carousel-item-corner: var(
163
+ --np-carousel-item-container-shape,
164
+ var(--np-shape-corner-extra-large)
165
+ );
166
+ border-radius: var(--_carousel-item-corner);
158
167
  box-shadow: var(--np-carousel-item-elevation, none);
159
168
  background-color: var(--np-carousel-item-container-color, transparent);
160
169
  --np-ripple-hover-color: var(--np-carousel-item-state-layer-color, var(--np-color-on-surface));
@@ -271,11 +280,14 @@
271
280
 
272
281
  @media (prefers-reduced-motion: no-preference) {
273
282
  .np-carousel-item {
274
- transition: border-radius var(--np-motion-expressive-fast-effects);
283
+ transition: --_carousel-item-corner var(--np-motion-expressive-fast-effects);
275
284
  }
276
285
 
277
286
  .np-carousel-item:is(button, a):is(:active, .np-carousel-item-pressed) {
278
- border-radius: var(--np-carousel-item-pressed-container-shape, var(--np-shape-corner-medium));
287
+ --_carousel-item-corner: var(
288
+ --np-carousel-item-pressed-container-shape,
289
+ var(--np-shape-corner-large)
290
+ );
279
291
  }
280
292
  }
281
293
 
@@ -57,7 +57,7 @@ export const carouselMorph = (options) => (scroller) => {
57
57
  sheet.replaceSync('');
58
58
  return;
59
59
  }
60
- const shape = 'var(--np-carousel-item-container-shape, var(--np-shape-corner-extra-large))';
60
+ const shape = 'var(--_carousel-item-corner)';
61
61
  const offset = (along) => vertical ? `inset-block-start:${px(along)}` : `inset-inline-start:${px(along)}`;
62
62
  const mask = (clip) => vertical ? `inset(${px(clip)} 0 round ${shape})` : `inset(0 ${px(clip)} round ${shape})`;
63
63
  const at = (percent) => `${Math.round(percent * 1000) / 1000}%`;
@@ -103,7 +103,7 @@ const build = (input, sizes, counts) => {
103
103
  keylines.push({
104
104
  kind,
105
105
  size,
106
- loc: stride / 2 + (index - focal) * stride,
106
+ loc: stride / 2 + index * stride,
107
107
  locOffset: offset + size / 2,
108
108
  });
109
109
  offset += size;
@@ -120,7 +120,7 @@ const build = (input, sizes, counts) => {
120
120
  loc: keylines[keylines.length - 1].loc + stride,
121
121
  locOffset: availableSpace + spacing / 2,
122
122
  });
123
- const scrollMax = Math.max(0, (itemCount - order.length + focal) * stride);
123
+ const scrollMax = Math.max(0, (itemCount - order.length) * stride);
124
124
  return {
125
125
  smallCount: counts.small,
126
126
  mediumCount: counts.medium,
@@ -221,38 +221,39 @@ const withOrder = (arrangement, order) => {
221
221
  keylines: [keylines[0], ...shifted, keylines[keylines.length - 1]],
222
222
  };
223
223
  };
224
+ const movableSlots = (arrangement, towards) => {
225
+ const order = arrangement.keylines.slice(1, -1);
226
+ const first = order.findIndex((slot) => slot.kind === 'large');
227
+ if (first < 0)
228
+ return 0;
229
+ if (towards === 'start')
230
+ return first;
231
+ return order.length - 1 - order.findLastIndex((slot) => slot.kind === 'large');
232
+ };
224
233
  export const shiftSteps = (arrangement, towards) => {
225
234
  const states = [arrangement];
226
- if (arrangement.keylines.length === 0)
227
- return states;
228
- const order = arrangement.keylines.slice(1, -1);
229
- const movable = order.filter((slot) => slot.kind !== 'large').length;
230
- if (movable === 0 || !order.some((slot) => slot.kind === 'large'))
231
- return states;
232
- const working = [...order];
235
+ const movable = movableSlots(arrangement, towards);
236
+ const working = arrangement.keylines.slice(1, -1);
233
237
  for (let step = 0; step < movable; step += 1) {
234
238
  if (towards === 'end') {
235
239
  const [moved] = working.splice(working.length - 1, 1);
236
- working.splice(working.findIndex((slot) => slot.kind === 'large'), 0, moved);
240
+ const first = working.findIndex((slot) => slot.kind === 'large');
241
+ const larger = working.findIndex((slot, index) => index < first && slot.size > moved.size);
242
+ working.splice(larger < 0 ? first : larger, 0, moved);
237
243
  }
238
244
  else {
239
245
  const [moved] = working.splice(0, 1);
240
- working.splice(working.findLastIndex((slot) => slot.kind === 'large') + 1, 0, moved);
246
+ const last = working.findLastIndex((slot) => slot.kind === 'large');
247
+ const smaller = working.findIndex((slot, index) => index > last && slot.size < moved.size);
248
+ working.splice(smaller < 0 ? working.length : smaller, 0, moved);
241
249
  }
242
250
  states.push(withOrder(arrangement, working));
243
251
  }
244
252
  return states;
245
253
  };
246
- const shiftDistance = (arrangement) => {
247
- const moving = arrangement.keylines
248
- .slice(1, -1)
249
- .filter((keyline) => keyline.kind !== 'large').length;
250
- return moving * arrangement.stride;
251
- };
252
254
  export const morphTrack = (arrangement) => {
253
- const shift = shiftDistance(arrangement);
254
- const startShift = arrangement.focal > 1 ? shift : 0;
255
- const endShift = shift;
255
+ const startShift = movableSlots(arrangement, 'start') * arrangement.stride;
256
+ const endShift = movableSlots(arrangement, 'end') * arrangement.stride;
256
257
  const scrollMax = arrangement.scrollMax + startShift + endShift;
257
258
  return {
258
259
  arrangement,
@@ -290,14 +291,11 @@ export const placeItemOnTrack = (track, index, scroll) => {
290
291
  return { x: base.x + held, clip: base.clip };
291
292
  };
292
293
  export const notches = (track) => {
293
- const { arrangement, startShift, scrollMax } = track;
294
- const uniform = arrangement.keylines.length === 0 || arrangement.stride <= 0;
295
- const step = uniform ? Math.max(1, arrangement.stride) : arrangement.stride;
296
- const last = uniform ? scrollMax : arrangement.scrollMax;
297
- const base = uniform ? 0 : startShift;
294
+ const { arrangement, scrollMax } = track;
295
+ const step = Math.max(1, arrangement.stride);
298
296
  const out = [0];
299
- for (let at = step; at <= last + 0.001; at += step)
300
- out.push(base + at);
297
+ for (let at = step; at <= scrollMax + 0.001; at += step)
298
+ out.push(at);
301
299
  if (scrollMax > out[out.length - 1] + 0.001)
302
300
  out.push(scrollMax);
303
301
  return out;
@@ -54,7 +54,7 @@
54
54
  <rect class="mark short" />
55
55
  <rect class="mark long" />
56
56
  </svg>
57
- <div class="np-input-wrapper">
57
+ <div class="np-input-wrapper np-focus-ring-within">
58
58
  {#if !attributes.disabled}
59
59
  <Ripple forElement={inputEl} />
60
60
  {/if}
@@ -87,6 +87,7 @@
87
87
  cursor: pointer;
88
88
  -webkit-tap-highlight-color: transparent;
89
89
  margin: var(--np-checkbox-margin, max(0px, (48px - 18px)/2));
90
+ --np-ripple-pressed-color: var(--np-color-primary);
90
91
  }
91
92
  :global(label) {
92
93
  -webkit-tap-highlight-color: transparent;
@@ -114,13 +115,6 @@
114
115
  cursor: inherit;
115
116
  }
116
117
 
117
- .np-input-wrapper:has(input:focus-visible) {
118
- outline-style: solid;
119
- outline-color: var(--np-color-secondary);
120
- outline-width: 3px;
121
- outline-offset: 2px;
122
- animation: focusAnimation var(--np-motion-expressive-slow-effects) forwards;
123
- }
124
118
  .np-outline,
125
119
  .np-background,
126
120
  .np-icon {
@@ -188,6 +182,10 @@
188
182
  .np-error .np-icon {
189
183
  fill: var(--np-color-on-error);
190
184
  }
185
+ .np-checkbox-container:not(.np-error):has(input:checked, input:indeterminate) {
186
+ --np-ripple-hover-color: var(--np-color-primary);
187
+ --np-ripple-pressed-color: var(--np-color-on-surface);
188
+ }
191
189
  .np-error {
192
190
  --np-ripple-hover-color: var(--np-color-error);
193
191
  --np-ripple-pressed-color: var(--np-color-error);
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { arrowKeyNav, rovingTabindex } from '../keyboard-nav.js'
2
+ import { arrowKeyNav, focusableItems, focusedItem, rovingTabindex } from '../keyboard-nav.js'
3
3
  import type { ChipSetProps } from './types.js'
4
4
 
5
5
  let { children, element = $bindable(), ...attributes }: ChipSetProps = $props()
@@ -9,7 +9,33 @@
9
9
  currentAttr: 'aria-current',
10
10
  currentValue: 'true',
11
11
  })
12
- const onkeydown = arrowKeyNav(CHIP_SELECTOR, 'horizontal')
12
+ const arrows = arrowKeyNav(CHIP_SELECTOR, 'horizontal')
13
+
14
+ // In a set that wraps, ↑ and ↓ move to the chip closest above or below the focused one.
15
+ const toRow = (event: KeyboardEvent & { currentTarget: EventTarget & HTMLElement }) => {
16
+ const chips = focusableItems(event.currentTarget, CHIP_SELECTOR)
17
+ const focused = focusedItem(event.currentTarget, CHIP_SELECTOR, chips)
18
+ if (!focused) return
19
+ const from = focused.getBoundingClientRect()
20
+ const fromX = from.left + from.width / 2
21
+ const below = event.key === 'ArrowDown'
22
+ const candidates = chips
23
+ .map((chip) => ({ chip, box: chip.getBoundingClientRect() }))
24
+ .filter(({ box }) => (below ? box.top >= from.bottom : box.bottom <= from.top))
25
+ if (!candidates.length) return
26
+ const rowTop = below
27
+ ? Math.min(...candidates.map(({ box }) => box.top))
28
+ : Math.max(...candidates.map(({ box }) => box.top))
29
+ const row = candidates.filter(({ box }) => Math.abs(box.top - rowTop) < 1)
30
+ const distance = (box: DOMRect) => Math.abs(box.left + box.width / 2 - fromX)
31
+ row.sort((a, b) => distance(a.box) - distance(b.box))[0].chip.focus()
32
+ event.preventDefault()
33
+ }
34
+
35
+ const onkeydown = (event: KeyboardEvent & { currentTarget: EventTarget & HTMLElement }) => {
36
+ if (event.key === 'ArrowUp' || event.key === 'ArrowDown') toRow(event)
37
+ else arrows(event)
38
+ }
13
39
  </script>
14
40
 
15
41
  <div {...attributes} bind:this={element} class={['np-chip-set-wrapper', attributes.class]}>