noph-ui 0.52.0 → 0.53.1

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 (104) hide show
  1. package/dist/animation.d.ts +1 -13
  2. package/dist/animation.js +18 -13
  3. package/dist/button/Button.svelte +2 -14
  4. package/dist/button/IconButton.svelte +5 -18
  5. package/dist/button/SegmentedButton.svelte +8 -11
  6. package/dist/card/Card.svelte +3 -19
  7. package/dist/card/types.d.ts +12 -2
  8. package/dist/carousel/Carousel.svelte +77 -14
  9. package/dist/carousel/CarouselItem.svelte +26 -14
  10. package/dist/carousel/carouselMorph.js +1 -1
  11. package/dist/carousel/carouselStrategy.js +25 -27
  12. package/dist/checkbox/Checkbox.svelte +1 -8
  13. package/dist/chip/FilterChip.svelte +1 -13
  14. package/dist/date-picker/Calendar.svelte +31 -30
  15. package/dist/date-picker/Calendar.svelte.d.ts +2 -0
  16. package/dist/date-picker/CalendarHeader.svelte +84 -62
  17. package/dist/date-picker/CalendarHeader.svelte.d.ts +3 -0
  18. package/dist/date-picker/DatePickerDialog.svelte +134 -75
  19. package/dist/date-picker/DatePickerDialog.svelte.d.ts +4 -3
  20. package/dist/date-picker/DateRangePicker.svelte +226 -78
  21. package/dist/date-picker/DateRangePicker.svelte.d.ts +3 -3
  22. package/dist/date-picker/DockedDatePicker.svelte +76 -464
  23. package/dist/date-picker/DockedDatePicker.svelte.d.ts +2 -2
  24. package/dist/date-picker/DockedDateTimePicker.svelte +128 -525
  25. package/dist/date-picker/DockedDateTimePicker.svelte.d.ts +2 -2
  26. package/dist/date-picker/DockedPickerField.svelte +221 -0
  27. package/dist/date-picker/DockedPickerField.svelte.d.ts +9 -0
  28. package/dist/date-picker/MonthPanel.svelte +291 -0
  29. package/dist/date-picker/MonthPanel.svelte.d.ts +12 -0
  30. package/dist/date-picker/SelectionList.svelte +7 -6
  31. package/dist/date-picker/SelectionList.svelte.d.ts +1 -0
  32. package/dist/date-picker/TimeColumn.svelte +2 -6
  33. package/dist/date-picker/TimeColumn.svelte.d.ts +1 -0
  34. package/dist/date-picker/YearGrid.svelte +2 -11
  35. package/dist/date-picker/YearGrid.svelte.d.ts +1 -0
  36. package/dist/date-picker/dateUtils.d.ts +4 -0
  37. package/dist/date-picker/dateUtils.js +15 -88
  38. package/dist/date-picker/exitVisibility.svelte.js +13 -6
  39. package/dist/date-picker/formatters.d.ts +16 -0
  40. package/dist/date-picker/formatters.js +48 -15
  41. package/dist/date-picker/internalTypes.d.ts +59 -1
  42. package/dist/date-picker/shortcuts.d.ts +11 -0
  43. package/dist/date-picker/shortcuts.js +40 -0
  44. package/dist/date-picker/timeUtils.d.ts +8 -3
  45. package/dist/date-picker/timeUtils.js +33 -51
  46. package/dist/date-picker/types.d.ts +22 -0
  47. package/dist/icons/Icon.svelte +1 -4
  48. package/dist/internal/exit.css +0 -14
  49. package/dist/internal/field.css +337 -0
  50. package/dist/internal/focus-ring.css +23 -8
  51. package/dist/internal/pop-in.css +19 -0
  52. package/dist/internal/visually-hidden.css +11 -0
  53. package/dist/list/Item.svelte +32 -18
  54. package/dist/menu/Menu.svelte +6 -25
  55. package/dist/menu/Menu.svelte.d.ts +1 -0
  56. package/dist/navigation-bar/NavigationBarItem.svelte +6 -30
  57. package/dist/navigation-drawer/NavigationDrawerItem.svelte +4 -11
  58. package/dist/navigation-rail/NavigationRail.svelte +2 -3
  59. package/dist/navigation-rail/NavigationRailItem.svelte +2 -13
  60. package/dist/press.svelte.d.ts +0 -1
  61. package/dist/press.svelte.js +0 -11
  62. package/dist/progress/CircularProgress.svelte +26 -81
  63. package/dist/progress/CircularProgress.svelte.d.ts +1 -0
  64. package/dist/progress/LinearProgress.svelte +19 -151
  65. package/dist/progress/LinearProgress.svelte.d.ts +1 -0
  66. package/dist/progress/four-color.css +34 -0
  67. package/dist/radio/Radio.svelte +6 -9
  68. package/dist/ripple/Ripple.svelte +8 -16
  69. package/dist/search/Search.svelte +2 -9
  70. package/dist/search/Search.svelte.d.ts +1 -0
  71. package/dist/select/Check.svelte +8 -31
  72. package/dist/select/Check.svelte.d.ts +1 -1
  73. package/dist/select/NativeSelect.svelte +97 -366
  74. package/dist/select/NativeSelect.svelte.d.ts +1 -0
  75. package/dist/select/Option.svelte +1 -9
  76. package/dist/select/Select.svelte +139 -746
  77. package/dist/select/Select.svelte.d.ts +1 -0
  78. package/dist/select/VirtualList.svelte +0 -1
  79. package/dist/sheet/Sheet.svelte +11 -22
  80. package/dist/slider/Slider.svelte +13 -37
  81. package/dist/slider/Slider.svelte.d.ts +1 -0
  82. package/dist/switch/Switch.svelte +2 -8
  83. package/dist/tabs/Tab.svelte +2 -4
  84. package/dist/text-field/TextField.svelte +81 -609
  85. package/dist/text-field/TextField.svelte.d.ts +1 -0
  86. package/dist/time-picker/ClockDial.svelte +8 -25
  87. package/dist/time-picker/ClockDial.svelte.d.ts +2 -0
  88. package/dist/time-picker/DockedTimePicker.svelte +102 -244
  89. package/dist/time-picker/DockedTimePicker.svelte.d.ts +2 -2
  90. package/dist/time-picker/PeriodSelector.svelte +8 -7
  91. package/dist/time-picker/PeriodSelector.svelte.d.ts +1 -0
  92. package/dist/time-picker/TimeInputs.svelte +13 -2
  93. package/dist/time-picker/TimePickerDialog.svelte +32 -26
  94. package/dist/time-picker/TimePickerDialog.svelte.d.ts +2 -2
  95. package/dist/time-picker/TimePickerPanel.svelte +26 -4
  96. package/dist/time-picker/TimeSelectors.svelte +4 -9
  97. package/dist/time-picker/TimeSelectors.svelte.d.ts +1 -0
  98. package/dist/time-picker/internalTypes.d.ts +2 -0
  99. package/dist/time-picker/timePickerState.svelte.js +3 -9
  100. package/dist/tooltip/RichTooltip.svelte +2 -23
  101. package/dist/tooltip/RichTooltip.svelte.d.ts +1 -0
  102. package/dist/tooltip/Tooltip.svelte +2 -24
  103. package/dist/tooltip/Tooltip.svelte.d.ts +1 -0
  104. package/package.json +12 -12
