@latte-macchiat-io/latte-vanilla-components 0.0.190 → 0.0.192
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/README.md +16 -6
- package/package.json +4 -1
- package/src/components/Actions/index.tsx +20 -0
- package/src/components/Actions/styles.css.ts +54 -0
- package/src/components/Button/index.tsx +29 -0
- package/src/components/Button/stories.tsx +4 -22
- package/src/components/Button/styles.css.ts +131 -0
- package/src/components/Carousel/{Carousel.tsx → index.tsx} +18 -115
- package/src/components/Carousel/styles.css.ts +176 -0
- package/src/components/Columns/index.tsx +36 -0
- package/src/components/Columns/styles.css.ts +70 -0
- package/src/components/ConsentCookie/ConsentCookie.css.ts +1 -1
- package/src/components/ConsentCookie/ConsentCookie.tsx +3 -3
- package/src/components/Footer/index.tsx +21 -0
- package/src/components/Footer/styles.css.ts +33 -0
- package/src/components/Form/Form.css.ts +1 -1
- package/src/components/Form/Row/Row.css.ts +1 -1
- package/src/components/Form/TextField/Input/Input.css.ts +1 -1
- package/src/components/Form/TextField/Label/Label.css.ts +1 -1
- package/src/components/Form/TextField/TextField.css.ts +1 -1
- package/src/components/Form/TextField/Textarea/Textarea.css.ts +1 -1
- package/src/components/Header/index.tsx +53 -0
- package/src/components/Header/styles.css.ts +89 -0
- package/src/components/Heading/index.tsx +22 -0
- package/src/components/Heading/styles.css.ts +66 -0
- package/src/components/Heading/types.tsx +1 -0
- package/src/components/Icon/index.tsx +25 -0
- package/src/components/Icon/style.css.ts +11 -0
- package/src/components/KeyNumber/index.tsx +51 -0
- package/src/components/KeyNumber/styles.css.ts +76 -0
- package/src/components/LanguageSwitcher/index.tsx +80 -0
- package/src/components/LanguageSwitcher/{LanguageSwitcher.css.ts → styles.css.ts} +1 -1
- package/src/components/Logo/index.tsx +13 -0
- package/src/components/Logo/styles.css.ts +14 -0
- package/src/components/Main/index.tsx +17 -0
- package/src/components/Main/styles.css.ts +14 -0
- package/src/components/Modal/index.tsx +42 -0
- package/src/components/Modal/stories.tsx +14 -358
- package/src/components/Modal/styles.css.ts +90 -0
- package/src/components/Nav/index.tsx +22 -0
- package/src/components/Nav/styles.css.ts +30 -0
- package/src/components/NavLegal/index.tsx +17 -0
- package/src/components/NavLegal/styles.css.ts +20 -0
- package/src/components/NavSocial/index.tsx +32 -0
- package/src/components/NavSocial/styles.css.ts +33 -0
- package/src/components/Section/index.tsx +20 -0
- package/src/components/Section/stories.tsx +5 -57
- package/src/components/Section/styles.css.ts +106 -0
- package/src/components/ThemeTest/ThemeTest.css.ts +11 -0
- package/src/components/ThemeTest/ThemeTest.tsx +12 -0
- package/src/components/ThemeToggle/ThemeToggle.tsx +30 -0
- package/src/components/Video/index.tsx +117 -0
- package/src/components/Video/styles.css.ts +200 -0
- package/src/index.ts +29 -41
- package/src/styles/mediaqueries.ts +2 -0
- package/src/styles/sprinkles.css.ts +11 -8
- package/src/theme/baseThemeValues.ts +1235 -0
- package/src/theme/contract.css.ts +676 -0
- package/src/{themes → theme}/createTheme.ts +40 -1
- package/src/theme/default.css.ts +10 -0
- package/src/utils/combineResponsive.ts +9 -0
- package/src/utils/generateResponsiveMedia.ts +19 -0
- package/src/components/Actions/Actions.css.ts +0 -113
- package/src/components/Actions/Actions.tsx +0 -132
- package/src/components/Button/Button.css.ts +0 -119
- package/src/components/Button/Button.tsx +0 -132
- package/src/components/Carousel/Carousel.css.ts +0 -179
- package/src/components/Columns/Columns.css.ts +0 -185
- package/src/components/Columns/Columns.tsx +0 -142
- package/src/components/Footer/Footer.css.ts +0 -108
- package/src/components/Footer/Footer.tsx +0 -130
- package/src/components/Header/Header.css.ts +0 -111
- package/src/components/Header/Header.tsx +0 -158
- package/src/components/Icon/Icon.css.ts +0 -101
- package/src/components/Icon/Icon.tsx +0 -159
- package/src/components/KeyNumber/KeyNumber.css.ts +0 -158
- package/src/components/KeyNumber/KeyNumber.tsx +0 -166
- package/src/components/LanguageSwitcher/LanguageSwitcher.tsx +0 -168
- package/src/components/Logo/Logo.css.ts +0 -98
- package/src/components/Logo/Logo.tsx +0 -137
- package/src/components/Main/Main.css.ts +0 -62
- package/src/components/Main/Main.tsx +0 -130
- package/src/components/Modal/Modal.css.ts +0 -203
- package/src/components/Modal/Modal.tsx +0 -194
- package/src/components/Nav/Nav.css.ts +0 -123
- package/src/components/Nav/Nav.tsx +0 -130
- package/src/components/NavLegal/NavLegal.css.ts +0 -121
- package/src/components/NavLegal/NavLegal.tsx +0 -133
- package/src/components/NavSocial/NavSocial.css.ts +0 -121
- package/src/components/NavSocial/NavSocial.tsx +0 -169
- package/src/components/Section/Section.css.ts +0 -101
- package/src/components/Section/Section.tsx +0 -130
- package/src/components/Video/Video.css.ts +0 -210
- package/src/components/Video/Video.tsx +0 -243
- package/src/components/VideoFullWidth/VideoFullWidth.css.ts +0 -50
- package/src/components/VideoFullWidth/VideoFullWidth.tsx +0 -152
- package/src/components/VideoFullWidth/export.tsx +0 -2
- package/src/themes/baseThemeValues.ts +0 -160
- package/src/themes/contract.css.ts +0 -83
- package/src/types/withClassName.ts +0 -4
- /package/src/{utils → components/ConsentCookie}/cookie.ts +0 -0
package/README.md
CHANGED
@@ -1,7 +1,9 @@
|
|
1
|
-
# 🥤 Latte Vanilla Components
|
1
|
+
# 🥤 Latte Vanilla Components [](https://www.npmjs.com/package/@latte-macchiat-io/latte-vanilla-components)
|
2
2
|
|
3
|
-
Beautiful, type-safe React components powered by Vanilla Extract CSS
|
4
|
-
|
3
|
+
Beautiful, type-safe React components powered by Vanilla Extract CSS for amazing projects, with a touch of Vanilla 🥤
|
4
|
+
|
5
|
+
Intended for internal use by the 👩💻🧑💻 [latte-macchiat.io](https://latte-macchiat.io) team, we can't offer official support, but feel free to reach out
|
6
|
+
with questions, feedback or project ideas!
|
5
7
|
|
6
8
|
## ✨ Features
|
7
9
|
|
@@ -12,6 +14,14 @@ system that's both powerful and easy to use.
|
|
12
14
|
- ⚡ **Developer Experience** - Hot reloading, IntelliSense, documentation
|
13
15
|
- 🎯 **Production Ready** - Optimized builds, minimal bundle size
|
14
16
|
|
17
|
+
## 🤓 Philosophy
|
18
|
+
|
19
|
+
This library is shipped as raw code (ESM + TypeScript) to be compiled in your project, allowing full
|
20
|
+
customization and tree-shaking. It is not a pre-built design system, but rather a set of building blocks
|
21
|
+
to create your own unique designs.
|
22
|
+
|
23
|
+
You will need to set up Vanilla Extract in your build system (Next.js, Vite, etc.) to use this library.
|
24
|
+
|
15
25
|
## 📦 Installation
|
16
26
|
|
17
27
|
```bash
|
@@ -30,7 +40,7 @@ yarn add @latte-macchiat-io/latte-vanilla-components
|
|
30
40
|
|
31
41
|
```typescript
|
32
42
|
// src/styles/theme.css.ts
|
33
|
-
import {createDarkTheme, createLightTheme} from "@latte-macchiat-io/latte-vanilla-components";
|
43
|
+
import { createDarkTheme, createLightTheme } from "@latte-macchiat-io/latte-vanilla-components";
|
34
44
|
|
35
45
|
// Create and apply the default light theme (minimal setup)
|
36
46
|
createLightTheme();
|
@@ -221,8 +231,8 @@ All components are mobile-first and responsive:
|
|
221
231
|
|
222
232
|
```typescript
|
223
233
|
// next.config.js
|
224
|
-
import type {NextConfig} from "next";
|
225
|
-
import {createVanillaExtractPlugin} from "@vanilla-extract/next-plugin";
|
234
|
+
import type { NextConfig } from "next";
|
235
|
+
import { createVanillaExtractPlugin } from "@vanilla-extract/next-plugin";
|
226
236
|
|
227
237
|
const nextConfig: NextConfig = {
|
228
238
|
/* config options here */
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@latte-macchiat-io/latte-vanilla-components",
|
3
|
-
"version": "0.0.
|
3
|
+
"version": "0.0.192",
|
4
4
|
"description": "Beautiful components for amazing projects, with a touch of Vanilla 🥤",
|
5
5
|
"type": "module",
|
6
6
|
"main": "./src/index.ts",
|
@@ -65,6 +65,9 @@
|
|
65
65
|
"plugin:storybook/recommended"
|
66
66
|
]
|
67
67
|
},
|
68
|
+
"dependencies": {
|
69
|
+
"@vanilla-extract/css-utils": "^0.1.6"
|
70
|
+
},
|
68
71
|
"scripts": {
|
69
72
|
"build": "echo \"Source-only distribution - no build needed\"",
|
70
73
|
"test": "echo \"Error: no test specified\" && exit 1",
|
@@ -0,0 +1,20 @@
|
|
1
|
+
'use client';
|
2
|
+
|
3
|
+
import { clsx } from 'clsx';
|
4
|
+
import { forwardRef } from 'react';
|
5
|
+
|
6
|
+
import { actionsRecipe, type ActionsVariants } from './styles.css';
|
7
|
+
|
8
|
+
export type ActionsProps = React.HTMLAttributes<HTMLDivElement> &
|
9
|
+
ActionsVariants & {
|
10
|
+
css?: string;
|
11
|
+
children: React.ReactNode;
|
12
|
+
};
|
13
|
+
|
14
|
+
export const Actions = forwardRef<HTMLDivElement, ActionsProps>(({ align, direction, css, className, children }, ref) => (
|
15
|
+
<div ref={ref} className={clsx(actionsRecipe({ align, direction }), css, className)}>
|
16
|
+
{children}
|
17
|
+
</div>
|
18
|
+
));
|
19
|
+
|
20
|
+
Actions.displayName = 'Actions';
|
@@ -0,0 +1,54 @@
|
|
1
|
+
import { recipe, RecipeVariants } from '@vanilla-extract/recipes';
|
2
|
+
|
3
|
+
import { themeContract } from '../../theme/contract.css';
|
4
|
+
import { generateResponsiveMedia } from '../../utils/generateResponsiveMedia';
|
5
|
+
|
6
|
+
export const actionsRecipe = recipe({
|
7
|
+
base: [
|
8
|
+
{
|
9
|
+
width: '100%',
|
10
|
+
display: 'flex',
|
11
|
+
flexWrap: 'wrap',
|
12
|
+
|
13
|
+
'@media': {
|
14
|
+
...generateResponsiveMedia({
|
15
|
+
gap: themeContract.actions.gap,
|
16
|
+
paddingTop: themeContract.actions.paddingBottom,
|
17
|
+
paddingBottom: themeContract.actions.paddingBottom,
|
18
|
+
}),
|
19
|
+
},
|
20
|
+
},
|
21
|
+
],
|
22
|
+
|
23
|
+
variants: {
|
24
|
+
align: {
|
25
|
+
left: {
|
26
|
+
alignItems: 'flex-start',
|
27
|
+
justifyContent: 'flex-start',
|
28
|
+
},
|
29
|
+
center: {
|
30
|
+
alignItems: 'center',
|
31
|
+
justifyContent: 'center',
|
32
|
+
},
|
33
|
+
right: {
|
34
|
+
alignItems: 'flex-end',
|
35
|
+
justifyContent: 'flex-end',
|
36
|
+
},
|
37
|
+
},
|
38
|
+
direction: {
|
39
|
+
row: {
|
40
|
+
flexDirection: 'row',
|
41
|
+
},
|
42
|
+
column: {
|
43
|
+
flexDirection: 'column',
|
44
|
+
},
|
45
|
+
},
|
46
|
+
},
|
47
|
+
|
48
|
+
defaultVariants: {
|
49
|
+
align: 'left',
|
50
|
+
direction: 'row',
|
51
|
+
},
|
52
|
+
});
|
53
|
+
|
54
|
+
export type ActionsVariants = RecipeVariants<typeof actionsRecipe>;
|
@@ -0,0 +1,29 @@
|
|
1
|
+
'use client';
|
2
|
+
|
3
|
+
import { clsx } from 'clsx';
|
4
|
+
import { type ComponentPropsWithoutRef, forwardRef } from 'react';
|
5
|
+
|
6
|
+
import { buttonRecipe, type ButtonVariants } from './styles.css';
|
7
|
+
|
8
|
+
export type ButtonProps = ComponentPropsWithoutRef<'button'> &
|
9
|
+
ButtonVariants & {
|
10
|
+
css?: string;
|
11
|
+
isPending?: boolean;
|
12
|
+
isDisabled?: boolean;
|
13
|
+
};
|
14
|
+
|
15
|
+
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
16
|
+
({ variant, style, size, fullWidth, isPending = false, isDisabled, css, className, children, onClick }, ref) => {
|
17
|
+
return (
|
18
|
+
<button
|
19
|
+
ref={ref}
|
20
|
+
onClick={onClick}
|
21
|
+
disabled={isDisabled || isPending}
|
22
|
+
className={clsx(buttonRecipe({ variant, style, size, fullWidth }), css, className)}>
|
23
|
+
{isPending ? 'Loading…' : children}
|
24
|
+
</button>
|
25
|
+
);
|
26
|
+
}
|
27
|
+
);
|
28
|
+
|
29
|
+
Button.displayName = 'Button';
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
2
2
|
import React from 'react';
|
3
|
-
import {
|
4
|
-
import {
|
3
|
+
import { Section } from '../Section';
|
4
|
+
import { Button } from '.';
|
5
5
|
|
6
6
|
const meta: Meta<typeof Button> = {
|
7
7
|
title: 'Interactive Components/Button',
|
@@ -74,13 +74,6 @@ export const Secondary: Story = {
|
|
74
74
|
},
|
75
75
|
};
|
76
76
|
|
77
|
-
export const Ghost: Story = {
|
78
|
-
args: {
|
79
|
-
variant: 'ghost',
|
80
|
-
children: 'Ghost Button',
|
81
|
-
},
|
82
|
-
};
|
83
|
-
|
84
77
|
// Size Variants
|
85
78
|
export const Small: Story = {
|
86
79
|
args: {
|
@@ -141,7 +134,6 @@ export const AllVariants: Story = {
|
|
141
134
|
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap', alignItems: 'center' }}>
|
142
135
|
<Button variant="primary">Primary</Button>
|
143
136
|
<Button variant="secondary">Secondary</Button>
|
144
|
-
<Button variant="ghost">Ghost</Button>
|
145
137
|
</div>
|
146
138
|
</Section>
|
147
139
|
),
|
@@ -202,15 +194,12 @@ export const InteractiveExample: Story = {
|
|
202
194
|
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
203
195
|
<Button variant="primary">Save</Button>
|
204
196
|
<Button variant="secondary">Cancel</Button>
|
205
|
-
<Button variant="danger">Delete</Button>
|
206
197
|
</div>
|
207
198
|
</div>
|
208
199
|
|
209
200
|
<div>
|
210
201
|
<h3>Navigation</h3>
|
211
202
|
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
212
|
-
<Button variant="ghost">← Back</Button>
|
213
|
-
<Button variant="ghost">Skip</Button>
|
214
203
|
<Button variant="primary">Next →</Button>
|
215
204
|
</div>
|
216
205
|
</div>
|
@@ -231,15 +220,8 @@ export const WithCustomStyling: Story = {
|
|
231
220
|
render: () => (
|
232
221
|
<Section>
|
233
222
|
<div style={{ display: 'flex', gap: '1rem', flexDirection: 'column' }}>
|
234
|
-
<Button variant="primary"
|
235
|
-
|
236
|
-
</Button>
|
237
|
-
<Button variant="secondary" fontSize="lg" fontWeight="bold">
|
238
|
-
Large Text Button
|
239
|
-
</Button>
|
240
|
-
<Button variant="ghost" boxShadow="lg">
|
241
|
-
Shadow Button
|
242
|
-
</Button>
|
223
|
+
<Button variant="primary">Rounded Button</Button>
|
224
|
+
<Button variant="secondary">Large Text Button</Button>
|
243
225
|
</div>
|
244
226
|
</Section>
|
245
227
|
),
|
@@ -0,0 +1,131 @@
|
|
1
|
+
import { recipe, RecipeVariants } from '@vanilla-extract/recipes';
|
2
|
+
|
3
|
+
import { themeContract } from '../../theme/contract.css';
|
4
|
+
import { generateResponsiveMedia } from '../../utils/generateResponsiveMedia';
|
5
|
+
|
6
|
+
export const buttonRecipe = recipe({
|
7
|
+
base: {
|
8
|
+
border: 'none',
|
9
|
+
outline: 'none',
|
10
|
+
cursor: 'pointer',
|
11
|
+
alignItems: 'center',
|
12
|
+
textDecoration: 'none',
|
13
|
+
display: 'inline-flex',
|
14
|
+
justifyContent: 'center',
|
15
|
+
|
16
|
+
minWidth: themeContract.button.minWidth,
|
17
|
+
fontWeight: themeContract.button.fontWeight,
|
18
|
+
transition: themeContract.button.transition,
|
19
|
+
fontFamily: themeContract.button.fontFamily,
|
20
|
+
borderRadius: themeContract.button.borderRadius,
|
21
|
+
letterSpacing: themeContract.button.letterSpacing,
|
22
|
+
|
23
|
+
':hover': {
|
24
|
+
opacity: '0.8',
|
25
|
+
},
|
26
|
+
|
27
|
+
':active': {},
|
28
|
+
|
29
|
+
':focus-visible': {},
|
30
|
+
|
31
|
+
':disabled': {
|
32
|
+
opacity: '0.5',
|
33
|
+
pointerEvents: 'none',
|
34
|
+
},
|
35
|
+
},
|
36
|
+
|
37
|
+
variants: {
|
38
|
+
variant: {
|
39
|
+
primary: {
|
40
|
+
color: themeContract.button.variant.primary.color,
|
41
|
+
backgroundColor: themeContract.button.variant.primary.backgroundColor,
|
42
|
+
},
|
43
|
+
|
44
|
+
secondary: {
|
45
|
+
color: themeContract.button.variant.secondary.color,
|
46
|
+
backgroundColor: themeContract.button.variant.secondary.backgroundColor,
|
47
|
+
},
|
48
|
+
},
|
49
|
+
|
50
|
+
style: {
|
51
|
+
outline: {
|
52
|
+
backgroundColor: 'transparent',
|
53
|
+
},
|
54
|
+
},
|
55
|
+
|
56
|
+
size: {
|
57
|
+
sm: [
|
58
|
+
{
|
59
|
+
fontSize: themeContract.fontSizes.sm,
|
60
|
+
|
61
|
+
'@media': {
|
62
|
+
...generateResponsiveMedia({
|
63
|
+
paddingTop: themeContract.button.size.small.paddingTop,
|
64
|
+
paddingLeft: themeContract.button.size.small.paddingLeft,
|
65
|
+
paddingRight: themeContract.button.size.small.paddingRight,
|
66
|
+
paddingBottom: themeContract.button.size.small.paddingBottom,
|
67
|
+
}),
|
68
|
+
},
|
69
|
+
},
|
70
|
+
],
|
71
|
+
md: [
|
72
|
+
{
|
73
|
+
fontSize: themeContract.fontSizes.md,
|
74
|
+
|
75
|
+
'@media': {
|
76
|
+
...generateResponsiveMedia({
|
77
|
+
paddingTop: themeContract.button.size.medium.paddingTop,
|
78
|
+
paddingLeft: themeContract.button.size.medium.paddingLeft,
|
79
|
+
paddingRight: themeContract.button.size.medium.paddingRight,
|
80
|
+
paddingBottom: themeContract.button.size.medium.paddingBottom,
|
81
|
+
}),
|
82
|
+
},
|
83
|
+
},
|
84
|
+
],
|
85
|
+
lg: [
|
86
|
+
{
|
87
|
+
fontSize: themeContract.fontSizes.lg,
|
88
|
+
|
89
|
+
'@media': {
|
90
|
+
...generateResponsiveMedia({
|
91
|
+
paddingTop: themeContract.button.size.large.paddingTop,
|
92
|
+
paddingLeft: themeContract.button.size.large.paddingLeft,
|
93
|
+
paddingRight: themeContract.button.size.large.paddingRight,
|
94
|
+
paddingBottom: themeContract.button.size.large.paddingBottom,
|
95
|
+
}),
|
96
|
+
},
|
97
|
+
},
|
98
|
+
],
|
99
|
+
},
|
100
|
+
|
101
|
+
fullWidth: {
|
102
|
+
true: {
|
103
|
+
width: '100%',
|
104
|
+
},
|
105
|
+
},
|
106
|
+
},
|
107
|
+
|
108
|
+
compoundVariants: [
|
109
|
+
{
|
110
|
+
variants: { variant: 'primary', style: 'outline' },
|
111
|
+
style: {
|
112
|
+
color: themeContract.button.variant.primary.backgroundColor,
|
113
|
+
border: `1px solid ${themeContract.button.variant.primary.backgroundColor}`,
|
114
|
+
},
|
115
|
+
},
|
116
|
+
{
|
117
|
+
variants: { variant: 'secondary', style: 'outline' },
|
118
|
+
style: {
|
119
|
+
color: themeContract.button.variant.secondary.backgroundColor,
|
120
|
+
border: `1px solid ${themeContract.button.variant.secondary.backgroundColor}`,
|
121
|
+
},
|
122
|
+
},
|
123
|
+
],
|
124
|
+
|
125
|
+
defaultVariants: {
|
126
|
+
size: 'md',
|
127
|
+
variant: 'primary',
|
128
|
+
},
|
129
|
+
});
|
130
|
+
|
131
|
+
export type ButtonVariants = RecipeVariants<typeof buttonRecipe>;
|
@@ -1,6 +1,7 @@
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
2
2
|
import { clsx } from 'clsx';
|
3
3
|
import { forwardRef, ReactNode, useEffect, useRef, useState } from 'react';
|
4
|
+
|
4
5
|
import {
|
5
6
|
carouselBullet,
|
6
7
|
carouselBulletActive,
|
@@ -12,10 +13,10 @@ import {
|
|
12
13
|
carouselRecipe,
|
13
14
|
carouselSlide,
|
14
15
|
type CarouselVariants,
|
15
|
-
} from './
|
16
|
+
} from './styles.css';
|
17
|
+
|
16
18
|
import { breakpoints } from '../../styles/mediaqueries';
|
17
|
-
import {
|
18
|
-
import { Icon } from '../Icon/Icon';
|
19
|
+
import { Icon } from '../Icon';
|
19
20
|
|
20
21
|
interface UseWindowSizeReturn {
|
21
22
|
width: number | undefined;
|
@@ -45,7 +46,7 @@ const useWindowSize = (): UseWindowSizeReturn => {
|
|
45
46
|
return windowSize;
|
46
47
|
};
|
47
48
|
|
48
|
-
export interface CarouselProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'color' | 'gap'>,
|
49
|
+
export interface CarouselProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'color' | 'gap'>, NonNullable<CarouselVariants> {
|
49
50
|
css?: string;
|
50
51
|
data: ReactNode[];
|
51
52
|
itemsPerView?: number;
|
@@ -53,8 +54,7 @@ export interface CarouselProps extends Omit<React.HTMLAttributes<HTMLDivElement>
|
|
53
54
|
showBullets?: boolean;
|
54
55
|
autoplay?: boolean;
|
55
56
|
autoplayInterval?: number;
|
56
|
-
gap?:
|
57
|
-
as?: 'div' | 'section';
|
57
|
+
gap?: number; // simple px value
|
58
58
|
}
|
59
59
|
|
60
60
|
export const Carousel = forwardRef<HTMLDivElement, CarouselProps>(
|
@@ -67,53 +67,9 @@ export const Carousel = forwardRef<HTMLDivElement, CarouselProps>(
|
|
67
67
|
autoplay = false,
|
68
68
|
autoplayInterval = 3000,
|
69
69
|
gap = 16,
|
70
|
-
|
71
|
-
as: Component = 'div',
|
70
|
+
isFullWidth,
|
72
71
|
css,
|
73
72
|
className,
|
74
|
-
// Extract sprinkles props
|
75
|
-
margin,
|
76
|
-
marginTop,
|
77
|
-
marginBottom,
|
78
|
-
marginLeft,
|
79
|
-
marginRight,
|
80
|
-
padding,
|
81
|
-
paddingTop,
|
82
|
-
paddingBottom,
|
83
|
-
paddingLeft,
|
84
|
-
paddingRight,
|
85
|
-
display,
|
86
|
-
flexDirection,
|
87
|
-
justifyContent,
|
88
|
-
flexWrap,
|
89
|
-
flex,
|
90
|
-
width,
|
91
|
-
height,
|
92
|
-
minWidth,
|
93
|
-
maxWidth,
|
94
|
-
minHeight,
|
95
|
-
position,
|
96
|
-
top,
|
97
|
-
bottom,
|
98
|
-
left,
|
99
|
-
right,
|
100
|
-
zIndex,
|
101
|
-
fontSize,
|
102
|
-
fontFamily,
|
103
|
-
lineHeight,
|
104
|
-
textAlign,
|
105
|
-
fontWeight,
|
106
|
-
color,
|
107
|
-
backgroundColor,
|
108
|
-
borderRadius,
|
109
|
-
borderWidth,
|
110
|
-
borderStyle,
|
111
|
-
borderColor,
|
112
|
-
boxShadow,
|
113
|
-
opacity,
|
114
|
-
overflow,
|
115
|
-
overflowX,
|
116
|
-
overflowY,
|
117
73
|
...htmlProps
|
118
74
|
},
|
119
75
|
ref
|
@@ -129,7 +85,7 @@ export const Carousel = forwardRef<HTMLDivElement, CarouselProps>(
|
|
129
85
|
const isTablet = windowWidth !== undefined && windowWidth > breakpoints.md;
|
130
86
|
const isDesktop = windowWidth !== undefined && windowWidth > breakpoints.lg;
|
131
87
|
|
132
|
-
//
|
88
|
+
// 🔹 Adapter le nombre d’items visibles selon le viewport
|
133
89
|
useEffect(() => {
|
134
90
|
if (isDesktop) {
|
135
91
|
setVisibleItems(itemsPerView);
|
@@ -140,7 +96,7 @@ export const Carousel = forwardRef<HTMLDivElement, CarouselProps>(
|
|
140
96
|
}
|
141
97
|
}, [isTablet, isDesktop, itemsPerView]);
|
142
98
|
|
143
|
-
//
|
99
|
+
// 🔹 Calcul largeur d’un item
|
144
100
|
useEffect(() => {
|
145
101
|
const calculateItemWidth = () => {
|
146
102
|
if (carouselRef.current) {
|
@@ -155,7 +111,7 @@ export const Carousel = forwardRef<HTMLDivElement, CarouselProps>(
|
|
155
111
|
return () => window.removeEventListener('resize', calculateItemWidth);
|
156
112
|
}, [visibleItems, gap]);
|
157
113
|
|
158
|
-
// Autoplay
|
114
|
+
// 🔹 Autoplay
|
159
115
|
useEffect(() => {
|
160
116
|
if (!autoplay) return;
|
161
117
|
|
@@ -169,7 +125,7 @@ export const Carousel = forwardRef<HTMLDivElement, CarouselProps>(
|
|
169
125
|
return () => clearInterval(interval);
|
170
126
|
}, [autoplay, autoplayInterval, data.length, visibleItems]);
|
171
127
|
|
172
|
-
//
|
128
|
+
// 🔹 Swipe mobile
|
173
129
|
useEffect(() => {
|
174
130
|
const carousel = carouselRef.current;
|
175
131
|
if (!carousel) return;
|
@@ -192,10 +148,8 @@ export const Carousel = forwardRef<HTMLDivElement, CarouselProps>(
|
|
192
148
|
|
193
149
|
if (Math.abs(diff) > swipeThreshold) {
|
194
150
|
if (diff > 0) {
|
195
|
-
// Swipe left - next
|
196
151
|
handleNext();
|
197
152
|
} else {
|
198
|
-
// Swipe right - previous
|
199
153
|
handlePrevious();
|
200
154
|
}
|
201
155
|
}
|
@@ -228,65 +182,14 @@ export const Carousel = forwardRef<HTMLDivElement, CarouselProps>(
|
|
228
182
|
const maxIndex = Math.max(0, data.length - visibleItems);
|
229
183
|
|
230
184
|
return (
|
231
|
-
<
|
232
|
-
ref={ref as any}
|
233
|
-
className={clsx(
|
234
|
-
carouselRecipe({ fullWidth }),
|
235
|
-
sprinkles({
|
236
|
-
margin,
|
237
|
-
marginTop,
|
238
|
-
marginBottom,
|
239
|
-
marginLeft,
|
240
|
-
marginRight,
|
241
|
-
padding,
|
242
|
-
paddingTop,
|
243
|
-
paddingBottom,
|
244
|
-
paddingLeft,
|
245
|
-
paddingRight,
|
246
|
-
display,
|
247
|
-
flexDirection,
|
248
|
-
justifyContent,
|
249
|
-
flexWrap,
|
250
|
-
flex,
|
251
|
-
width,
|
252
|
-
height,
|
253
|
-
minWidth,
|
254
|
-
maxWidth,
|
255
|
-
minHeight,
|
256
|
-
position,
|
257
|
-
top,
|
258
|
-
bottom,
|
259
|
-
left,
|
260
|
-
right,
|
261
|
-
zIndex,
|
262
|
-
fontSize,
|
263
|
-
fontFamily,
|
264
|
-
lineHeight,
|
265
|
-
textAlign,
|
266
|
-
fontWeight,
|
267
|
-
color,
|
268
|
-
backgroundColor,
|
269
|
-
borderRadius,
|
270
|
-
borderWidth,
|
271
|
-
borderStyle,
|
272
|
-
borderColor,
|
273
|
-
boxShadow,
|
274
|
-
opacity,
|
275
|
-
overflow,
|
276
|
-
overflowX,
|
277
|
-
overflowY,
|
278
|
-
}),
|
279
|
-
css,
|
280
|
-
className
|
281
|
-
)}
|
282
|
-
{...htmlProps}>
|
185
|
+
<div ref={ref as any} className={clsx(carouselRecipe({ isFullWidth }), css, className)} {...htmlProps}>
|
283
186
|
<div ref={carouselRef} className={carouselContent}>
|
284
187
|
<div
|
285
188
|
ref={slideRef}
|
286
189
|
className={carouselSlide}
|
287
190
|
style={{
|
288
|
-
transform: `translateX(${translateX}px)`,
|
289
191
|
gap: `${gap}px`,
|
192
|
+
transform: `translateX(${translateX}px)`,
|
290
193
|
}}>
|
291
194
|
{data.map((item, index) => (
|
292
195
|
<div key={index} className={carouselItem} style={{ width: `${itemWidth}px` }}>
|
@@ -299,10 +202,10 @@ export const Carousel = forwardRef<HTMLDivElement, CarouselProps>(
|
|
299
202
|
{showNavButtons && (
|
300
203
|
<div className={carouselNav}>
|
301
204
|
<button type="button" className={carouselNavButton} onClick={handlePrevious} disabled={currentIndex === 0} aria-label="Previous slide">
|
302
|
-
<Icon icon="arrowBack"
|
205
|
+
<Icon icon="arrowBack" />
|
303
206
|
</button>
|
304
207
|
<button type="button" className={carouselNavButton} onClick={handleNext} disabled={currentIndex >= maxIndex} aria-label="Next slide">
|
305
|
-
<Icon icon="arrowForward"
|
208
|
+
<Icon icon="arrowForward" />
|
306
209
|
</button>
|
307
210
|
</div>
|
308
211
|
)}
|
@@ -313,14 +216,14 @@ export const Carousel = forwardRef<HTMLDivElement, CarouselProps>(
|
|
313
216
|
<button
|
314
217
|
key={index}
|
315
218
|
type="button"
|
316
|
-
className={clsx(carouselBullet, index === currentIndex && carouselBulletActive)}
|
317
|
-
onClick={() => handleBulletClick(index)}
|
318
219
|
aria-label={`Go to slide ${index + 1}`}
|
220
|
+
onClick={() => handleBulletClick(index)}
|
221
|
+
className={clsx(carouselBullet, index === currentIndex && carouselBulletActive)}
|
319
222
|
/>
|
320
223
|
))}
|
321
224
|
</div>
|
322
225
|
)}
|
323
|
-
</
|
226
|
+
</div>
|
324
227
|
);
|
325
228
|
}
|
326
229
|
);
|