@inglorious/ui 5.0.0 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +19 -9
- package/src/controls/controls.css +1 -1
- package/src/data-display/before-after/before-after.stories.js +74 -0
- package/src/data-display/before-after/handlers.js +44 -0
- package/src/data-display/before-after/helpers.js +4 -0
- package/src/data-display/before-after/index.js +17 -0
- package/src/data-display/before-after/style.css +128 -0
- package/src/data-display/before-after/template.js +225 -0
- package/src/data-display/carousel/carousel.stories.js +166 -0
- package/src/data-display/carousel/handlers.js +78 -0
- package/src/data-display/carousel/helpers.js +54 -0
- package/src/data-display/carousel/index.js +17 -0
- package/src/data-display/carousel/style.css +233 -0
- package/src/data-display/carousel/template.js +655 -0
- package/src/data-display/data-display.css +2 -0
- package/src/examples/dashboard/app-drawer.js +6 -0
- package/src/examples/dashboard/before-after-data.js +29 -0
- package/src/examples/dashboard/primitive-content.js +151 -33
- package/src/index.js +2 -0
- package/src/layout/flex/flex.stories.js +49 -0
- package/src/layout/flex/style.css +59 -0
- package/src/layout/flex/template.js +2 -0
- package/types/data-display/before-after.d.ts +39 -0
- package/types/data-display/carousel.d.ts +72 -0
- package/types/layout/flex.d.ts +1 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { html } from "@inglorious/web"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createEntityRender,
|
|
5
|
+
notifyActionArgType,
|
|
6
|
+
} from "../../stories/notifyStory.js"
|
|
7
|
+
import { Carousel } from "./index.js"
|
|
8
|
+
|
|
9
|
+
const COLORS = ["#22d3ee", "#4f46e5", "#f59e0b", "#10b981", "#ef4444"]
|
|
10
|
+
|
|
11
|
+
const slide = (index) =>
|
|
12
|
+
html`<div
|
|
13
|
+
style="display:grid;place-items:center;height:14rem;border-radius:0.5rem;
|
|
14
|
+
background:${COLORS[index]};color:#fff;font:700 2rem sans-serif;"
|
|
15
|
+
>
|
|
16
|
+
${index + 1}
|
|
17
|
+
</div>`
|
|
18
|
+
|
|
19
|
+
const items = COLORS.map((color, index) => slide(index))
|
|
20
|
+
|
|
21
|
+
export default {
|
|
22
|
+
title: "Data Display/Carousel",
|
|
23
|
+
tags: ["autodocs"],
|
|
24
|
+
render: createEntityRender({ Carousel }),
|
|
25
|
+
argTypes: {
|
|
26
|
+
...notifyActionArgType,
|
|
27
|
+
label: { control: "text", description: "Accessible label." },
|
|
28
|
+
page: {
|
|
29
|
+
control: "number",
|
|
30
|
+
description: "Index of the item the viewport settled on.",
|
|
31
|
+
},
|
|
32
|
+
axis: {
|
|
33
|
+
control: "inline-radio",
|
|
34
|
+
options: ["x", "y"],
|
|
35
|
+
description: "Scroll direction.",
|
|
36
|
+
},
|
|
37
|
+
gap: {
|
|
38
|
+
control: "select",
|
|
39
|
+
options: ["none", "sm", "md", "lg", "xl"],
|
|
40
|
+
description: "Spacing between items.",
|
|
41
|
+
},
|
|
42
|
+
align: {
|
|
43
|
+
control: "select",
|
|
44
|
+
options: ["stretch", "start", "center", "end"],
|
|
45
|
+
description: "Off-axis alignment of items.",
|
|
46
|
+
},
|
|
47
|
+
arrowPlacement: {
|
|
48
|
+
control: "inline-radio",
|
|
49
|
+
options: ["inside", "outside"],
|
|
50
|
+
description: "Float the arrows over the slides, or sit them beside.",
|
|
51
|
+
},
|
|
52
|
+
arrowVariant: {
|
|
53
|
+
control: "select",
|
|
54
|
+
options: ["default", "outline", "ghost"],
|
|
55
|
+
description: "Button variant used for the arrows.",
|
|
56
|
+
},
|
|
57
|
+
arrowColor: {
|
|
58
|
+
control: "select",
|
|
59
|
+
options: ["primary", "secondary", "success", "warning", "error", "info"],
|
|
60
|
+
description: "Button color used for the arrows.",
|
|
61
|
+
},
|
|
62
|
+
isInfinite: {
|
|
63
|
+
control: "boolean",
|
|
64
|
+
description: "Wrap past the ends by rotating the items.",
|
|
65
|
+
},
|
|
66
|
+
hasArrows: {
|
|
67
|
+
control: "boolean",
|
|
68
|
+
description: "Show previous/next arrows.",
|
|
69
|
+
},
|
|
70
|
+
hasIndicators: { control: "boolean", description: "Show pagination dots." },
|
|
71
|
+
isFullWidth: {
|
|
72
|
+
control: "boolean",
|
|
73
|
+
description: "Expand to container width.",
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
parameters: {
|
|
77
|
+
docs: {
|
|
78
|
+
description: {
|
|
79
|
+
component:
|
|
80
|
+
"Carousel built on CSS scroll snapping: swiping, momentum and snapping come from the browser, while the entity holds the current page. Drag, use the arrows or dots, or focus the viewport and use the arrow keys / Home / End.",
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const Default = {}
|
|
87
|
+
Default.args = {
|
|
88
|
+
id: "carousel",
|
|
89
|
+
type: "Carousel",
|
|
90
|
+
label: "Example carousel",
|
|
91
|
+
items,
|
|
92
|
+
page: 0,
|
|
93
|
+
axis: "x",
|
|
94
|
+
gap: "md",
|
|
95
|
+
align: "stretch",
|
|
96
|
+
arrowPlacement: "inside",
|
|
97
|
+
arrowVariant: "default",
|
|
98
|
+
isInfinite: true,
|
|
99
|
+
hasArrows: true,
|
|
100
|
+
hasIndicators: true,
|
|
101
|
+
isFullWidth: true,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Arrows beside the scrolling area, as most carousels show them. */
|
|
105
|
+
export const ArrowsOutside = {}
|
|
106
|
+
ArrowsOutside.args = {
|
|
107
|
+
...Default.args,
|
|
108
|
+
id: "carousel-arrows-outside",
|
|
109
|
+
arrowPlacement: "outside",
|
|
110
|
+
arrowVariant: "ghost",
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Wraps past the ends: after 5 it continues to 1, rather than rewinding. */
|
|
114
|
+
export const Infinite = {}
|
|
115
|
+
Infinite.args = {
|
|
116
|
+
...Default.args,
|
|
117
|
+
id: "carousel-infinite",
|
|
118
|
+
isInfinite: true,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const StartOnThirdSlide = {}
|
|
122
|
+
StartOnThirdSlide.args = {
|
|
123
|
+
...Default.args,
|
|
124
|
+
id: "carousel-third",
|
|
125
|
+
page: 2,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Vertical carousels need a height to scroll within: set
|
|
130
|
+
* `--iw-carousel-viewport-size` (it defaults to `20rem`).
|
|
131
|
+
*/
|
|
132
|
+
export const Vertical = {}
|
|
133
|
+
Vertical.args = {
|
|
134
|
+
...Default.args,
|
|
135
|
+
id: "carousel-vertical",
|
|
136
|
+
axis: "y",
|
|
137
|
+
}
|
|
138
|
+
Vertical.decorators = [
|
|
139
|
+
(story) =>
|
|
140
|
+
html`<div style="--iw-carousel-viewport-size: 14rem">${story()}</div>`,
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
export const Bare = {}
|
|
144
|
+
Bare.args = {
|
|
145
|
+
...Default.args,
|
|
146
|
+
id: "carousel-bare",
|
|
147
|
+
hasArrows: false,
|
|
148
|
+
hasIndicators: false,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Several items at once, by overriding `--iw-carousel-item-size`.
|
|
153
|
+
* Trailing items share the final scroll position, so the last pages settle
|
|
154
|
+
* together — one item per view is the exact case.
|
|
155
|
+
*/
|
|
156
|
+
export const ManyPerView = {}
|
|
157
|
+
ManyPerView.args = {
|
|
158
|
+
...Default.args,
|
|
159
|
+
id: "carousel-many",
|
|
160
|
+
// The infinite treadmill keeps one slide at its centre, so it is not a good
|
|
161
|
+
// fit when several slides are intentionally visible at once.
|
|
162
|
+
isInfinite: false,
|
|
163
|
+
}
|
|
164
|
+
ManyPerView.decorators = [
|
|
165
|
+
(story) => html`<div style="--iw-carousel-item-size: 14rem">${story()}</div>`,
|
|
166
|
+
]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('../../../types/data-display/carousel.js').CarouselProps} CarouselEntity
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
clampPage,
|
|
7
|
+
DEFAULT_ALIGN,
|
|
8
|
+
DEFAULT_ARROW_PLACEMENT,
|
|
9
|
+
DEFAULT_ARROW_VARIANT,
|
|
10
|
+
DEFAULT_AXIS,
|
|
11
|
+
DEFAULT_GAP,
|
|
12
|
+
FIRST_PAGE,
|
|
13
|
+
NO_ROTATION,
|
|
14
|
+
normalizeRotation,
|
|
15
|
+
} from "./helpers.js"
|
|
16
|
+
|
|
17
|
+
const ENOUGH_TO_WRAP = 1
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Initialize missing carousel state defaults.
|
|
21
|
+
* The only persistent piece of state is `page`: the index of the item the
|
|
22
|
+
* viewport has settled on. Everything else is configuration.
|
|
23
|
+
* @param {CarouselEntity} entity
|
|
24
|
+
*/
|
|
25
|
+
export function create(entity) {
|
|
26
|
+
entity.items ??= []
|
|
27
|
+
entity.page ??= FIRST_PAGE
|
|
28
|
+
entity.rotation ??= NO_ROTATION
|
|
29
|
+
|
|
30
|
+
entity.isInfinite ??= false
|
|
31
|
+
entity.axis ??= DEFAULT_AXIS
|
|
32
|
+
entity.gap ??= DEFAULT_GAP
|
|
33
|
+
entity.align ??= DEFAULT_ALIGN
|
|
34
|
+
|
|
35
|
+
entity.arrowPlacement ??= DEFAULT_ARROW_PLACEMENT
|
|
36
|
+
entity.arrowVariant ??= DEFAULT_ARROW_VARIANT
|
|
37
|
+
|
|
38
|
+
entity.label ??= ""
|
|
39
|
+
entity.hasArrows ??= true
|
|
40
|
+
entity.hasIndicators ??= true
|
|
41
|
+
entity.isFullWidth ??= false
|
|
42
|
+
|
|
43
|
+
// An infinite carousel keeps the current item in the middle slot, so the
|
|
44
|
+
// starting item is placed there by rotating the strip up front.
|
|
45
|
+
if (entity.isInfinite && entity.items.length > ENOUGH_TO_WRAP) {
|
|
46
|
+
const home = Math.floor(entity.items.length / 2)
|
|
47
|
+
entity.rotation = normalizeRotation(entity.page - home, entity.items.length)
|
|
48
|
+
entity.page = home
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Settle on another page, clamped to the available items.
|
|
54
|
+
* @param {CarouselEntity} entity
|
|
55
|
+
* @param {number} page
|
|
56
|
+
*/
|
|
57
|
+
export function pageChange(entity, page) {
|
|
58
|
+
const value = Number(page)
|
|
59
|
+
if (Number.isNaN(value)) return
|
|
60
|
+
|
|
61
|
+
entity.page = clampPage(value, entity.items)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Rotate the items by `step` (wrapping), which is how an infinite carousel
|
|
66
|
+
* continues forward past the end instead of rewinding to the start.
|
|
67
|
+
* @param {CarouselEntity} entity
|
|
68
|
+
* @param {number} step
|
|
69
|
+
*/
|
|
70
|
+
export function rotate(entity, step) {
|
|
71
|
+
const value = Number(step)
|
|
72
|
+
if (Number.isNaN(value)) return
|
|
73
|
+
|
|
74
|
+
entity.rotation = normalizeRotation(
|
|
75
|
+
(entity.rotation ?? NO_ROTATION) + value,
|
|
76
|
+
entity.items?.length ?? 0,
|
|
77
|
+
)
|
|
78
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { clamp } from "@inglorious/utils/math/numbers.js"
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_AXIS = "x"
|
|
4
|
+
export const VERTICAL_AXIS = "y"
|
|
5
|
+
export const DEFAULT_GAP = "none"
|
|
6
|
+
export const DEFAULT_ALIGN = "stretch"
|
|
7
|
+
export const DEFAULT_ARROW_PLACEMENT = "inside"
|
|
8
|
+
export const DEFAULT_ARROW_VARIANT = "default"
|
|
9
|
+
export const FIRST_PAGE = 0
|
|
10
|
+
export const NO_ROTATION = 0
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Bring a rotation into the `0 .. len - 1` range, wrapping negatives.
|
|
14
|
+
* @param {number} rotation
|
|
15
|
+
* @param {number} length
|
|
16
|
+
* @returns {number}
|
|
17
|
+
*/
|
|
18
|
+
export function normalizeRotation(rotation, length) {
|
|
19
|
+
if (length <= 0) return NO_ROTATION
|
|
20
|
+
return ((rotation % length) + length) % length
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Rotate items so that `items[rotation]` becomes the first one — this is how an
|
|
25
|
+
* infinite carousel turns `[1, 2, 3, 4, 5]` into `[2, 3, 4, 5, 1]` instead of
|
|
26
|
+
* rewinding all the way back to the start.
|
|
27
|
+
* @param {unknown[]} [items]
|
|
28
|
+
* @param {number} [rotation]
|
|
29
|
+
* @returns {unknown[]}
|
|
30
|
+
*/
|
|
31
|
+
export function rotateItems(items = [], rotation = NO_ROTATION) {
|
|
32
|
+
if (!items.length) return items
|
|
33
|
+
const at = normalizeRotation(rotation, items.length)
|
|
34
|
+
return items.slice(at).concat(items.slice(0, at))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get the index of the last available page.
|
|
39
|
+
* @param {unknown[]} [items]
|
|
40
|
+
* @returns {number}
|
|
41
|
+
*/
|
|
42
|
+
export function getLastPage(items = []) {
|
|
43
|
+
return Math.max(FIRST_PAGE, items.length - 1)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Keep a page index within the available pages.
|
|
48
|
+
* @param {number} page
|
|
49
|
+
* @param {unknown[]} [items]
|
|
50
|
+
* @returns {number}
|
|
51
|
+
*/
|
|
52
|
+
export function clampPage(page, items) {
|
|
53
|
+
return clamp(page, FIRST_PAGE, getLastPage(items))
|
|
54
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as handlers from "./handlers.js"
|
|
2
|
+
import { Carousel as renderers } from "./template.js"
|
|
3
|
+
|
|
4
|
+
export const Carousel = {
|
|
5
|
+
...handlers,
|
|
6
|
+
...renderers,
|
|
7
|
+
render(entity, api) {
|
|
8
|
+
const id = entity.id
|
|
9
|
+
const props = {
|
|
10
|
+
...entity,
|
|
11
|
+
onPageChange: (page) => api.notify(`#${id}:pageChange`, page),
|
|
12
|
+
onRotate: (step) => api.notify(`#${id}:rotate`, step),
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return this.renderCarousel(props)
|
|
16
|
+
},
|
|
17
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
.iw-carousel {
|
|
2
|
+
--iw-carousel-arrow-size: 2.25rem;
|
|
3
|
+
--iw-carousel-arrow-bg: var(--iw-color-surface);
|
|
4
|
+
--iw-carousel-arrow-color: var(--iw-color-text);
|
|
5
|
+
--iw-carousel-arrow-inset: var(--iw-space-2);
|
|
6
|
+
--iw-carousel-arrow-chevron: 5px;
|
|
7
|
+
--iw-carousel-indicator-size: 0.5rem;
|
|
8
|
+
--iw-carousel-indicator-color: var(--iw-color-border);
|
|
9
|
+
--iw-carousel-indicator-color-current: var(--iw-color-primary);
|
|
10
|
+
|
|
11
|
+
position: relative;
|
|
12
|
+
display: inline-block;
|
|
13
|
+
max-width: 100%;
|
|
14
|
+
|
|
15
|
+
&.iw-carousel-full-width {
|
|
16
|
+
display: block;
|
|
17
|
+
width: 100%;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* A column stage otherwise shrink-wraps to its arrows instead of the
|
|
21
|
+
available inline size. */
|
|
22
|
+
&.iw-carousel-vertical {
|
|
23
|
+
display: block;
|
|
24
|
+
width: 100%;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* Snapping, swiping, momentum and RTL all come from the browser. */
|
|
28
|
+
.iw-carousel-viewport {
|
|
29
|
+
position: relative;
|
|
30
|
+
display: flex;
|
|
31
|
+
overflow-x: auto;
|
|
32
|
+
overflow-y: hidden;
|
|
33
|
+
scroll-snap-type: x mandatory;
|
|
34
|
+
overscroll-behavior-x: contain;
|
|
35
|
+
scrollbar-width: none;
|
|
36
|
+
outline: none;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.iw-carousel-viewport::-webkit-scrollbar {
|
|
40
|
+
display: none;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.iw-carousel-viewport:focus-visible {
|
|
44
|
+
outline: 2px solid var(--iw-color-primary);
|
|
45
|
+
outline-offset: 2px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* One item per view by default, so every item can reach the start of the
|
|
49
|
+
viewport and each one is exactly one page. Override
|
|
50
|
+
--iw-carousel-item-size to show several at once. */
|
|
51
|
+
.iw-carousel-item {
|
|
52
|
+
flex: 0 0 var(--iw-carousel-item-size, 100%);
|
|
53
|
+
scroll-snap-align: start;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* Smooth scrolling would animate every drag step, so it is turned off while
|
|
57
|
+
dragging. Snapping stays on: the browser then lands on the nearest slide by
|
|
58
|
+
itself, and re-enabling it instead would snap back to the item it
|
|
59
|
+
remembered. Doubled up on the class so this outranks the reduced-motion
|
|
60
|
+
rule at the end of the file. */
|
|
61
|
+
.iw-carousel-viewport.iw-carousel-viewport-dragging {
|
|
62
|
+
scroll-behavior: auto;
|
|
63
|
+
scroll-snap-type: none;
|
|
64
|
+
cursor: grabbing;
|
|
65
|
+
user-select: none;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* A vertical carousel has to be given a height, otherwise the viewport just
|
|
69
|
+
grows to fit every item and there is nothing left to scroll. Override
|
|
70
|
+
--iw-carousel-viewport-size to change it. */
|
|
71
|
+
&.iw-carousel-vertical .iw-carousel-viewport {
|
|
72
|
+
flex-direction: column;
|
|
73
|
+
height: var(--iw-carousel-viewport-size, 20rem);
|
|
74
|
+
overflow-x: hidden;
|
|
75
|
+
overflow-y: auto;
|
|
76
|
+
scroll-snap-type: y mandatory;
|
|
77
|
+
overscroll-behavior-y: contain;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* Gap tokens mirror Flex. */
|
|
81
|
+
&.iw-carousel-gap-sm .iw-carousel-viewport {
|
|
82
|
+
gap: var(--iw-space-2);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
&.iw-carousel-gap-md .iw-carousel-viewport {
|
|
86
|
+
gap: var(--iw-space-4);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
&.iw-carousel-gap-lg .iw-carousel-viewport {
|
|
90
|
+
gap: var(--iw-space-6);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
&.iw-carousel-gap-xl .iw-carousel-viewport {
|
|
94
|
+
gap: var(--iw-space-8);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* Off-axis alignment. */
|
|
98
|
+
&.iw-carousel-align-start .iw-carousel-viewport {
|
|
99
|
+
align-items: flex-start;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
&.iw-carousel-align-center .iw-carousel-viewport {
|
|
103
|
+
align-items: center;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
&.iw-carousel-align-end .iw-carousel-viewport {
|
|
107
|
+
align-items: flex-end;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* The stage holds the viewport and its arrows, so centring ignores the dots. */
|
|
111
|
+
.iw-carousel-stage {
|
|
112
|
+
position: relative;
|
|
113
|
+
display: flex;
|
|
114
|
+
align-items: center;
|
|
115
|
+
gap: var(--iw-space-2);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
&.iw-carousel-vertical .iw-carousel-stage {
|
|
119
|
+
flex-direction: column;
|
|
120
|
+
align-items: stretch;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.iw-carousel-stage .iw-carousel-viewport {
|
|
124
|
+
flex: 1 1 auto;
|
|
125
|
+
min-width: 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* Inside: float the arrows over the slides. */
|
|
129
|
+
&.iw-carousel-arrows-inside .iw-carousel-stage {
|
|
130
|
+
gap: 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
&.iw-carousel-arrows-inside .iw-carousel-arrow {
|
|
134
|
+
position: absolute;
|
|
135
|
+
top: 50%;
|
|
136
|
+
z-index: 1;
|
|
137
|
+
transform: translateY(-50%);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
&.iw-carousel-arrows-inside .iw-carousel-arrow-previous {
|
|
141
|
+
left: var(--iw-carousel-arrow-inset);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
&.iw-carousel-arrows-inside .iw-carousel-arrow-next {
|
|
145
|
+
right: var(--iw-carousel-arrow-inset);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
&.iw-carousel-vertical.iw-carousel-arrows-inside .iw-carousel-arrow {
|
|
149
|
+
top: auto;
|
|
150
|
+
right: auto;
|
|
151
|
+
left: 50%;
|
|
152
|
+
transform: translateX(-50%);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
&.iw-carousel-vertical.iw-carousel-arrows-inside .iw-carousel-arrow-previous {
|
|
156
|
+
top: var(--iw-carousel-arrow-inset);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
&.iw-carousel-vertical.iw-carousel-arrows-inside .iw-carousel-arrow-next {
|
|
160
|
+
bottom: var(--iw-carousel-arrow-inset);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* Outside: the arrows sit beside the scrolling area, flex handles centring. */
|
|
164
|
+
&.iw-carousel-arrows-outside .iw-carousel-arrow {
|
|
165
|
+
position: static;
|
|
166
|
+
flex: 0 0 auto;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* Chevrons drawn with borders, no icon font needed. */
|
|
170
|
+
.iw-carousel-chevron {
|
|
171
|
+
display: block;
|
|
172
|
+
width: 0;
|
|
173
|
+
height: 0;
|
|
174
|
+
border-top: var(--iw-carousel-arrow-chevron) solid transparent;
|
|
175
|
+
border-bottom: var(--iw-carousel-arrow-chevron) solid transparent;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.iw-carousel-arrow-previous .iw-carousel-chevron {
|
|
179
|
+
margin-right: 2px;
|
|
180
|
+
border-right: 7px solid currentcolor;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.iw-carousel-arrow-next .iw-carousel-chevron {
|
|
184
|
+
margin-left: 2px;
|
|
185
|
+
border-left: 7px solid currentcolor;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
&.iw-carousel-vertical .iw-carousel-chevron {
|
|
189
|
+
border-right: none;
|
|
190
|
+
border-left: none;
|
|
191
|
+
border-inline: var(--iw-carousel-arrow-chevron) solid transparent;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
&.iw-carousel-vertical .iw-carousel-arrow-previous .iw-carousel-chevron {
|
|
195
|
+
margin: 0 0 2px;
|
|
196
|
+
border-top: none;
|
|
197
|
+
border-bottom: 7px solid currentcolor;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
&.iw-carousel-vertical .iw-carousel-arrow-next .iw-carousel-chevron {
|
|
201
|
+
margin: 2px 0 0;
|
|
202
|
+
border-top: 7px solid currentcolor;
|
|
203
|
+
border-bottom: none;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.iw-carousel-indicators {
|
|
207
|
+
display: flex;
|
|
208
|
+
justify-content: center;
|
|
209
|
+
gap: var(--iw-space-2);
|
|
210
|
+
padding: var(--iw-space-2);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.iw-carousel-indicator {
|
|
214
|
+
width: var(--iw-carousel-indicator-size);
|
|
215
|
+
height: var(--iw-carousel-indicator-size);
|
|
216
|
+
padding: 0;
|
|
217
|
+
border: none;
|
|
218
|
+
border-radius: var(--iw-radius-full);
|
|
219
|
+
background: var(--iw-carousel-indicator-color);
|
|
220
|
+
cursor: pointer;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.iw-carousel-indicator-current {
|
|
224
|
+
background: var(--iw-carousel-indicator-color-current);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/* Respect reduced motion: only animate paging when the user allows it. */
|
|
229
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
230
|
+
.iw-carousel .iw-carousel-viewport {
|
|
231
|
+
scroll-behavior: smooth;
|
|
232
|
+
}
|
|
233
|
+
}
|