@@ -1,10 +1,5 @@
1
1
  import type { Attachment } from 'svelte/attachments';
2
2
  export declare const afterTwoFrames: (fn: () => void) => () => void;
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
3
  export declare const afterExit: (element: Element | undefined, fn: () => void, { subtree }?: {
9
4
  subtree?: boolean;
10
5
  }) => () => void;
@@ -12,16 +7,9 @@ export declare const parseMotion: (token: string) => {
12
7
  duration: number;
13
8
  easing: string;
14
9
  };
15
- /** Reads a motion token such as `--np-motion-standard-fast-spatial` off an element's styles. */
16
10
  export declare const motionOf: (element: Element, token: string) => {
17
11
  duration: number;
18
12
  easing: string;
19
13
  };
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
14
  export declare const exitAnimation: Attachment<HTMLElement>;
15
+ export declare const morphSize: (element: HTMLElement | undefined, apply: () => void, token: string) => void;
package/dist/animation.js CHANGED
@@ -8,11 +8,6 @@ export const afterTwoFrames = (fn) => {
8
8
  cancelAnimationFrame(inner);
9
9
  };
10
10
  };
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
11
  export const afterExit = (element, fn, { subtree = false } = {}) => {
17
12
  if (!element) {
18
13
  fn();
@@ -47,15 +42,7 @@ export const parseMotion = (token) => {
47
42
  easing: easing || 'linear',
48
43
  };
49
44
  };
50
- /** Reads a motion token such as `--np-motion-standard-fast-spatial` off an element's styles. */
51
45
  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
46
  export const exitAnimation = (element) => {
60
47
  if (CSS.supports('overlay: auto'))
61
48
  return;
@@ -79,3 +66,21 @@ export const exitAnimation = (element) => {
79
66
  done();
80
67
  };
81
68
  };
69
+ export const morphSize = (element, apply, token) => {
70
+ if (!element || matchMedia('(prefers-reduced-motion: reduce)').matches) {
71
+ apply();
72
+ return;
73
+ }
74
+ const from = element.getBoundingClientRect();
75
+ element.getAnimations().forEach((animation) => animation.cancel());
76
+ apply();
77
+ const to = element.getBoundingClientRect();
78
+ const frame = (rect) => ({
79
+ ...(from.width !== to.width && { width: `${rect.width}px` }),
80
+ ...(from.height !== to.height && { height: `${rect.height}px` }),
81
+ });
82
+ if (from.width === to.width && from.height === to.height)
83
+ return;
84
+ element.style.overflow = 'hidden';
85
+ element.animate([frame(from), frame(to)], motionOf(element, token)).finished.then(() => element.style.removeProperty('overflow'), () => { });
86
+ };
@@ -99,6 +99,7 @@
99
99
  bind:this={element}
100
100
  class={[
101
101
  'np-button',
102
+ 'np-focus-ring',
102
103
  size,
103
104
  shapeClass,
104
105
  isToggle && 'toggle',
@@ -134,6 +135,7 @@
134
135
  }}
135
136
  class={[
136
137
  'np-button',
138
+ 'np-focus-ring',
137
139
  size,
138
140
  shapeClass,
139
141
  isToggle && 'toggle',
@@ -294,15 +296,6 @@
294
296
  .outlined-disabled {
295
297
  --_outlined-border-color: color-mix(in srgb, var(--np-color-on-surface) 12%, transparent);
296
298
  }
297
- .enabled:focus-visible {
298
- outline-style: solid;
299
- outline-color: var(--np-color-secondary);
300
- outline-width: 3px;
301
- outline-offset: 2px;
302
- }
303
- .enabled:focus-visible :global(.np-ripple-surface)::before {
304
- opacity: var(--np-ripple-focus-opacity, 0.1);
305
- }
306
299
  /* Extra small and small buttons keep a 48px tall target around them. */
307
300
  .np-touch {
308
301
  position: absolute;
@@ -311,11 +304,6 @@
311
304
  translate: 0 -50%;
312
305
  height: max(3rem, 100%);
313
306
  }
314
- @media (prefers-reduced-motion: no-preference) {
315
- .enabled:focus-visible {
316
- animation: focusAnimation var(--np-motion-expressive-slow-effects) forwards;
317
- }
318
- }
319
307
  .text {
320
308
  color: var(--np-text-button-label-text-color, var(--np-color-primary));
321
309
  }
@@ -89,6 +89,7 @@
89
89
  bind:this={element}
90
90
  class={[
91
91
  'np-icon-button',
92
+ 'np-focus-ring',
92
93
  size,
93
94
  width,
94
95
  variant,
@@ -125,6 +126,7 @@
125
126
  }}
126
127
  class={[
127
128
  'np-icon-button',
129
+ 'np-focus-ring',
128
130
  size,
129
131
  width,
130
132
  shapeClass,
@@ -294,20 +296,6 @@
294
296
  outline-width: 1px;
295
297
  outline-offset: -1px;
296
298
  }
297
- .enabled:focus-visible {
298
- outline-style: solid;
299
- outline-color: var(--np-color-secondary);
300
- outline-width: 3px;
301
- outline-offset: 2px;
302
- }
303
- .enabled:focus-visible :global(.np-ripple-surface)::before {
304
- opacity: var(--np-ripple-focus-opacity, 0.1);
305
- }
306
- @media (prefers-reduced-motion: no-preference) {
307
- .enabled:focus-visible {
308
- animation: focusAnimation var(--np-motion-expressive-slow-effects) forwards;
309
- }
310
- }
311
299
  .text {
312
300
  color: var(--np-icon-button-icon-color, var(--np-color-on-surface-variant));
313
301
  }
@@ -363,8 +351,7 @@
363
351
  color: var(--np-color-on-surface-variant);
364
352
  }
365
353
 
366
- .outlined:not(.selected)::after,
367
- .outlined-disabled::after {
354
+ .outlined:not(.selected)::after {
368
355
  content: '';
369
356
  position: absolute;
370
357
  inset: 0;
@@ -379,8 +366,8 @@
379
366
  }
380
367
  .np-touch {
381
368
  position: absolute;
382
- height: max(calc(var(--_button-height, 40px) + 8px), 100%);
383
- width: max(calc(var(--_button-width, 40px) + 8px), 100%);
369
+ height: max(3rem, 100%);
370
+ width: max(3rem, 100%);
384
371
  }
385
372
 
386
373
  .np-icon-button-circular-progress {
@@ -45,7 +45,14 @@
45
45
  {#each options as option, i (i)}
46
46
  {let optionLabel = $derived(option.label ?? `${name}-${i}`)}
47
47
  {let optionValue = $derived(option.value ?? optionLabel)}
48
- <label class={['np-segmented-button', option.icon && 'width-icon']}>
48
+ <label
49
+ class={[
50
+ 'np-segmented-button',
51
+ 'np-focus-ring-within',
52
+ 'np-focus-ring-inset',
53
+ option.icon && 'width-icon',
54
+ ]}
55
+ >
49
56
  <div class="check-icon-wrapper">
50
57
  <div class="check-icon">
51
58
  <CheckIcon />
@@ -257,13 +264,6 @@
257
264
  opacity: var(--np-ripple-focus-opacity, 0.1);
258
265
  }
259
266
 
260
- .np-segmented-button:has(input:focus-visible) {
261
- outline-style: solid;
262
- outline-color: var(--np-color-secondary);
263
- outline-width: 3px;
264
- outline-offset: -3px;
265
- }
266
-
267
267
  @media (prefers-reduced-motion: no-preference) {
268
268
  .np-segmented-button {
269
269
  transition: all var(--np-motion-expressive-fast-effects);
@@ -282,8 +282,5 @@
282
282
  width var(--np-motion-expressive-fast-effects),
283
283
  opacity var(--np-motion-expressive-fast-effects);
284
284
  }
285
- .np-segmented-button:has(input:focus-visible) {
286
- animation: focusAnimationInset var(--np-motion-expressive-slow-effects) forwards;
287
- }
288
285
  }
289
286
  </style>
@@ -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;