noph-ui 0.47.0 → 0.48.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.
@@ -0,0 +1,5 @@
1
+ import '../internal/focus-ring.css';
2
+ import type { CarouselItemProps } from './types.ts';
3
+ declare const CarouselItem: import("svelte").Component<CarouselItemProps, {}, "element">;
4
+ type CarouselItem = ReturnType<typeof CarouselItem>;
5
+ export default CarouselItem;
@@ -0,0 +1,13 @@
1
+ import type { Attachment } from 'svelte/attachments';
2
+ import type { CarouselAlignment, CarouselItemElement } from './types.ts';
3
+ export interface CarouselMorphOptions {
4
+ variant: 'multi-browse' | 'hero';
5
+ alignment: CarouselAlignment;
6
+ vertical: boolean;
7
+ items: CarouselItemElement[];
8
+ track: HTMLElement | undefined;
9
+ metrics: HTMLElement | undefined;
10
+ uid: string;
11
+ }
12
+ export declare const scrollTimelines: boolean;
13
+ export declare const carouselMorph: (options: CarouselMorphOptions) => Attachment<HTMLElement>;
@@ -0,0 +1,100 @@
1
+ import { arrange, itemKeyframes, morphTrack, notches } from "./carouselStrategy.js";
2
+ export const scrollTimelines = typeof CSS !== 'undefined' && CSS.supports('animation-timeline', 'scroll(nearest inline)');
3
+ const px = (value) => `${Math.round(value * 100) / 100}px`;
4
+ const lengthOf = (value, fallback) => {
5
+ const parsed = Number.parseFloat(value ?? '');
6
+ return Number.isFinite(parsed) ? parsed : fallback;
7
+ };
8
+ export const carouselMorph = (options) => (scroller) => {
9
+ const { items, vertical, track, metrics, uid } = options;
10
+ if (!track || items.length === 0)
11
+ return;
12
+ const name = `np-carousel-morph-${uid}`;
13
+ const sheet = new CSSStyleSheet();
14
+ document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
15
+ const markers = scroller.querySelector('.np-carousel-snap-points')?.children ?? [];
16
+ const itemProbe = metrics?.firstElementChild;
17
+ const smallProbe = metrics?.lastElementChild;
18
+ for (const [index, item] of items.entries()) {
19
+ item.style.setProperty('--_morph', `${name}-${index}`);
20
+ }
21
+ let built;
22
+ const measure = () => {
23
+ const style = getComputedStyle(track);
24
+ const spacing = lengthOf(vertical ? style.rowGap : style.columnGap, 0);
25
+ const lead = lengthOf(vertical ? style.paddingBlockStart : style.paddingInlineStart, 0);
26
+ const trail = lengthOf(vertical ? style.paddingBlockEnd : style.paddingInlineEnd, 0);
27
+ const port = vertical ? scroller.clientHeight : scroller.clientWidth;
28
+ const itemSize = itemProbe ? (vertical ? itemProbe.offsetHeight : itemProbe.offsetWidth) : 0;
29
+ const smallStyle = smallProbe && getComputedStyle(smallProbe);
30
+ const smallMin = lengthOf(vertical ? smallStyle?.minBlockSize : smallStyle?.minInlineSize, 0);
31
+ const smallMax = lengthOf(vertical ? smallStyle?.maxBlockSize : smallStyle?.maxInlineSize, Number.POSITIVE_INFINITY);
32
+ const signature = [port, lead, trail, spacing, itemSize, smallMin, smallMax].join(' ');
33
+ if (signature === built)
34
+ return;
35
+ built = signature;
36
+ const arrangement = arrange({
37
+ variant: options.variant,
38
+ alignment: options.alignment,
39
+ itemCount: items.length,
40
+ availableSpace: Math.max(0, port - lead - trail),
41
+ itemSize,
42
+ spacing,
43
+ smallMin,
44
+ smallMax,
45
+ });
46
+ const morph = morphTrack(arrangement);
47
+ scroller.style.setProperty('--_item', px(arrangement.itemSize));
48
+ scroller.style.setProperty('--_track', px(morph.trackSize));
49
+ const snapAt = notches(morph);
50
+ for (let index = 0; index < markers.length; index += 1) {
51
+ const marker = markers[index];
52
+ const offset = snapAt[index];
53
+ marker.style.setProperty('--_at', offset === undefined ? '' : px(offset));
54
+ marker.toggleAttribute('hidden', offset === undefined);
55
+ }
56
+ if (arrangement.keylines.length === 0 || arrangement.scrollMax <= 0) {
57
+ sheet.replaceSync('');
58
+ return;
59
+ }
60
+ const shape = 'var(--np-carousel-item-container-shape, var(--np-shape-corner-extra-large))';
61
+ const offset = (along) => vertical ? `inset-block-start:${px(along)}` : `inset-inline-start:${px(along)}`;
62
+ const mask = (clip) => vertical ? `inset(${px(clip)} 0 round ${shape})` : `inset(0 ${px(clip)} round ${shape})`;
63
+ const at = (percent) => `${Math.round(percent * 1000) / 1000}%`;
64
+ const blocks = [];
65
+ for (const index of items.keys()) {
66
+ const stops = itemKeyframes(morph, index)
67
+ .map(({ percent, x, clip }) => `${at(percent)}{${offset(x)};clip-path:${mask(clip)};--np-carousel-clip:${px(clip)}}`)
68
+ .join('');
69
+ blocks.push(`@keyframes ${name}-${index}{${stops}}`);
70
+ }
71
+ sheet.replaceSync(blocks.join(''));
72
+ };
73
+ const axisClass = vertical ? 'np-carousel-morphing-block' : 'np-carousel-morphing-inline';
74
+ scroller.classList.add('np-carousel-morphing', axisClass);
75
+ measure();
76
+ let frame = 0;
77
+ const resize = new ResizeObserver(() => {
78
+ if (frame)
79
+ return;
80
+ frame = requestAnimationFrame(() => {
81
+ frame = 0;
82
+ measure();
83
+ });
84
+ });
85
+ resize.observe(scroller);
86
+ if (itemProbe)
87
+ resize.observe(itemProbe);
88
+ return () => {
89
+ if (frame)
90
+ cancelAnimationFrame(frame);
91
+ resize.disconnect();
92
+ document.adoptedStyleSheets = document.adoptedStyleSheets.filter((one) => one !== sheet);
93
+ scroller.classList.remove('np-carousel-morphing', axisClass);
94
+ for (const property of ['--_item', '--_track']) {
95
+ scroller.style.removeProperty(property);
96
+ }
97
+ for (const item of items)
98
+ item.style.removeProperty('--_morph');
99
+ };
100
+ };
@@ -0,0 +1,61 @@
1
+ import type { CarouselAlignment, CarouselVariant } from './types.ts';
2
+ export type CarouselKeylineKind = 'collapsed' | 'small' | 'medium' | 'large';
3
+ export interface CarouselKeyline {
4
+ kind: CarouselKeylineKind;
5
+ size: number;
6
+ loc: number;
7
+ locOffset: number;
8
+ }
9
+ export interface CarouselKeyframe {
10
+ percent: number;
11
+ x: number;
12
+ clip: number;
13
+ }
14
+ export interface CarouselStrategyInput {
15
+ variant: CarouselVariant;
16
+ alignment: CarouselAlignment;
17
+ itemCount: number;
18
+ availableSpace: number;
19
+ itemSize: number;
20
+ spacing: number;
21
+ smallMin: number;
22
+ smallMax: number;
23
+ }
24
+ export interface CarouselArrangement {
25
+ smallCount: number;
26
+ mediumCount: number;
27
+ largeCount: number;
28
+ smallSize: number;
29
+ mediumSize: number;
30
+ largeSize: number;
31
+ stride: number;
32
+ itemSize: number;
33
+ spacing: number;
34
+ focal: number;
35
+ keylines: CarouselKeyline[];
36
+ scrollMax: number;
37
+ trackSize: number;
38
+ }
39
+ export declare const arrange: (input: CarouselStrategyInput) => CarouselArrangement;
40
+ export declare const placeItem: (arrangement: CarouselArrangement, index: number, scroll: number) => {
41
+ x: number;
42
+ clip: number;
43
+ };
44
+ export declare const shiftSteps: (arrangement: CarouselArrangement, towards: "start" | "end") => CarouselArrangement[];
45
+ export interface CarouselTrack {
46
+ arrangement: CarouselArrangement;
47
+ atStart: CarouselArrangement[];
48
+ atEnd: CarouselArrangement[];
49
+ startShift: number;
50
+ endShift: number;
51
+ scrollMax: number;
52
+ trackSize: number;
53
+ }
54
+ export declare const morphTrack: (arrangement: CarouselArrangement) => CarouselTrack;
55
+ export declare const placeItemOnTrack: (track: CarouselTrack, index: number, scroll: number) => {
56
+ x: number;
57
+ clip: number;
58
+ };
59
+ export declare const notches: (track: CarouselTrack) => number[];
60
+ export declare const itemKeyframes: (track: CarouselTrack, index: number, subdivisions?: number) => CarouselKeyframe[];
61
+ export declare const minVisibleFraction: (ratio: number | null | undefined) => number;
@@ -0,0 +1,344 @@
1
+ const clamp = (value, low, high) => Math.min(high, Math.max(low, value));
2
+ const slotTargets = (input) => {
3
+ const { availableSpace, itemSize, spacing, smallMin, smallMax, variant } = input;
4
+ const large = Math.min((variant === 'hero' ? itemSize * 2 : itemSize) + spacing, availableSpace);
5
+ const smallLow = smallMin + spacing;
6
+ const smallHigh = smallMax + spacing;
7
+ const small = clamp((variant === 'hero' ? large : itemSize + spacing) / 3, smallLow, smallHigh);
8
+ return { small, medium: (large + small) / 2, large, smallLow, smallHigh };
9
+ };
10
+ const descending = (from, to) => {
11
+ const out = [];
12
+ for (let n = to; n >= from; n -= 1)
13
+ out.push(n);
14
+ return out;
15
+ };
16
+ const candidateCounts = (input, targets) => {
17
+ const { variant, alignment, availableSpace, spacing, smallMin } = input;
18
+ const centred = alignment === 'center';
19
+ if (variant === 'hero') {
20
+ const noRoom = availableSpace < (smallMin + spacing) * 2;
21
+ return {
22
+ smalls: noRoom ? [0] : [centred ? 2 : 1],
23
+ mediums: centred ? [0, 2] : [0, 1],
24
+ larges: descending(1, Math.max(1, Math.floor(availableSpace / targets.large))),
25
+ };
26
+ }
27
+ const flank = centred ? 2 : 1;
28
+ const smalls = [flank];
29
+ const mediums = [flank, 0];
30
+ const leastLargeSpace = availableSpace - (targets.medium + targets.small) * flank;
31
+ const budget = Math.max(1, input.itemCount - 2 * flank);
32
+ const low = Math.max(1, Math.min(Math.floor(leastLargeSpace / targets.large), budget));
33
+ const high = Math.max(low, Math.min(Math.ceil(availableSpace / targets.large), budget));
34
+ return { smalls, mediums, larges: descending(low, high) };
35
+ };
36
+ const fit = (availableSpace, targets, counts) => {
37
+ const share = counts.small + counts.medium / 2;
38
+ const divisor = counts.large + counts.medium / 2;
39
+ if (divisor <= 0)
40
+ return undefined;
41
+ const solve = (small) => (availableSpace - small * share) / divisor;
42
+ let small = clamp(targets.small, targets.smallLow, targets.smallHigh);
43
+ let large = solve(small);
44
+ if (large > targets.large && share > 0) {
45
+ small = clamp((availableSpace - targets.large * divisor) / share, targets.smallLow, targets.smallHigh);
46
+ large = solve(small);
47
+ }
48
+ if (large < small)
49
+ return undefined;
50
+ const medium = (large + small) / 2;
51
+ const cost = Math.abs(large - targets.large) * counts.large +
52
+ Math.abs(medium - targets.medium) * counts.medium +
53
+ Math.abs(small - targets.small) * counts.small;
54
+ return { small, medium, large, cost };
55
+ };
56
+ const uniform = (input) => {
57
+ const { availableSpace, itemSize, spacing, itemCount } = input;
58
+ const count = Math.max(0, itemCount);
59
+ const large = Math.max(spacing, Math.min(itemSize + spacing, availableSpace) || availableSpace);
60
+ const scrollMax = Math.max(0, count * large - availableSpace);
61
+ return {
62
+ smallCount: 0,
63
+ mediumCount: 0,
64
+ largeCount: count,
65
+ smallSize: large,
66
+ mediumSize: large,
67
+ largeSize: large,
68
+ stride: large,
69
+ itemSize: Math.max(0, large - spacing),
70
+ spacing,
71
+ focal: 0,
72
+ keylines: [],
73
+ scrollMax,
74
+ trackSize: availableSpace + scrollMax,
75
+ };
76
+ };
77
+ const build = (input, sizes, counts) => {
78
+ const { alignment, spacing, availableSpace, itemCount } = input;
79
+ const order = [];
80
+ const push = (kind, count) => {
81
+ for (let i = 0; i < count; i += 1)
82
+ order.push(kind);
83
+ };
84
+ if (alignment === 'center') {
85
+ push('small', counts.small / 2);
86
+ push('medium', counts.medium / 2);
87
+ push('large', counts.large);
88
+ push('medium', counts.medium / 2);
89
+ push('small', counts.small / 2);
90
+ }
91
+ else {
92
+ push('large', counts.large);
93
+ push('medium', counts.medium);
94
+ push('small', counts.small);
95
+ }
96
+ const focal = order.indexOf('large');
97
+ const stride = sizes.large;
98
+ const sizeOf = (kind) => kind === 'large' ? sizes.large : kind === 'medium' ? sizes.medium : sizes.small;
99
+ const keylines = [];
100
+ let offset = 0;
101
+ for (const [index, kind] of order.entries()) {
102
+ const size = sizeOf(kind);
103
+ keylines.push({
104
+ kind,
105
+ size,
106
+ loc: stride / 2 + (index - focal) * stride,
107
+ locOffset: offset + size / 2,
108
+ });
109
+ offset += size;
110
+ }
111
+ keylines.unshift({
112
+ kind: 'collapsed',
113
+ size: spacing,
114
+ loc: keylines[0].loc - stride,
115
+ locOffset: -spacing / 2,
116
+ });
117
+ keylines.push({
118
+ kind: 'collapsed',
119
+ size: spacing,
120
+ loc: keylines[keylines.length - 1].loc + stride,
121
+ locOffset: availableSpace + spacing / 2,
122
+ });
123
+ const scrollMax = Math.max(0, (itemCount - order.length + focal) * stride);
124
+ return {
125
+ smallCount: counts.small,
126
+ mediumCount: counts.medium,
127
+ largeCount: counts.large,
128
+ smallSize: sizes.small,
129
+ mediumSize: sizes.medium,
130
+ largeSize: sizes.large,
131
+ stride,
132
+ itemSize: Math.max(0, sizes.large - spacing),
133
+ spacing,
134
+ focal: focal + 1,
135
+ keylines,
136
+ scrollMax,
137
+ trackSize: availableSpace + scrollMax,
138
+ };
139
+ };
140
+ export const arrange = (input) => {
141
+ if (input.variant === 'uncontained' || input.variant === 'full-screen')
142
+ return uniform(input);
143
+ if (input.availableSpace <= 0 || input.itemCount <= 0)
144
+ return uniform(input);
145
+ if (input.itemCount * (input.itemSize + input.spacing) - input.spacing <= input.availableSpace) {
146
+ return uniform(input);
147
+ }
148
+ const targets = slotTargets(input);
149
+ if (!(targets.large > 0))
150
+ return uniform(input);
151
+ const { smalls, mediums, larges } = candidateCounts(input, targets);
152
+ let best;
153
+ for (const large of larges) {
154
+ for (const medium of mediums) {
155
+ for (const small of smalls) {
156
+ const counts = { small, medium, large };
157
+ if (small + medium + large > input.itemCount)
158
+ continue;
159
+ const sizes = fit(input.availableSpace, targets, counts);
160
+ if (!sizes)
161
+ continue;
162
+ if (best && sizes.cost >= best.sizes.cost)
163
+ continue;
164
+ best = { sizes, counts };
165
+ }
166
+ }
167
+ }
168
+ if (!best && input.alignment === 'center')
169
+ return arrange({ ...input, alignment: 'start' });
170
+ if (!best)
171
+ return uniform(input);
172
+ if (best.counts.small === 0 && best.counts.medium === 0 && input.itemCount > best.counts.large) {
173
+ const counts = { ...best.counts, small: 1 };
174
+ const sizes = fit(input.availableSpace, targets, counts);
175
+ if (sizes)
176
+ best = { sizes, counts };
177
+ }
178
+ const arrangement = build(input, best.sizes, best.counts);
179
+ return arrangement.scrollMax > 0 ? arrangement : uniform(input);
180
+ };
181
+ export const placeItem = (arrangement, index, scroll) => {
182
+ const { keylines, stride, spacing, itemSize } = arrangement;
183
+ if (keylines.length === 0)
184
+ return { x: 0, clip: 0 };
185
+ const unmorphed = index * stride + stride / 2 - scroll;
186
+ const first = keylines[0];
187
+ const last = keylines[keylines.length - 1];
188
+ const atAnchor = (keyline) => ({
189
+ x: keyline.locOffset - keyline.loc,
190
+ clip: Math.max(0, (itemSize - (keyline.size - spacing)) / 2),
191
+ });
192
+ if (unmorphed <= first.loc)
193
+ return atAnchor(first);
194
+ if (unmorphed >= last.loc)
195
+ return atAnchor(last);
196
+ const slot = clamp(Math.ceil((unmorphed - first.loc) / stride), 1, keylines.length - 1);
197
+ const low = keylines[slot - 1];
198
+ const high = keylines[slot];
199
+ const ratio = (unmorphed - low.loc) / stride;
200
+ const size = low.size + (high.size - low.size) * ratio;
201
+ const centre = low.locOffset + (high.locOffset - low.locOffset) * ratio;
202
+ return { x: centre - unmorphed, clip: Math.max(0, (itemSize - (size - spacing)) / 2) };
203
+ };
204
+ const withOrder = (arrangement, order) => {
205
+ const { keylines } = arrangement;
206
+ const slots = keylines.slice(1, -1);
207
+ const shifted = [];
208
+ let offset = 0;
209
+ for (const [index, slot] of order.entries()) {
210
+ shifted.push({
211
+ kind: slot.kind,
212
+ size: slot.size,
213
+ loc: slots[index].loc,
214
+ locOffset: offset + slot.size / 2,
215
+ });
216
+ offset += slot.size;
217
+ }
218
+ return {
219
+ ...arrangement,
220
+ focal: order.findIndex((slot) => slot.kind === 'large') + 1,
221
+ keylines: [keylines[0], ...shifted, keylines[keylines.length - 1]],
222
+ };
223
+ };
224
+ export const shiftSteps = (arrangement, towards) => {
225
+ 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];
233
+ for (let step = 0; step < movable; step += 1) {
234
+ if (towards === 'end') {
235
+ const [moved] = working.splice(working.length - 1, 1);
236
+ working.splice(working.findIndex((slot) => slot.kind === 'large'), 0, moved);
237
+ }
238
+ else {
239
+ const [moved] = working.splice(0, 1);
240
+ working.splice(working.findLastIndex((slot) => slot.kind === 'large') + 1, 0, moved);
241
+ }
242
+ states.push(withOrder(arrangement, working));
243
+ }
244
+ return states;
245
+ };
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
+ export const morphTrack = (arrangement) => {
253
+ const shift = shiftDistance(arrangement);
254
+ const startShift = arrangement.focal > 1 ? shift : 0;
255
+ const endShift = shift;
256
+ const scrollMax = arrangement.scrollMax + startShift + endShift;
257
+ return {
258
+ arrangement,
259
+ atStart: startShift > 0 ? shiftSteps(arrangement, 'start') : [arrangement],
260
+ atEnd: shiftSteps(arrangement, 'end'),
261
+ startShift,
262
+ endShift,
263
+ scrollMax,
264
+ trackSize: arrangement.trackSize - arrangement.scrollMax + scrollMax,
265
+ };
266
+ };
267
+ const mix = (from, to, ratio) => from + (to - from) * ratio;
268
+ export const placeItemOnTrack = (track, index, scroll) => {
269
+ const { arrangement, atStart, atEnd, startShift, endShift } = track;
270
+ if (arrangement.keylines.length === 0)
271
+ return placeItem(arrangement, index, scroll);
272
+ const traversal = clamp(scroll - startShift, 0, arrangement.scrollMax);
273
+ const held = scroll - traversal;
274
+ const along = (steps, ratio) => {
275
+ const at = clamp(ratio, 0, 1) * (steps.length - 1);
276
+ const step = Math.min(Math.floor(at), steps.length - 2);
277
+ const low = placeItem(steps[step], index, traversal);
278
+ const high = placeItem(steps[step + 1], index, traversal);
279
+ const frac = at - step;
280
+ return { x: mix(low.x, high.x, frac) + held, clip: mix(low.clip, high.clip, frac) };
281
+ };
282
+ if (startShift > 0 && scroll < startShift && atStart.length > 1) {
283
+ return along(atStart, 1 - scroll / startShift);
284
+ }
285
+ const endFrom = startShift + arrangement.scrollMax;
286
+ if (endShift > 0 && scroll > endFrom && atEnd.length > 1) {
287
+ return along(atEnd, (scroll - endFrom) / endShift);
288
+ }
289
+ const base = placeItem(arrangement, index, traversal);
290
+ return { x: base.x + held, clip: base.clip };
291
+ };
292
+ 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;
298
+ const out = [0];
299
+ for (let at = step; at <= last + 0.001; at += step)
300
+ out.push(base + at);
301
+ if (scrollMax > out[out.length - 1] + 0.001)
302
+ out.push(scrollMax);
303
+ return out;
304
+ };
305
+ export const itemKeyframes = (track, index, subdivisions = 6) => {
306
+ const { arrangement, startShift, endShift, scrollMax } = track;
307
+ const { keylines, stride } = arrangement;
308
+ if (keylines.length === 0 || scrollMax <= 0)
309
+ return [];
310
+ const offsets = new Set([0, scrollMax, startShift, startShift + arrangement.scrollMax]);
311
+ for (const keyline of keylines) {
312
+ const at = startShift + index * stride + stride / 2 - keyline.loc;
313
+ if (at > 0 && at < scrollMax)
314
+ offsets.add(at);
315
+ }
316
+ const walk = (from, span, steps) => {
317
+ if (span <= 0 || steps < 1)
318
+ return;
319
+ const total = steps * subdivisions;
320
+ for (let part = 0; part <= total; part += 1)
321
+ offsets.add(from + (span * part) / total);
322
+ };
323
+ walk(0, startShift, track.atStart.length - 1);
324
+ walk(startShift + arrangement.scrollMax, endShift, track.atEnd.length - 1);
325
+ return [...offsets]
326
+ .sort((a, b) => a - b)
327
+ .map((offset) => ({
328
+ percent: (offset / scrollMax) * 100,
329
+ ...placeItemOnTrack(track, index, offset),
330
+ }));
331
+ };
332
+ export const minVisibleFraction = (ratio) => {
333
+ if (ratio === null || ratio === undefined || !Number.isFinite(ratio) || ratio <= 0)
334
+ return 1 / 3;
335
+ const wide = 16 / 9;
336
+ const tall = 9 / 16;
337
+ if (ratio >= wide)
338
+ return 1 / 2;
339
+ if (ratio >= 1)
340
+ return mix(1 / 3, 1 / 2, (ratio - 1) / (wide - 1));
341
+ if (ratio >= tall)
342
+ return mix(1 / 4, 1 / 3, (ratio - tall) / (1 - tall));
343
+ return 1 / 4;
344
+ };
@@ -0,0 +1,2 @@
1
+ import type { CarouselContext } from './types.ts';
2
+ export declare const getCarouselContext: () => CarouselContext, setCarouselContext: (context: CarouselContext) => CarouselContext;
@@ -0,0 +1,2 @@
1
+ import { createContext } from 'svelte';
2
+ export const [getCarouselContext, setCarouselContext] = createContext();
@@ -0,0 +1,3 @@
1
+ export { default as Carousel } from './Carousel.svelte';
2
+ export { default as CarouselItem } from './CarouselItem.svelte';
3
+ export { getCarouselContext, setCarouselContext } from './context.js';
@@ -0,0 +1,3 @@
1
+ export { default as Carousel } from './Carousel.svelte';
2
+ export { default as CarouselItem } from './CarouselItem.svelte';
3
+ export { getCarouselContext, setCarouselContext } from './context.js';
@@ -0,0 +1,31 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAnchorAttributes, HTMLAttributes, HTMLButtonAttributes } from 'svelte/elements';
3
+ export type CarouselVariant = 'multi-browse' | 'uncontained' | 'hero' | 'full-screen';
4
+ export type CarouselAlignment = 'start' | 'center';
5
+ export type CarouselOrientation = 'horizontal' | 'vertical';
6
+ export type CarouselItemElement = HTMLDivElement | HTMLButtonElement | HTMLAnchorElement;
7
+ export interface CarouselProps extends HTMLAttributes<HTMLDivElement> {
8
+ variant?: CarouselVariant;
9
+ alignment?: CarouselAlignment;
10
+ orientation?: CarouselOrientation;
11
+ snap?: boolean;
12
+ label?: string | null;
13
+ itemLabel?: (label: string, position: number, total: number) => string;
14
+ children?: Snippet;
15
+ scroller?: HTMLDivElement;
16
+ element?: HTMLDivElement;
17
+ }
18
+ export interface CarouselItemProps extends HTMLAttributes<CarouselItemElement>, Omit<HTMLButtonAttributes, keyof HTMLAttributes<HTMLButtonElement> | 'type'>, Omit<HTMLAnchorAttributes, keyof HTMLAttributes<HTMLAnchorElement> | 'type'> {
19
+ type: 'text' | 'button' | 'link';
20
+ label?: string | null;
21
+ image?: string | null;
22
+ aspectRatio?: number | null;
23
+ disabled?: boolean | null;
24
+ children?: Snippet;
25
+ element?: CarouselItemElement;
26
+ }
27
+ export interface CarouselContext {
28
+ items: CarouselItemElement[];
29
+ position: (item: CarouselItemElement) => number;
30
+ itemLabel: (label: string, position: number, total: number) => string;
31
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export * from './autocomplete/index.js';
3
3
  export * from './badge/index.js';
4
4
  export * from './button/index.js';
5
5
  export * from './card/index.js';
6
+ export * from './carousel/index.js';
6
7
  export * from './checkbox/index.js';
7
8
  export * from './chip/index.js';
8
9
  export * from './date-picker/index.js';
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ export * from './autocomplete/index.js';
3
3
  export * from './badge/index.js';
4
4
  export * from './button/index.js';
5
5
  export * from './card/index.js';
6
+ export * from './carousel/index.js';
6
7
  export * from './checkbox/index.js';
7
8
  export * from './chip/index.js';
8
9
  export * from './date-picker/index.js';
@@ -3,6 +3,8 @@ export declare const rovingTabindex: (itemSelector: string, options?: {
3
3
  currentAttr?: string;
4
4
  currentValue?: string;
5
5
  }) => Attachment<HTMLElement>;
6
- export declare const arrowKeyNav: (itemSelector: string, orientation?: "vertical" | "horizontal") => (event: KeyboardEvent & {
6
+ export declare const arrowKeyNav: (itemSelector: string, orientation?: "vertical" | "horizontal", { wrap }?: {
7
+ wrap?: boolean;
8
+ }) => (event: KeyboardEvent & {
7
9
  currentTarget: EventTarget & HTMLElement;
8
10
  }) => void;
@@ -47,7 +47,7 @@ export const rovingTabindex = (itemSelector, options = {}) => {
47
47
  };
48
48
  };
49
49
  };
50
- export const arrowKeyNav = (itemSelector, orientation = 'vertical') => (event) => {
50
+ export const arrowKeyNav = (itemSelector, orientation = 'vertical', { wrap = true } = {}) => (event) => {
51
51
  const [prev, next] = orientation === 'vertical'
52
52
  ? ['ArrowUp', 'ArrowDown']
53
53
  : ['ArrowLeft', 'ArrowRight'];
@@ -71,7 +71,11 @@ export const arrowKeyNav = (itemSelector, orientation = 'vertical') => (event) =
71
71
  else {
72
72
  const delta = key === next ? 1 : -1;
73
73
  const index = currentIndex + delta;
74
- target = index < 0 ? items[items.length - 1] : index >= items.length ? items[0] : items[index];
74
+ // `delta` is only ever ±1, so wrapping is one modulo and clamping is one clamp.
75
+ target =
76
+ items[wrap
77
+ ? (index + items.length) % items.length
78
+ : Math.min(Math.max(index, 0), items.length - 1)];
75
79
  }
76
80
  target.focus();
77
81
  event.preventDefault();
@@ -228,16 +228,15 @@
228
228
  min-width var(--np-motion-expressive-default-spatial);
229
229
  }
230
230
 
231
- .np-search-docked.np-search-expanded .np-search-container {
232
- animation: unroll var(--np-motion-expressive-default-spatial);
233
- }
234
231
  .np-search-docked.np-search-expanded .np-search-results {
235
- animation: fadeIn var(--np-motion-expressive-default-effects);
232
+ animation:
233
+ unroll var(--np-motion-expressive-default-spatial),
234
+ fadeIn var(--np-motion-expressive-default-effects);
236
235
  }
237
236
  }
238
237
  @keyframes unroll {
239
238
  from {
240
- clip-path: inset(-1.5rem -1.5rem calc(100% - var(--_bar-height)) -1.5rem);
239
+ clip-path: inset(0 -1.5rem 100% -1.5rem);
241
240
  }
242
241
  to {
243
242
  clip-path: inset(-1.5rem);
@@ -270,7 +269,9 @@
270
269
  .np-search-bar {
271
270
  transition:
272
271
  height var(--np-motion-expressive-default-spatial),
273
- background-color var(--np-motion-expressive-fast-effects);
272
+ background-color var(--np-motion-expressive-fast-effects),
273
+ border-start-start-radius var(--np-motion-expressive-fast-effects),
274
+ border-start-end-radius var(--np-motion-expressive-fast-effects);
274
275
  }
275
276
  }
276
277