@dcl/asset-packs 2.7.2-20251121153532.commit-10a35d7 → 2.7.2-20251208165801.commit-7c106da
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/.cursor/rules/dcl-styled-components.mdc +849 -0
- package/.cursor/rules/dcl-testing.mdc +356 -0
- package/README.md +9 -0
- package/catalog.json +2555 -0
- package/package.json +4 -3
|
@@ -0,0 +1,849 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Standards for styled-components in Decentraland projects."
|
|
3
|
+
globs: src/**/*.styled.ts,src/**/*.styled.tsx
|
|
4
|
+
alwaysApply: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Styled Components
|
|
8
|
+
|
|
9
|
+
## Scope
|
|
10
|
+
|
|
11
|
+
These standards MUST be applied to all files that match the following patterns, always:
|
|
12
|
+
|
|
13
|
+
- `src/**/*.styled.ts` - TypeScript styled component files
|
|
14
|
+
- `src/**/*.styled.tsx` - TypeScript styled component files with JSX
|
|
15
|
+
|
|
16
|
+
> **Note:** Files must use the `.styled.ts` or `.styled.tsx` naming convention. Other naming patterns are not supported.
|
|
17
|
+
|
|
18
|
+
## Introduction
|
|
19
|
+
|
|
20
|
+
This document describes the standards for styled components in our codebase. These standards ensure consistency and maintainability across our UI components.
|
|
21
|
+
|
|
22
|
+
## Theme Structure
|
|
23
|
+
|
|
24
|
+
The theme in `decentraland-ui2` is built on top of Material-UI's theme system. Understanding the theme structure is crucial for writing consistent styled components.
|
|
25
|
+
|
|
26
|
+
### Theme Source
|
|
27
|
+
|
|
28
|
+
The theme is defined in the `decentraland-ui2` package and follows Material-UI's theme structure. The exact implementation may vary, but the theme provides consistent design tokens across the application.
|
|
29
|
+
|
|
30
|
+
**Reference**: The complete theme configuration can be found in the [decentraland/ui2 repository](https://github.com/decentraland/ui2/tree/master/src/theme), specifically in the [theme index file](https://github.com/decentraland/ui2/blob/master/src/theme/index.ts).
|
|
31
|
+
|
|
32
|
+
**Additional Theme Files**:
|
|
33
|
+
|
|
34
|
+
- [Color Schemes](https://github.com/decentraland/ui2/blob/master/src/theme/colorSchemes.ts) - Defines the color palette for light and dark themes
|
|
35
|
+
- [Typography](https://github.com/decentraland/ui2/blob/master/src/theme/typography.ts) - Defines font families, sizes, and typography variants
|
|
36
|
+
|
|
37
|
+
### Available Theme Properties
|
|
38
|
+
|
|
39
|
+
The theme provides the following design tokens that should be used instead of hardcoded values. For the most up-to-date values, refer to the [theme configuration in the repository](https://github.com/decentraland/ui2/blob/master/src/theme/index.ts):
|
|
40
|
+
|
|
41
|
+
1. **Palette**: Colors for text, background, primary, secondary, etc.
|
|
42
|
+
|
|
43
|
+
- `theme.palette.text.primary` - Main text color
|
|
44
|
+
- `theme.palette.text.secondary` - Secondary text color
|
|
45
|
+
- `theme.palette.background.default` - Default background color
|
|
46
|
+
- `theme.palette.primary.main` - Primary color
|
|
47
|
+
- `theme.palette.secondary.main` - Secondary color
|
|
48
|
+
|
|
49
|
+
**Reference**: See [colorSchemes.ts](https://github.com/decentraland/ui2/blob/master/src/theme/colorSchemes.ts) for the complete color palette definition.
|
|
50
|
+
|
|
51
|
+
2. **Typography**: Predefined text styles
|
|
52
|
+
|
|
53
|
+
- `theme.typography.h1` through `theme.typography.h6` - Headings
|
|
54
|
+
- `theme.typography.body1`, `theme.typography.body2` - Body text
|
|
55
|
+
- `theme.typography.caption` - Caption text
|
|
56
|
+
- `theme.typography.button` - Button text
|
|
57
|
+
|
|
58
|
+
**Reference**: See [typography.ts](https://github.com/decentraland/ui2/blob/master/src/theme/typography.ts) for font families, sizes, and typography variants.
|
|
59
|
+
|
|
60
|
+
3. **Spacing**: Consistent spacing units (base multiplier of 4px)
|
|
61
|
+
|
|
62
|
+
- `theme.spacing(1)` - Small spacing unit (4px)
|
|
63
|
+
- `theme.spacing(2)` - Medium spacing unit (8px)
|
|
64
|
+
- `theme.spacing(3)` - Large spacing unit (12px)
|
|
65
|
+
- `theme.spacing(4)` - Extra large spacing unit (16px)
|
|
66
|
+
|
|
67
|
+
**Reference**: The spacing system follows Material-UI's default spacing function with a base of 4px. For the exact implementation, see [Material-UI spacing documentation](https://mui.com/material-ui/customization/spacing/) and check the spacing configuration in the [theme index file](https://github.com/decentraland/ui2/blob/master/src/theme/index.ts).
|
|
68
|
+
|
|
69
|
+
4. **Breakpoints**: Responsive design breakpoints
|
|
70
|
+
|
|
71
|
+
**Breakpoint Values:**
|
|
72
|
+
|
|
73
|
+
- `xs`: 768px (mobile)
|
|
74
|
+
- `sm`: 991px (tablet)
|
|
75
|
+
- `md`: 1024px (desktop)
|
|
76
|
+
- `lg`: 1280px (large desktop)
|
|
77
|
+
- `xl`: 1500px (extra large desktop)
|
|
78
|
+
|
|
79
|
+
**Usage Examples:**
|
|
80
|
+
|
|
81
|
+
- `theme.breakpoints.down("sm")` - Below 991px
|
|
82
|
+
- `theme.breakpoints.up("md")` - Above 1024px
|
|
83
|
+
- `theme.breakpoints.between("sm", "lg")` - Between 991px and 1280px
|
|
84
|
+
- `theme.breakpoints.only("md")` - Only between 1024px and 1280px
|
|
85
|
+
|
|
86
|
+
5. **Shape**: Common shape properties
|
|
87
|
+
- `theme.shape.borderRadius` - Default border radius
|
|
88
|
+
|
|
89
|
+
**Note**: The exact breakpoint values and other theme properties are defined in the [theme configuration](https://github.com/decentraland/ui2/blob/master/src/theme/index.ts). Always refer to the source for the most current values.
|
|
90
|
+
|
|
91
|
+
### Theme Usage Examples
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
// Using palette colors
|
|
95
|
+
const Title = styled(Typography)(({ theme }) => ({
|
|
96
|
+
color: theme.palette.text.primary, // Main text color
|
|
97
|
+
backgroundColor: theme.palette.background.default,
|
|
98
|
+
}));
|
|
99
|
+
|
|
100
|
+
// Using typography variants
|
|
101
|
+
const Heading = styled(Typography)(({ theme }) => ({
|
|
102
|
+
...theme.typography.h3, // Predefined h3 styles
|
|
103
|
+
}));
|
|
104
|
+
|
|
105
|
+
// Using spacing units
|
|
106
|
+
const Container = styled(Box)(({ theme }) => ({
|
|
107
|
+
padding: theme.spacing(3), // Consistent spacing
|
|
108
|
+
margin: theme.spacing(2), // Consistent spacing
|
|
109
|
+
}));
|
|
110
|
+
|
|
111
|
+
// Using breakpoints
|
|
112
|
+
const ResponsiveBox = styled(Box)(({ theme }) => ({
|
|
113
|
+
[theme.breakpoints.down("sm")]: {
|
|
114
|
+
// Responsive behavior (below 991px)
|
|
115
|
+
flexDirection: "column",
|
|
116
|
+
},
|
|
117
|
+
}));
|
|
118
|
+
|
|
119
|
+
// Using shape properties
|
|
120
|
+
const RoundedCard = styled(Box)(({ theme }) => ({
|
|
121
|
+
borderRadius: theme.shape.borderRadius, // Consistent border radius
|
|
122
|
+
}));
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Import Standards
|
|
126
|
+
|
|
127
|
+
### Import Sources
|
|
128
|
+
|
|
129
|
+
1. MUST use styled components from `decentraland-ui2`:
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
// ❌ Wrong
|
|
133
|
+
import styled from "@emotion/styled";
|
|
134
|
+
import { keyframes } from "@emotion/react";
|
|
135
|
+
|
|
136
|
+
// ✅ Correct
|
|
137
|
+
import { styled, keyframes } from "decentraland-ui2";
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
2. MUST import MUI components from `decentraland-ui2`:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
// ❌ Wrong
|
|
144
|
+
import { Box } from "@mui/material";
|
|
145
|
+
|
|
146
|
+
// ✅ Correct
|
|
147
|
+
import { Box } from "decentraland-ui2";
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
3. MUST order imports alphabetically according to ESLint rules:
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// ❌ Wrong
|
|
154
|
+
import { Modal, Box, styled, keyframes } from "decentraland-ui2";
|
|
155
|
+
|
|
156
|
+
// ✅ Correct
|
|
157
|
+
import { Box, keyframes, Modal, styled } from "decentraland-ui2";
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
4. MUST follow ESLint import order rules:
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
// ✅ Correct import order
|
|
164
|
+
import { Box, Button, styled, Typography } from "decentraland-ui2";
|
|
165
|
+
import { neutral } from "../../theme/colors";
|
|
166
|
+
import { Modal } from "../Modal/Modal";
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Component Definition
|
|
170
|
+
|
|
171
|
+
### Styled Component Structure
|
|
172
|
+
|
|
173
|
+
1. MUST define all styled components as constants:
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
// ❌ Wrong
|
|
177
|
+
export const Container = styled(Box)({...});
|
|
178
|
+
|
|
179
|
+
// ✅ Correct
|
|
180
|
+
const Container = styled(Box)({...});
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### File Extensions
|
|
184
|
+
|
|
185
|
+
1. SHOULD use `.styled.ts` for TypeScript styled component files:
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
// ✅ Correct
|
|
189
|
+
Component.styled.ts
|
|
190
|
+
Component.tsx
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
2. MAY use `.styled.tsx` when styled components include JSX elements:
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
// ✅ Acceptable for complex styled components
|
|
197
|
+
Component.styled.tsx
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
3. MUST group all exports at the end of the file:
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
// ❌ Wrong
|
|
204
|
+
export const Container = styled(Box)({...});
|
|
205
|
+
export const Title = styled(Typography)({...});
|
|
206
|
+
export const Image = styled("img")({...});
|
|
207
|
+
|
|
208
|
+
// ✅ Correct
|
|
209
|
+
const Container = styled(Box)({...});
|
|
210
|
+
const Title = styled(Typography)({...});
|
|
211
|
+
const Image = styled("img")({...});
|
|
212
|
+
|
|
213
|
+
export {
|
|
214
|
+
Container,
|
|
215
|
+
Image,
|
|
216
|
+
Title,
|
|
217
|
+
};
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
4. MUST order exports alphabetically:
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
// ❌ Wrong
|
|
224
|
+
export {
|
|
225
|
+
RewardReachedModal,
|
|
226
|
+
RewardContainer,
|
|
227
|
+
AnimatedBackground,
|
|
228
|
+
RewardImage,
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// ✅ Correct
|
|
232
|
+
export {
|
|
233
|
+
AnimatedBackground,
|
|
234
|
+
RewardContainer,
|
|
235
|
+
RewardImage,
|
|
236
|
+
RewardReachedModal,
|
|
237
|
+
};
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
5. MUST NOT use comments in styled component files:
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
// ❌ Wrong
|
|
244
|
+
/**
|
|
245
|
+
* Component that displays a card with a gradient border
|
|
246
|
+
*/
|
|
247
|
+
const Card = styled(Box)({...});
|
|
248
|
+
|
|
249
|
+
// Layout styles
|
|
250
|
+
const Container = styled(Box)({
|
|
251
|
+
// Position
|
|
252
|
+
position: "relative",
|
|
253
|
+
// Display
|
|
254
|
+
display: "flex",
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// ✅ Correct
|
|
258
|
+
const Card = styled(Box)({...});
|
|
259
|
+
|
|
260
|
+
const Container = styled(Box)({
|
|
261
|
+
position: "relative",
|
|
262
|
+
display: "flex",
|
|
263
|
+
});
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Styling Patterns
|
|
267
|
+
|
|
268
|
+
### Object Syntax Standard
|
|
269
|
+
|
|
270
|
+
UI2 components MUST use the object syntax. This ensures strong TypeScript support, csstype validation, and direct theme integration. Avoid the template literal syntax.
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
// ❌ Wrong
|
|
274
|
+
const Button = styled("button")`
|
|
275
|
+
color: turquoise;
|
|
276
|
+
`;
|
|
277
|
+
|
|
278
|
+
// ✅ Correct
|
|
279
|
+
const Button = styled("button")({
|
|
280
|
+
color: "turquoise",
|
|
281
|
+
});
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
With theme and props:
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
interface Props {
|
|
288
|
+
primary?: boolean;
|
|
289
|
+
}
|
|
290
|
+
const Button = styled("button")<Props>(({ theme, primary }) => ({
|
|
291
|
+
color: primary ? theme.palette.primary.main : theme.palette.text.primary,
|
|
292
|
+
borderRadius: theme.shape.borderRadius,
|
|
293
|
+
}));
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
⚠️ Rule: Values must always come from the UI2 theme. Arbitrary hex codes or pixel values are not allowed.
|
|
297
|
+
|
|
298
|
+
### Allowed Element Syntax
|
|
299
|
+
|
|
300
|
+
When styling native HTML elements, always use the function call form `styled('tag')`. Do not use the property form `styled.tag`, which is legacy syntax and conflicts with our object-style standard.
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
// ❌ Wrong
|
|
304
|
+
const Container = styled.div`
|
|
305
|
+
display: flex;
|
|
306
|
+
`;
|
|
307
|
+
|
|
308
|
+
// ✅ Correct
|
|
309
|
+
const Container = styled("div")({
|
|
310
|
+
display: "flex",
|
|
311
|
+
});
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Specific case: prefer `styled('button')` over `styled.button`
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
// ❌ Wrong
|
|
318
|
+
const Action = styled.button`
|
|
319
|
+
color: red;
|
|
320
|
+
`;
|
|
321
|
+
|
|
322
|
+
// ✅ Correct
|
|
323
|
+
const Action = styled("button")(({ theme }) => ({
|
|
324
|
+
color: theme.palette.text.primary,
|
|
325
|
+
}));
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Component Base
|
|
329
|
+
|
|
330
|
+
1. MUST extend existing UI2 components when possible:
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
// ❌ Wrong
|
|
334
|
+
const Container = styled("div")({...});
|
|
335
|
+
|
|
336
|
+
// ✅ Correct
|
|
337
|
+
const Container = styled(Box)({...});
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
2. MUST use semantic HTML elements for basic elements:
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
// ❌ Wrong
|
|
344
|
+
const Image = styled("div")({...});
|
|
345
|
+
|
|
346
|
+
// ✅ Correct
|
|
347
|
+
const Image = styled("img")({...});
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Props and Types
|
|
351
|
+
|
|
352
|
+
1. MUST type props for styled components that accept them:
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
// ❌ Wrong
|
|
356
|
+
const GradientBorder = styled(Box)(({ completed }) => ({...}));
|
|
357
|
+
|
|
358
|
+
// ✅ Correct
|
|
359
|
+
const GradientBorder = styled(Box)<{ completed: boolean }>(({ completed }) => ({...}));
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
2. MUST type custom properties that do not exist in the base component:
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
// ❌ Wrong
|
|
366
|
+
const Container = styled(Box)(({ theme, imageUrl }) => ({
|
|
367
|
+
backgroundImage: `url(${imageUrl})`,
|
|
368
|
+
}));
|
|
369
|
+
|
|
370
|
+
// ✅ Correct
|
|
371
|
+
const Container = styled(Box)<{ imageUrl: string }>(
|
|
372
|
+
({ theme, imageUrl }) => ({
|
|
373
|
+
backgroundImage: `url(${imageUrl})`,
|
|
374
|
+
})
|
|
375
|
+
);
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
3. SHOULD use theme types when accessing theme properties:
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
381
|
+
// ❌ Wrong
|
|
382
|
+
const Title = styled(Typography)({
|
|
383
|
+
color: "#fff",
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// ✅ Correct
|
|
387
|
+
const Title = styled(Typography)(({ theme }) => ({
|
|
388
|
+
color: theme.palette.text.primary,
|
|
389
|
+
}));
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### Style Organization
|
|
393
|
+
|
|
394
|
+
1. SHOULD group related styles together without comments:
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
// ❌ Wrong
|
|
398
|
+
const Step = styled(Box)(({ theme }) => ({
|
|
399
|
+
// Layout
|
|
400
|
+
display: "flex",
|
|
401
|
+
alignItems: "flex-start",
|
|
402
|
+
|
|
403
|
+
// Visual
|
|
404
|
+
background: "rgba(255, 255, 255, 0.1)",
|
|
405
|
+
borderRadius: "16px",
|
|
406
|
+
|
|
407
|
+
// Spacing
|
|
408
|
+
padding: "24px",
|
|
409
|
+
}));
|
|
410
|
+
|
|
411
|
+
// ✅ Correct
|
|
412
|
+
const Step = styled(Box)(({ theme }) => ({
|
|
413
|
+
display: "flex",
|
|
414
|
+
alignItems: "flex-start",
|
|
415
|
+
background: "rgba(255, 255, 255, 0.1)",
|
|
416
|
+
borderRadius: "16px",
|
|
417
|
+
padding: "24px",
|
|
418
|
+
}));
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
2. SHOULD use theme breakpoints for responsive design:
|
|
422
|
+
|
|
423
|
+
```typescript
|
|
424
|
+
// ❌ Wrong
|
|
425
|
+
const Container = styled(Box)({
|
|
426
|
+
"@media (max-width: 768px)": {
|
|
427
|
+
flexDirection: "column",
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// ✅ Correct
|
|
432
|
+
const Container = styled(Box)(({ theme }) => ({
|
|
433
|
+
[theme.breakpoints.down("sm")]: {
|
|
434
|
+
flexDirection: "column",
|
|
435
|
+
},
|
|
436
|
+
}));
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### Common Patterns
|
|
440
|
+
|
|
441
|
+
1. SHOULD use theme colors and values:
|
|
442
|
+
|
|
443
|
+
```typescript
|
|
444
|
+
// ❌ Wrong
|
|
445
|
+
const Title = styled(Typography)({
|
|
446
|
+
color: "#fff",
|
|
447
|
+
fontSize: "16px",
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
// ✅ Correct
|
|
451
|
+
const Title = styled(Typography)(({ theme }) => ({
|
|
452
|
+
color: theme.palette.text.primary,
|
|
453
|
+
fontSize: theme.typography.body1.fontSize,
|
|
454
|
+
}));
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
2. SHOULD use theme spacing units:
|
|
458
|
+
|
|
459
|
+
```typescript
|
|
460
|
+
// ❌ Wrong
|
|
461
|
+
const Container = styled(Box)({
|
|
462
|
+
padding: "24px",
|
|
463
|
+
margin: "16px",
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
// ✅ Correct
|
|
467
|
+
const Container = styled(Box)(({ theme }) => ({
|
|
468
|
+
padding: theme.spacing(3),
|
|
469
|
+
margin: theme.spacing(2),
|
|
470
|
+
}));
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
3. SHOULD use theme typography variants:
|
|
474
|
+
|
|
475
|
+
```typescript
|
|
476
|
+
// ❌ Wrong
|
|
477
|
+
const Title = styled(Typography)({
|
|
478
|
+
fontSize: "24px",
|
|
479
|
+
fontWeight: 700,
|
|
480
|
+
lineHeight: "32px",
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// ✅ Correct
|
|
484
|
+
const Title = styled(Typography)(({ theme }) => ({
|
|
485
|
+
...theme.typography.h3,
|
|
486
|
+
}));
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
### Complex Styles
|
|
490
|
+
|
|
491
|
+
1. MUST use object syntax with theme values for complex styles:
|
|
492
|
+
|
|
493
|
+
```typescript
|
|
494
|
+
const GradientBorder = styled(Box)<{ completed: boolean }>(
|
|
495
|
+
({ theme, completed }) => ({
|
|
496
|
+
background: completed
|
|
497
|
+
? `linear-gradient(243.96deg, ${theme.palette.primary.main} -11.67%, ${theme.palette.secondary.main} 88.23%)`
|
|
498
|
+
: theme.palette.action.hover,
|
|
499
|
+
})
|
|
500
|
+
);
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
2. SHOULD use theme utilities for common patterns:
|
|
504
|
+
|
|
505
|
+
```typescript
|
|
506
|
+
const Card = styled(Box)(({ theme }) => ({
|
|
507
|
+
display: "flex",
|
|
508
|
+
alignItems: "center",
|
|
509
|
+
justifyContent: "center",
|
|
510
|
+
padding: theme.spacing(2),
|
|
511
|
+
backgroundColor: theme.palette.background.paper,
|
|
512
|
+
borderRadius: theme.shape.borderRadius,
|
|
513
|
+
}));
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
3. MUST avoid !important declarations. Use better CSS specificity instead:
|
|
517
|
+
|
|
518
|
+
```typescript
|
|
519
|
+
// ❌ Wrong
|
|
520
|
+
const StatLabel = styled(Typography)(({ theme }) => ({
|
|
521
|
+
"&&": {
|
|
522
|
+
color: "rgba(255, 255, 255, 0.75) !important",
|
|
523
|
+
},
|
|
524
|
+
}));
|
|
525
|
+
|
|
526
|
+
// ✅ Correct
|
|
527
|
+
const StatLabel = styled(Typography)(({ theme }) => ({
|
|
528
|
+
color: theme.palette.text.secondary,
|
|
529
|
+
opacity: 0.75,
|
|
530
|
+
}));
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
### No Inline Styles
|
|
534
|
+
|
|
535
|
+
Inline styles (style= ... ) MUST NOT be used in UI2 components. They bypass theme typing, make styles harder to maintain, and prevent reusability. If a style value needs to be dynamic, pass it as a typed prop to the styled component.
|
|
536
|
+
|
|
537
|
+
```typescript
|
|
538
|
+
// ❌ Wrong
|
|
539
|
+
<Card key={id} style={{ backgroundColor: color } as React.CSSProperties}>
|
|
540
|
+
<CardHeader>
|
|
541
|
+
<Title style={{ fontSize: 20 }}>{title}</Title>
|
|
542
|
+
</CardHeader>
|
|
543
|
+
</Card>
|
|
544
|
+
|
|
545
|
+
// ✅ Correct
|
|
546
|
+
interface CardProps { color: string }
|
|
547
|
+
const Card = styled(Box)<CardProps>(({ color }) => ({ backgroundColor: color }))
|
|
548
|
+
const Title = styled('h2')<CardProps>(({ fontSize }) => ({ fontSize }))
|
|
549
|
+
|
|
550
|
+
<Card key={id} color={color}>
|
|
551
|
+
<CardHeader>
|
|
552
|
+
<Title fontSize={20}>{title}</Title>
|
|
553
|
+
</CardHeader>
|
|
554
|
+
</Card>
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
## Best Practices
|
|
558
|
+
|
|
559
|
+
### Breakpoints
|
|
560
|
+
|
|
561
|
+
Use theme.breakpoints helpers instead of hardcoded pixel values. Always use up, down, between, or only.
|
|
562
|
+
|
|
563
|
+
```typescript
|
|
564
|
+
// ❌ Wrong
|
|
565
|
+
const Panel = styled("div")`
|
|
566
|
+
@media (max-width: 768px) {
|
|
567
|
+
width: 100%;
|
|
568
|
+
}
|
|
569
|
+
`;
|
|
570
|
+
|
|
571
|
+
// ✅ Correct
|
|
572
|
+
interface PanelProps {
|
|
573
|
+
expanded: boolean;
|
|
574
|
+
}
|
|
575
|
+
const Panel = styled("div")<PanelProps>(({ theme, expanded }) => ({
|
|
576
|
+
width: expanded ? "400px" : "0",
|
|
577
|
+
[theme.breakpoints.down("sm")]: { width: expanded ? "100%" : "0" },
|
|
578
|
+
}));
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
### Spacing Scale and theme.spacing
|
|
582
|
+
|
|
583
|
+
Use theme.spacing exclusively for margins, paddings, and gaps.
|
|
584
|
+
|
|
585
|
+
- Convention: theme.spacing(n) where n is an integer. If the base is 4, 1 → 4px and 2 → 8px.
|
|
586
|
+
- Do not use raw px values. If a new value is needed, propose it in the theme.
|
|
587
|
+
|
|
588
|
+
```typescript
|
|
589
|
+
// ✅ Correct
|
|
590
|
+
const Box = styled("div")(({ theme }) => ({
|
|
591
|
+
padding: theme.spacing(2),
|
|
592
|
+
gap: theme.spacing(3),
|
|
593
|
+
}));
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
### Z-index, elevation, and stacking context
|
|
597
|
+
|
|
598
|
+
Follow the theme scale: theme.zIndex.appBar, drawer, modal, tooltip. Avoid ad‑hoc z-index. If a new layer is needed, add it to the theme first. Manage stacking context consciously and avoid unnecessary position: relative on parents.
|
|
599
|
+
|
|
600
|
+
```typescript
|
|
601
|
+
// ✅ Correct
|
|
602
|
+
const StickyBar = styled("div")(({ theme }) => ({
|
|
603
|
+
position: "sticky",
|
|
604
|
+
top: 0,
|
|
605
|
+
zIndex: theme.zIndex.appBar,
|
|
606
|
+
}));
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
### Focus visible and interactive states
|
|
610
|
+
|
|
611
|
+
All interactive controls MUST show a visible focus and declare hover, active, and disabled states.
|
|
612
|
+
|
|
613
|
+
```typescript
|
|
614
|
+
// ✅ Correct
|
|
615
|
+
const Action = styled("button")(({ theme }) => ({
|
|
616
|
+
outline: "none",
|
|
617
|
+
"&:focus-visible": {
|
|
618
|
+
outline: `2px solid ${theme.palette.primary.main}`,
|
|
619
|
+
outlineOffset: 2,
|
|
620
|
+
},
|
|
621
|
+
"&:hover": { backgroundColor: theme.palette.action.hover },
|
|
622
|
+
"&:active": { backgroundColor: theme.palette.action.selected },
|
|
623
|
+
"&:disabled": { opacity: 0.5, pointerEvents: "none" },
|
|
624
|
+
}));
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
### Performance with styled
|
|
628
|
+
|
|
629
|
+
- Do not create styled components inside render paths.
|
|
630
|
+
- Memoize derived props that trigger restyling.
|
|
631
|
+
- Do not pass inline object props. Prefer primitive props and map them inside styled.
|
|
632
|
+
|
|
633
|
+
```typescript
|
|
634
|
+
// ❌ Wrong
|
|
635
|
+
const MyComponent = ({ color, ...props }) => {
|
|
636
|
+
const StyledDiv = styled("div")(({ theme }) => ({ color })); // Created in render
|
|
637
|
+
return <StyledDiv style={{ backgroundColor: color }} {...props} />;
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
// ✅ Correct
|
|
641
|
+
const StyledDiv = styled("div")<{ color: string }>(({ color }) => ({ color }));
|
|
642
|
+
|
|
643
|
+
const MyComponent = ({ color, ...props }) => {
|
|
644
|
+
return <StyledDiv color={color} {...props} />;
|
|
645
|
+
};
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
### File Organization
|
|
649
|
+
|
|
650
|
+
1. MUST keep styled components in separate files:
|
|
651
|
+
|
|
652
|
+
```
|
|
653
|
+
Component.tsx
|
|
654
|
+
Component.styled.ts
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
2. MUST use meaningful names that reflect the component's purpose:
|
|
658
|
+
|
|
659
|
+
```typescript
|
|
660
|
+
// ❌ Wrong
|
|
661
|
+
const Div = styled(Box)({...});
|
|
662
|
+
const Wrapper = styled(Box)({...});
|
|
663
|
+
|
|
664
|
+
// ✅ Correct
|
|
665
|
+
const CardContainer = styled(Box)({...});
|
|
666
|
+
const RewardImage = styled("img")({...});
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
3. MUST use consistent naming conventions:
|
|
670
|
+
- Use PascalCase for component names
|
|
671
|
+
- Use camelCase for style properties
|
|
672
|
+
- Use kebab-case for HTML attributes
|
|
673
|
+
```typescript
|
|
674
|
+
const RewardCardContainer = styled(Box)(({ theme }) => ({
|
|
675
|
+
backgroundColor: theme.palette.background.paper,
|
|
676
|
+
"data-testid": "reward-card",
|
|
677
|
+
}));
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
## Examples
|
|
681
|
+
|
|
682
|
+
### Basic Styled Component
|
|
683
|
+
|
|
684
|
+
```typescript
|
|
685
|
+
import { Box, styled, Typography } from "decentraland-ui2";
|
|
686
|
+
|
|
687
|
+
const CardContainer = styled(Box)(({ theme }) => ({
|
|
688
|
+
display: "flex",
|
|
689
|
+
flexDirection: "column",
|
|
690
|
+
padding: theme.spacing(3),
|
|
691
|
+
backgroundColor: theme.palette.background.default,
|
|
692
|
+
borderRadius: theme.shape.borderRadius,
|
|
693
|
+
}));
|
|
694
|
+
|
|
695
|
+
const CardTitle = styled(Typography)(({ theme }) => ({
|
|
696
|
+
...theme.typography.h4,
|
|
697
|
+
color: theme.palette.text.primary,
|
|
698
|
+
marginBottom: theme.spacing(2),
|
|
699
|
+
}));
|
|
700
|
+
|
|
701
|
+
const CardContent = styled(Box)(({ theme }) => ({
|
|
702
|
+
color: theme.palette.text.secondary,
|
|
703
|
+
...theme.typography.body1,
|
|
704
|
+
}));
|
|
705
|
+
|
|
706
|
+
export { CardContainer, CardContent, CardTitle };
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
### Styled Component with Props
|
|
710
|
+
|
|
711
|
+
```typescript
|
|
712
|
+
import { Box, styled } from "decentraland-ui2";
|
|
713
|
+
|
|
714
|
+
const GradientButton = styled(Box)<{ variant: "primary" | "secondary" }>(
|
|
715
|
+
({ theme, variant }) => ({
|
|
716
|
+
display: "flex",
|
|
717
|
+
alignItems: "center",
|
|
718
|
+
justifyContent: "center",
|
|
719
|
+
padding: theme.spacing(2),
|
|
720
|
+
borderRadius: theme.shape.borderRadius,
|
|
721
|
+
cursor: "pointer",
|
|
722
|
+
backgroundColor:
|
|
723
|
+
variant === "primary"
|
|
724
|
+
? theme.palette.primary.main
|
|
725
|
+
: theme.palette.secondary.main,
|
|
726
|
+
color: theme.palette.common.white,
|
|
727
|
+
"&:hover": {
|
|
728
|
+
opacity: 0.8,
|
|
729
|
+
},
|
|
730
|
+
})
|
|
731
|
+
);
|
|
732
|
+
|
|
733
|
+
const ResponsiveContainer = styled(Box)(({ theme }) => ({
|
|
734
|
+
maxWidth: "1200px",
|
|
735
|
+
margin: "0 auto",
|
|
736
|
+
padding: theme.spacing(3),
|
|
737
|
+
[theme.breakpoints.down("sm")]: {
|
|
738
|
+
padding: theme.spacing(2),
|
|
739
|
+
},
|
|
740
|
+
}));
|
|
741
|
+
|
|
742
|
+
export { GradientButton, ResponsiveContainer };
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
### Complex Styled Component
|
|
746
|
+
|
|
747
|
+
```typescript
|
|
748
|
+
import { Box, styled } from "decentraland-ui2";
|
|
749
|
+
|
|
750
|
+
const AnimatedCard = styled(Box)<{ isVisible: boolean }>(
|
|
751
|
+
({ theme, isVisible }) => ({
|
|
752
|
+
display: "flex",
|
|
753
|
+
flexDirection: "column",
|
|
754
|
+
padding: theme.spacing(3),
|
|
755
|
+
backgroundColor: theme.palette.background.paper,
|
|
756
|
+
borderRadius: theme.shape.borderRadius,
|
|
757
|
+
boxShadow: theme.shadows[1],
|
|
758
|
+
transform: isVisible ? "translateY(0)" : "translateY(20px)",
|
|
759
|
+
opacity: isVisible ? 1 : 0,
|
|
760
|
+
transition: "all 0.3s ease-in-out",
|
|
761
|
+
"&:hover": {
|
|
762
|
+
boxShadow: theme.shadows[4],
|
|
763
|
+
transform: "translateY(-2px)",
|
|
764
|
+
},
|
|
765
|
+
})
|
|
766
|
+
);
|
|
767
|
+
|
|
768
|
+
const StatusBadge = styled(Box)<{ status: "success" | "warning" | "error" }>(
|
|
769
|
+
({ theme, status }) => ({
|
|
770
|
+
display: "inline-flex",
|
|
771
|
+
alignItems: "center",
|
|
772
|
+
padding: `${theme.spacing(0.5)} ${theme.spacing(1)}`,
|
|
773
|
+
borderRadius: theme.shape.borderRadius,
|
|
774
|
+
fontSize: theme.typography.caption.fontSize,
|
|
775
|
+
fontWeight: theme.typography.caption.fontWeight,
|
|
776
|
+
backgroundColor:
|
|
777
|
+
status === "success"
|
|
778
|
+
? theme.palette.success.main
|
|
779
|
+
: status === "warning"
|
|
780
|
+
? theme.palette.warning.main
|
|
781
|
+
: theme.palette.error.main,
|
|
782
|
+
color: theme.palette.common.white,
|
|
783
|
+
})
|
|
784
|
+
);
|
|
785
|
+
|
|
786
|
+
export { AnimatedCard, StatusBadge };
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
## Validation Checklist
|
|
790
|
+
|
|
791
|
+
Before creating styled components, ensure:
|
|
792
|
+
|
|
793
|
+
- [ ] MUST and SHOULD sections are clearly defined
|
|
794
|
+
- [ ] All imports are from `decentraland-ui2`
|
|
795
|
+
- [ ] All MUI components are imported from `decentraland-ui2`
|
|
796
|
+
- [ ] Styled components use `styled` from `decentraland-ui2`
|
|
797
|
+
- [ ] Imports are ordered alphabetically according to ESLint rules
|
|
798
|
+
- [ ] Object syntax is used for all styled components
|
|
799
|
+
- [ ] Template literal syntax is avoided
|
|
800
|
+
- [ ] Function call form `styled('tag')` is used for HTML elements
|
|
801
|
+
- [ ] No !important declarations are used
|
|
802
|
+
- [ ] CSS specificity is properly managed
|
|
803
|
+
- [ ] Inline styles are avoided
|
|
804
|
+
- [ ] No inline comments are used in styled component files
|
|
805
|
+
- [ ] Dynamic values are passed as typed props
|
|
806
|
+
- [ ] Styled components are defined as constants
|
|
807
|
+
- [ ] All exports are grouped at the end of the file
|
|
808
|
+
- [ ] Exports are ordered alphabetically
|
|
809
|
+
- [ ] Props are properly typed for styled components
|
|
810
|
+
- [ ] Theme properties are used instead of hardcoded values
|
|
811
|
+
- [ ] Theme breakpoints are used for responsive design
|
|
812
|
+
- [ ] Theme spacing units are used for consistent spacing
|
|
813
|
+
- [ ] Theme typography variants are used for text styles
|
|
814
|
+
- [ ] Z-index values come from theme.zIndex scale
|
|
815
|
+
- [ ] Interactive states (hover, focus, active, disabled) are implemented
|
|
816
|
+
- [ ] Focus-visible styles are included for accessibility
|
|
817
|
+
- [ ] Performance best practices are followed
|
|
818
|
+
- [ ] Styled components are not created in render paths
|
|
819
|
+
- [ ] Primitive props are preferred over object props
|
|
820
|
+
- [ ] Component names are meaningful and follow PascalCase
|
|
821
|
+
- [ ] Style properties use camelCase
|
|
822
|
+
- [ ] HTML attributes use kebab-case
|
|
823
|
+
- [ ] Theme colors use palette instead of arbitrary hex values
|
|
824
|
+
- [ ] Breakpoint helpers (up, down, between) are used instead of media queries
|
|
825
|
+
- [ ] File organization follows Component.tsx / Component.styled.ts pattern
|
|
826
|
+
- [ ] Specific code examples are included
|
|
827
|
+
- [ ] Incorrect (❌) and correct (✅) patterns are shown
|
|
828
|
+
- [ ] Rules are specific and actionable
|
|
829
|
+
- [ ] Scope clearly defines file patterns
|
|
830
|
+
- [ ] Description is concise and descriptive
|
|
831
|
+
- [ ] Glob patterns are appropriate for frontend
|
|
832
|
+
- [ ] Rules follow the established format and structure
|
|
833
|
+
- [ ] Content is comprehensive and well-organized
|
|
834
|
+
- [ ] Examples are relevant and demonstrate best practices
|
|
835
|
+
- [ ] Theme integration is properly implemented
|
|
836
|
+
- [ ] Responsive design patterns are followed
|
|
837
|
+
- [ ] Component reusability is maximized
|
|
838
|
+
- [ ] Performance considerations are addressed
|
|
839
|
+
- [ ] Accessibility requirements are met
|
|
840
|
+
- [ ] Interactive states documentation is complete
|
|
841
|
+
- [ ] Object syntax standards are enforced
|
|
842
|
+
- [ ] Element syntax standards are enforced
|
|
843
|
+
- [ ] No inline styles prohibition is enforced
|
|
844
|
+
- [ ] Rules are independent and don't share state
|
|
845
|
+
- [ ] Prefer specific outcomes over generic ones
|
|
846
|
+
- [ ] Context is properly defined and not repeated
|
|
847
|
+
- [ ] Each section has appropriate setup and cleanup
|
|
848
|
+
|
|
849
|
+
**Purpose**: The Validation Checklist serves as a quality gate for all styled components, ensuring they meet the established standards for consistency, maintainability, performance, and accessibility. It helps maintain consistency across all UI components and provides a clear framework for developers to follow.
|