@rescui/card 0.14.0 → 0.14.1-RUI-293-Update-menu-component-7a032357e.20

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/llm/cards.md ADDED
@@ -0,0 +1,3650 @@
1
+ # Cards
2
+
3
+ Cards Catalog contains a large variety of card snippets to show different ways of building any type of card from scratch
4
+
5
+ ## Cards
6
+
7
+ ### Main types
8
+
9
+ In most cases, using the [cardCn className helper](/components/card-cn/) will be sufficient
10
+
11
+ Refer to [this guide](/components/card-cn/) to learn more about differences between cardCn and Cards Catalog
12
+
13
+ If you need more control over the card, you can use the following snippets, as well as a [step-by-step guide](/cards/guide/), to create a custom card from scratch:
14
+
15
+ #### Clickable cards
16
+
17
+ Cards with hover, active and focus states
18
+
19
+ [Open page](/cards/clickable/)
20
+
21
+ #### Tiles
22
+
23
+ Non-clickable cards
24
+
25
+ [Open page](/cards/tiles/)
26
+
27
+ ### Other resources
28
+
29
+ #### Controls
30
+
31
+ Stateful carousel buttons, slider controls, etc.
32
+
33
+ [Open page](/cards/controls/)
34
+
35
+ #### Step-by-step guide
36
+
37
+ A description of what you should pay attention to when creating your own cards
38
+
39
+ [Open page](/cards/guide/)
40
+
41
+ #### cardCn
42
+
43
+ A ClassName Helper for creating cards with simple styles and some customizations
44
+
45
+ [Open page](/components/card-cn/)
46
+
47
+ ## Clickable
48
+
49
+ ### Base example
50
+
51
+ ##### Details:
52
+
53
+ - Base example
54
+ - Using css variables to control the border
55
+ - Set border on the pseudo-element to avoid scaling bug when changing `border-width`
56
+
57
+ - [cardCn implementation](/components/card-cn/#default-clickable-card)
58
+
59
+ ```tsx
60
+ import cn from 'classnames';
61
+ import { textCn } from '@rescui/typography';
62
+
63
+ const Demo = () => {
64
+ return (
65
+ <button type="button" className="base-card" style={{ maxWidth: 400 }}>
66
+ <h3 className={textCn('rs-h3')}>Card title</h3>
67
+ <p className={cn(textCn('rs-text-2'), 'rs-docs-offset-top-24')}>
68
+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab animi
69
+ aspernatur autem commodi consequuntur esse laudantium nobis numquam
70
+ officiis omnis, perferendis perspiciatis, possimus qui quidem, quis
71
+ repellat tempore unde veniam?
72
+ </p>
73
+ </button>
74
+ );
75
+ };
76
+
77
+ render(<Demo />);
78
+ ```
79
+
80
+ ```postcss
81
+ .base-card {
82
+ --my-card-border-width: 1px;
83
+ --my-card-border-color: var(--rs-color-black-t20);
84
+
85
+ position: relative;
86
+
87
+ display: block;
88
+
89
+ overflow: hidden;
90
+
91
+ box-sizing: border-box;
92
+ min-height: 0;
93
+
94
+ border: none;
95
+ border-radius: 8px;
96
+
97
+ cursor: pointer;
98
+ text-align: inherit;
99
+
100
+ background-color: var(--rs-color-white);
101
+ box-shadow: inset 0 0 0 var(--my-card-border-width) var(
102
+ --my-card-border-color
103
+ );
104
+
105
+ transition: box-shadow 100ms;
106
+ appearance: none;
107
+ padding: 24px;
108
+
109
+ &,
110
+ &:hover {
111
+ text-decoration: none;
112
+ }
113
+
114
+ /* reset user agent stylesheet at Chrome */
115
+ &:focus-visible {
116
+ outline: none;
117
+ }
118
+
119
+ @media (hover: hover) {
120
+ &:hover {
121
+ --my-card-border-color: var(--rs-color-black);
122
+ }
123
+ }
124
+
125
+ &:focus-visible,
126
+ &:active {
127
+ --my-card-border-color: var(--rs-color-black);
128
+ }
129
+
130
+ &:active {
131
+ --my-card-border-width: 2px;
132
+ }
133
+ }
134
+ ```
135
+
136
+ ### Dark theme
137
+
138
+ ##### Details:
139
+
140
+ - Dark theme
141
+ - Using css variables to control the border
142
+ - Set border on the pseudo-element to avoid scaling bug when changing `border-width`
143
+
144
+ - [cardCn implementation](/components/card-cn/#theme-dark)
145
+
146
+ ```tsx
147
+ import cn from 'classnames';
148
+ import { textCn } from '@rescui/typography';
149
+
150
+ const Demo = () => {
151
+ return (
152
+ <div className="dark-wrapper">
153
+ <button type="button" className="dark-card" style={{ maxWidth: 400 }}>
154
+ <h3 className={textCn('rs-h3')}>Card title</h3>
155
+ <p className={cn(textCn('rs-text-2'), 'rs-docs-offset-top-24')}>
156
+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab animi
157
+ aspernatur autem commodi consequuntur esse laudantium nobis numquam
158
+ officiis omnis, perferendis perspiciatis, possimus qui quidem, quis
159
+ repellat tempore unde veniam?
160
+ </p>
161
+ </button>
162
+ </div>
163
+ );
164
+ };
165
+
166
+ render(<Demo />);
167
+ ```
168
+
169
+ ```postcss
170
+ .dark-wrapper {
171
+ --rs-theme-dark: 1;
172
+ padding: 48px;
173
+ background: var(--rs-color-black);
174
+ }
175
+
176
+ .dark-card {
177
+ --my-card-border-width: 1px;
178
+ --my-card-border-color: var(--rs-color-white-t20);
179
+
180
+ position: relative;
181
+
182
+ display: block;
183
+
184
+ overflow: hidden;
185
+
186
+ box-sizing: border-box;
187
+ min-height: 0;
188
+
189
+ border: none;
190
+ border-radius: 8px;
191
+
192
+ cursor: pointer;
193
+ text-align: inherit;
194
+
195
+ background-color: var(--rs-color-grey-90);
196
+ box-shadow: inset 0 0 0 var(--my-card-border-width) var(
197
+ --my-card-border-color
198
+ );
199
+
200
+ transition: box-shadow 100ms;
201
+ appearance: none;
202
+ padding: 24px;
203
+
204
+ &,
205
+ &:hover {
206
+ text-decoration: none;
207
+ }
208
+
209
+ /* reset user agent stylesheet at Chrome */
210
+ &:focus-visible {
211
+ outline: none;
212
+ }
213
+
214
+ @media (hover: hover) {
215
+ &:hover {
216
+ --my-card-border-color: var(--rs-color-white);
217
+ }
218
+ }
219
+
220
+ &:focus-visible,
221
+ &:active {
222
+ --my-card-border-color: var(--rs-color-white);
223
+ }
224
+
225
+ &:active {
226
+ --my-card-border-width: 2px;
227
+ }
228
+ }
229
+ ```
230
+
231
+ ### Borderless example
232
+
233
+ ##### Details:
234
+
235
+ - Borderless example
236
+ - States change background color
237
+ - [cardCn implementation](/components/card-cn/css/#borderless-card)
238
+
239
+ ```tsx
240
+ import cn from 'classnames';
241
+ import { textCn } from '@rescui/typography';
242
+
243
+ const Demo = () => {
244
+ return (
245
+ <button type="button" className="borderless-card" style={{ maxWidth: 400 }}>
246
+ <h3 className={textCn('rs-h3')}>Card title</h3>
247
+ <p className={cn(textCn('rs-text-2'), 'rs-docs-offset-top-24')}>
248
+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab animi
249
+ aspernatur autem commodi consequuntur esse laudantium nobis numquam
250
+ officiis omnis, perferendis perspiciatis, possimus qui quidem, quis
251
+ repellat tempore unde veniam?
252
+ </p>
253
+ </button>
254
+ );
255
+ };
256
+
257
+ render(<Demo />);
258
+ ```
259
+
260
+ ```postcss
261
+ .borderless-card {
262
+ display: block;
263
+
264
+ overflow: hidden;
265
+
266
+ box-sizing: border-box;
267
+ min-height: 0;
268
+
269
+ border: none;
270
+ border-radius: 8px;
271
+
272
+ cursor: pointer;
273
+ text-align: inherit;
274
+
275
+ background-color: var(--rs-color-grey-5);
276
+
277
+ transition: background-color 100ms;
278
+ appearance: none;
279
+ padding: 24px;
280
+
281
+ &,
282
+ &:hover {
283
+ text-decoration: none;
284
+ }
285
+
286
+ /* reset user agent stylesheet at Chrome */
287
+ &:focus-visible {
288
+ outline: none;
289
+ }
290
+
291
+ @media (hover: hover) {
292
+ &:hover {
293
+ background-color: var(--rs-color-grey-10);
294
+ }
295
+ }
296
+
297
+ &:focus-visible {
298
+ background-color: var(--rs-color-grey-10);
299
+ }
300
+
301
+ &:active {
302
+ background-color: var(--rs-color-grey-20);
303
+ }
304
+ }
305
+ ```
306
+
307
+ ### With glow hover
308
+
309
+ ##### Details:
310
+
311
+ - Borderless example with `glow-hover` effect
312
+ - States don't change anything in css
313
+ - Hover and active states are covered by `glow-hover`
314
+ - [cardCn implementation](/components/card-cn/#with-glow-hover)
315
+
316
+ ```tsx
317
+ import cn from 'classnames';
318
+ import { textCn } from '@rescui/typography';
319
+ import { useGlowHover } from '@rescui/use-glow-hover';
320
+
321
+ const Demo = () => {
322
+ const ref = useGlowHover({ preset: 'transparent-blue', enableBurst: true });
323
+ return (
324
+ <button
325
+ type="button"
326
+ ref={ref}
327
+ className="borderless-with-glow-hover-card"
328
+ style={{ maxWidth: 400 }}
329
+ >
330
+ <h3 className={textCn('rs-h3')}>Card title</h3>
331
+ <p className={cn(textCn('rs-text-2'), 'rs-docs-offset-top-24')}>
332
+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab animi
333
+ aspernatur autem commodi consequuntur esse laudantium nobis numquam
334
+ officiis omnis, perferendis perspiciatis, possimus qui quidem, quis
335
+ repellat tempore unde veniam?
336
+ </p>
337
+ </button>
338
+ );
339
+ };
340
+
341
+ render(<Demo />);
342
+ ```
343
+
344
+ ```postcss
345
+ .borderless-with-glow-hover-card {
346
+ display: block;
347
+
348
+ overflow: hidden;
349
+
350
+ box-sizing: border-box;
351
+ min-height: 0;
352
+
353
+ border: none;
354
+ border-radius: 8px;
355
+
356
+ cursor: pointer;
357
+ text-align: inherit;
358
+
359
+ background-color: var(--rs-color-grey-5);
360
+
361
+ transition: background-color 100ms;
362
+ appearance: none;
363
+ padding: 24px;
364
+
365
+ &,
366
+ &:hover {
367
+ text-decoration: none;
368
+ }
369
+
370
+ /* reset user agent stylesheet at Chrome */
371
+ &:focus-visible {
372
+ outline: none;
373
+ }
374
+
375
+ &:focus-visible {
376
+ background-color: var(--rs-color-grey-10);
377
+ }
378
+ }
379
+ ```
380
+
381
+ ### With action button inside
382
+
383
+ ##### Details:
384
+
385
+ - With border via pseudo-element `::before`
386
+
387
+ - Forward states to the `Button` inside
388
+
389
+ - Card should have the `button` HTML tag
390
+
391
+ - Main action, like `onClick`, should be on the card element
392
+
393
+ - Use `notFocusable` property with `Button`
394
+
395
+ - Use only one action element inside
396
+ - [cardCn implementation](/components/card-cn/#with-action-button-inside)
397
+
398
+ ```tsx
399
+ import cn from 'classnames';
400
+ import { textCn } from '@rescui/typography';
401
+ import { Button } from '@rescui/button';
402
+
403
+ const Demo = () => {
404
+ return (
405
+ <button
406
+ type="button"
407
+ onClick={() => console.log('Card onClick called')}
408
+ className="action-button-inside-card"
409
+ style={{ maxWidth: 400 }}
410
+ >
411
+ <h3 className={textCn('rs-h3')}>Card title</h3>
412
+ <p className={cn(textCn('rs-text-2'), 'rs-docs-offset-top-24')}>
413
+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab animi
414
+ aspernatur autem commodi consequuntur esse laudantium nobis numquam
415
+ officiis omnis, perferendis perspiciatis, possimus qui quidem, quis
416
+ repellat tempore unde veniam?
417
+ </p>
418
+ <div className="rs-docs-offset-top-24">
419
+ <Button notFocusable className="action-button-inside-card__button">
420
+ Click me
421
+ </Button>
422
+ </div>
423
+ </button>
424
+ );
425
+ };
426
+
427
+ render(<Demo />);
428
+ ```
429
+
430
+ ```postcss
431
+ @import '@rescui/button/lib/public-api.p.css';
432
+
433
+ .action-button-inside-card {
434
+ --my-card-border-width: 1px;
435
+ --my-card-border-color: var(--rs-color-black-t20);
436
+
437
+ position: relative;
438
+
439
+ display: block;
440
+
441
+ overflow: hidden;
442
+
443
+ box-sizing: border-box;
444
+ min-height: 0;
445
+
446
+ border: none;
447
+ border-radius: 8px;
448
+
449
+ cursor: pointer;
450
+ text-align: inherit;
451
+
452
+ background-color: var(--rs-color-white);
453
+ box-shadow: inset 0 0 0 var(--my-card-border-width) var(
454
+ --my-card-border-color
455
+ );
456
+
457
+ transition: box-shadow 100ms;
458
+ appearance: none;
459
+ padding: 24px;
460
+
461
+ &,
462
+ &:hover {
463
+ text-decoration: none;
464
+ }
465
+
466
+ /* reset user agent stylesheet at Chrome */
467
+ &:focus-visible {
468
+ outline: none;
469
+ }
470
+
471
+ @media (hover: hover) {
472
+ &:hover {
473
+ --my-card-border-color: var(--rs-color-black);
474
+ }
475
+ }
476
+
477
+ &:focus-visible,
478
+ &:active {
479
+ --my-card-border-color: var(--rs-color-black);
480
+ }
481
+
482
+ &:active {
483
+ --my-card-border-width: 2px;
484
+ }
485
+
486
+ @media (hover: hover) {
487
+ &:hover &__button {
488
+ @mixin rs-button-state-hover;
489
+ }
490
+ }
491
+
492
+ &:active &__button {
493
+ @mixin rs-button-state-active;
494
+ }
495
+
496
+ &:focus-visible &__button {
497
+ @mixin rs-button-state-focus-visible;
498
+ }
499
+
500
+ &:active:focus-visible:not(:hover) &__button {
501
+ @mixin rs-button-state-active-focus-visible-no-hover;
502
+ }
503
+ }
504
+ ```
505
+
506
+ ### With action link inside
507
+
508
+ ##### Details:
509
+
510
+ - With border via pseudo-element `::before`
511
+
512
+ - Forward states to the `rs-link` inside
513
+
514
+ - Card should have the `a` HTML tag
515
+
516
+ - Main action, like `href`, should be on the card element
517
+
518
+ - `rs-link` should have `span` HTML tag
519
+
520
+ - Use only one action element inside
521
+ - [cardCn implementation](/components/card-cn/#with-action-link-inside)
522
+
523
+ ```tsx
524
+ import cn from 'classnames';
525
+ import { textCn } from '@rescui/typography';
526
+
527
+ const Demo = () => {
528
+ return (
529
+ <a
530
+ href="#with-action-link-inside"
531
+ className="action-link-inside-card"
532
+ style={{ maxWidth: 400 }}
533
+ >
534
+ <h3 className={textCn('rs-h3')}>Card title</h3>
535
+ <p className={cn(textCn('rs-text-2'), 'rs-docs-offset-top-24')}>
536
+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab animi
537
+ aspernatur autem commodi consequuntur esse laudantium nobis numquam
538
+ officiis omnis, perferendis perspiciatis, possimus qui quidem, quis
539
+ repellat tempore unde veniam?
540
+ </p>
541
+ <div className={cn(textCn('rs-text-2'), 'rs-docs-offset-top-24')}>
542
+ <span
543
+ className={cn(
544
+ textCn('rs-link', { mode: 'standalone' }),
545
+ 'action-link-inside-card__link'
546
+ )}
547
+ >
548
+ Click me
549
+ </span>
550
+ </div>
551
+ </a>
552
+ );
553
+ };
554
+
555
+ render(<Demo />);
556
+ ```
557
+
558
+ ```postcss
559
+ @import '@rescui/typography/lib/public-api.p.css';
560
+
561
+ .action-link-inside-card {
562
+ --my-card-border-width: 1px;
563
+ --my-card-border-color: var(--rs-color-black-t20);
564
+
565
+ position: relative;
566
+
567
+ display: block;
568
+
569
+ overflow: hidden;
570
+
571
+ box-sizing: border-box;
572
+ min-height: 0;
573
+
574
+ border: none;
575
+ border-radius: 8px;
576
+
577
+ cursor: pointer;
578
+ text-align: inherit;
579
+
580
+ background-color: var(--rs-color-white);
581
+ box-shadow: inset 0 0 0 var(--my-card-border-width) var(
582
+ --my-card-border-color
583
+ );
584
+
585
+ transition: box-shadow 100ms;
586
+ appearance: none;
587
+ padding: 24px;
588
+
589
+ &,
590
+ &:hover {
591
+ text-decoration: none;
592
+ }
593
+
594
+ /* reset user agent stylesheet at Chrome */
595
+ &:focus-visible {
596
+ outline: none;
597
+ }
598
+
599
+ @media (hover: hover) {
600
+ &:hover {
601
+ --my-card-border-color: var(--rs-color-black);
602
+ }
603
+ }
604
+
605
+ &:focus-visible,
606
+ &:active {
607
+ --my-card-border-color: var(--rs-color-black);
608
+ }
609
+
610
+ &:active {
611
+ --my-card-border-width: 2px;
612
+ }
613
+
614
+ @media (hover: hover) {
615
+ &:hover &__link {
616
+ @mixin rs-typography-link-state-hover;
617
+ }
618
+ }
619
+
620
+ &:focus-visible &__link {
621
+ @mixin rs-typography-link-state-focus-visible;
622
+ }
623
+ }
624
+ ```
625
+
626
+ ### Rock mode
627
+
628
+ ##### Details:
629
+
630
+ - With static border on the main element (border width is constant)
631
+
632
+ - Decrease inner padding by border width value to compensate for the offset
633
+ - Change `border-color` to transparent on hover/active instead of changing its width
634
+
635
+ - Change background-color to contrast value on hover/active
636
+ - Use `--rs-theme-flip` css-variable to reverse theme for all RescUI components on hover/active
637
+
638
+ ```tsx
639
+ import cn from 'classnames';
640
+ import { textCn } from '@rescui/typography';
641
+
642
+ const Demo = () => {
643
+ return (
644
+ <button type="button" className="rock-mode-card" style={{ maxWidth: 400 }}>
645
+ <h3 className={textCn('rs-h3')}>Card title</h3>
646
+ <p className={cn(textCn('rs-text-2'), 'rs-docs-offset-top-24')}>
647
+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab animi
648
+ aspernatur autem commodi consequuntur esse laudantium nobis numquam
649
+ officiis omnis, perferendis perspiciatis, possimus qui quidem, quis
650
+ repellat tempore unde veniam?
651
+ </p>
652
+ </button>
653
+ );
654
+ };
655
+
656
+ render(<Demo />);
657
+ ```
658
+
659
+ ```postcss
660
+ .rock-mode-card {
661
+ --my-card-border-color: var(--rs-color-black-t20);
662
+
663
+ display: block;
664
+
665
+ overflow: hidden;
666
+
667
+ box-sizing: border-box;
668
+ min-height: 0;
669
+
670
+ border: none;
671
+ border-radius: 8px;
672
+
673
+ cursor: pointer;
674
+ text-align: inherit;
675
+
676
+ background-color: var(--rs-color-white);
677
+ box-shadow: inset 0 0 0 1px var(--my-card-border-color);
678
+
679
+ transition: color 100ms, background-color 100ms, box-shadow 100ms;
680
+ appearance: none;
681
+ padding: 24px;
682
+
683
+ &,
684
+ &:hover {
685
+ text-decoration: none;
686
+ }
687
+
688
+ /* reset user agent stylesheet at Chrome */
689
+ &:focus-visible {
690
+ outline: none;
691
+ }
692
+
693
+ @media (hover: hover) {
694
+ &:hover {
695
+ --rs-theme-flip: 1;
696
+ --my-card-border-color: transparent;
697
+ background-color: var(--rs-color-black);
698
+ }
699
+ }
700
+
701
+ &:focus-visible,
702
+ &:active {
703
+ --rs-theme-flip: 1;
704
+ --my-card-border-color: transparent;
705
+ background-color: var(--rs-color-black);
706
+ }
707
+
708
+ &:active {
709
+ --rs-theme-flip: 1;
710
+ --my-card-border-color: transparent;
711
+ background-color: var(--rs-color-grey-90);
712
+ }
713
+ }
714
+ ```
715
+
716
+ ### Aligned grid with subgrid
717
+
718
+ ##### Details:
719
+
720
+ - With border via pseudo-element `::before`
721
+
722
+ - Inside grid container
723
+ - Each card takes up two rows and applies subgrid to its children
724
+ - Cards text content lays on the second row, aligning with the content in the sibling cards
725
+
726
+ ```tsx
727
+ import cn from 'classnames';
728
+ import { textCn } from '@rescui/typography';
729
+
730
+ const Demo = () => {
731
+ return (
732
+ <div className="aligned-grid">
733
+ <button type="button" className="aligned-grid__card">
734
+ <h3 className={textCn('rs-h3')}>
735
+ Some long card title which takes two lines
736
+ </h3>
737
+ <p className={cn(textCn('rs-text-2'), 'aligned-grid__content')}>
738
+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab animi
739
+ aspernatur autem commodi consequuntur esse laudantium nobis numquam
740
+ officiis omnis, perferendis perspiciatis, possimus qui quidem, quis
741
+ repellat tempore unde veniam?
742
+ </p>
743
+ </button>
744
+ <button type="button" className="aligned-grid__card">
745
+ <h3 className={textCn('rs-h3')}>Awesome card title</h3>
746
+ <p className={cn(textCn('rs-text-2'), 'aligned-grid__content')}>
747
+ The closest known nebula to Earth is called the Helix Nebula. It is
748
+ the remnant of a dying star — possibly one like the Sun. It is
749
+ approximately 700 light-years away from Earth.
750
+ </p>
751
+ </button>
752
+ <button type="button" className="aligned-grid__card">
753
+ <h3 className={textCn('rs-h3')}>Cool card title</h3>
754
+ <p className={cn(textCn('rs-text-2'), 'aligned-grid__content')}>
755
+ We help developers work faster by automating common, repetitive tasks
756
+ to enable them to stay focused on code design and the big picture. We
757
+ provide tools to explore and familiarize with code bases faster.
758
+ </p>
759
+ </button>
760
+ <button type="button" className="aligned-grid__card">
761
+ <h3 className={textCn('rs-h3')}>
762
+ Another long card title which takes two lines
763
+ </h3>
764
+ <p className={cn(textCn('rs-text-2'), 'aligned-grid__content')}>
765
+ Fëanor was a Ñoldorin elf and one of the Elven kindred that departed
766
+ from Valinor in the land of Aman, where they had lived with the Valar.
767
+ </p>
768
+ </button>
769
+ <button type="button" className="aligned-grid__card">
770
+ <h3 className={textCn('rs-h3')}>Neat card title</h3>
771
+ <p className={cn(textCn('rs-text-2'), 'aligned-grid__content')}>
772
+ It’s a dangerous business, Frodo, going out your door. You step onto
773
+ the road, and if you don’t keep your feet, there’s no knowing where
774
+ you might be swept off to.
775
+ </p>
776
+ </button>
777
+ </div>
778
+ );
779
+ };
780
+
781
+ render(<Demo />);
782
+ ```
783
+
784
+ ```postcss
785
+ .aligned-grid {
786
+ display: grid;
787
+ column-gap: 32px;
788
+ row-gap: 24px;
789
+ grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
790
+ grid-template-rows: auto;
791
+ }
792
+
793
+ .aligned-grid__card {
794
+ --my-card-border-width: 1px;
795
+ --my-card-border-color: var(--rs-color-black-t20);
796
+
797
+ position: relative;
798
+
799
+ display: grid;
800
+
801
+ overflow: hidden;
802
+
803
+ box-sizing: border-box;
804
+ min-height: 0;
805
+ padding: 24px;
806
+
807
+ border: none;
808
+ border-radius: 8px;
809
+
810
+ cursor: pointer;
811
+ text-align: inherit;
812
+
813
+ background-color: var(--rs-color-white);
814
+ grid-template-columns: 1fr;
815
+ grid-template-rows: subgrid;
816
+ grid-row: auto / span 2;
817
+ row-gap: 24px;
818
+ box-shadow: inset 0 0 0 var(--my-card-border-width) var(
819
+ --my-card-border-color
820
+ );
821
+
822
+ transition: box-shadow 100ms;
823
+ appearance: none;
824
+
825
+ &,
826
+ &:hover {
827
+ text-decoration: none;
828
+ }
829
+
830
+ /* reset user agent stylesheet at Chrome */
831
+ &:focus-visible {
832
+ outline: none;
833
+ }
834
+
835
+ @media (hover: hover) {
836
+ &:hover {
837
+ --my-card-border-color: var(--rs-color-black);
838
+ }
839
+ }
840
+
841
+ &:focus-visible,
842
+ &:active {
843
+ --my-card-border-color: var(--rs-color-black);
844
+ }
845
+
846
+ &:active {
847
+ --my-card-border-width: 2px;
848
+ }
849
+ }
850
+
851
+ .aligned-grid__content {
852
+ grid-row: 2 / span 1;
853
+ }
854
+ ```
855
+
856
+ ### Small cards
857
+
858
+ ##### Details:
859
+
860
+ - With border via pseudo-element `::before`
861
+
862
+ - With colored bg and border
863
+ - `border-color` is constant, but the `background-color` changes on hover
864
+ - Text is center aligned
865
+ - Card's width is constant
866
+
867
+ ```tsx
868
+ import cn from 'classnames';
869
+ import { textCn } from '@rescui/typography';
870
+ import apacheDerbyLogoSvg from '@/parts/cards/db-logos/apache-derby-logo.svg';
871
+ import azureLogoSvg from '@/parts/cards/db-logos/azure-logo.svg';
872
+ import h2DatabaseLogoSvg from '@/parts/cards/db-logos/h2-database-logo.svg';
873
+ import ibmDB2LogoSvg from '@/parts/cards/db-logos/ibm-db2-logo.svg';
874
+ import mongodbLogoSvg from '@/parts/cards/db-logos/mongodb-logo.svg';
875
+ import mysqlLogoSvg from '@/parts/cards/db-logos/mysql-logo.svg';
876
+ import redisLogoSvg from '@/parts/cards/db-logos/redis-logo.svg';
877
+ import sqliteLogoSvg from '@/parts/cards/db-logos/sqlite-logo.svg';
878
+ import sybaseLogoSvg from '@/parts/cards/db-logos/sybase-logo.svg';
879
+ import awsDynamodbLogo from '@/parts/cards/db-logos/aws-dynamodb-logo.svg';
880
+
881
+ const data = [
882
+ { title: 'MySQL', logoSrc: mysqlLogoSvg.src },
883
+ { title: 'MongoDB', logoSrc: mongodbLogoSvg.src },
884
+ { title: 'Redis', logoSrc: redisLogoSvg.src },
885
+ { title: 'Azure SQL', logoSrc: azureLogoSvg.src },
886
+ { title: 'Amazon DynamoDB', logoSrc: awsDynamodbLogo.src },
887
+ { title: 'SQLite', logoSrc: sqliteLogoSvg.src },
888
+ { title: 'IBM Db2', logoSrc: ibmDB2LogoSvg.src },
889
+ { title: 'H2', logoSrc: h2DatabaseLogoSvg.src },
890
+ { title: 'Sybase ASE', logoSrc: sybaseLogoSvg.src },
891
+ { title: 'Apache Derby', logoSrc: apacheDerbyLogoSvg.src }
892
+ ];
893
+
894
+ const Demo = () => {
895
+ return (
896
+ <ul className={cn('small-cards', 'jb-offset-top-32')}>
897
+ {data.map(item => (
898
+ <li key={item.title}>
899
+ <a className={cn('small-cards__card')} href={'#small-cards'}>
900
+ <img
901
+ src={item.logoSrc}
902
+ alt="Database logo"
903
+ className="small-cards__card-logo"
904
+ />
905
+ <h4 className={cn(textCn('rs-h5'), 'rs-docs-offset-top-12')}>
906
+ {item.title}
907
+ </h4>
908
+ </a>
909
+ </li>
910
+ ))}
911
+ </ul>
912
+ );
913
+ };
914
+
915
+ render(<Demo />);
916
+ ```
917
+
918
+ ```postcss attached
919
+ .small-cards {
920
+ display: flex;
921
+ flex-wrap: wrap;
922
+ column-gap: 24px;
923
+ row-gap: 24px;
924
+
925
+ text-align: center;
926
+ }
927
+
928
+ .small-cards__card {
929
+ --my-card-border-width: 1px;
930
+ --my-card-border-color: var(--rs-color-primary-light-theme);
931
+ --my-card-background-color: var(--rs-color-white);
932
+
933
+ position: relative;
934
+
935
+ display: block;
936
+
937
+ overflow: hidden;
938
+
939
+ box-sizing: border-box;
940
+ width: 150px;
941
+ min-height: 0;
942
+ padding: 32px 8px;
943
+
944
+ border: none;
945
+ border-radius: 8px;
946
+
947
+ cursor: pointer;
948
+ text-align: inherit;
949
+
950
+ background-color: var(--my-card-background-color);
951
+ box-shadow: inset 0 0 0 var(--my-card-border-width) var(
952
+ --my-card-border-color
953
+ );
954
+
955
+ transition: box-shadow 100ms;
956
+ appearance: none;
957
+
958
+ &,
959
+ &:hover {
960
+ text-decoration: none;
961
+ }
962
+
963
+ /* reset user agent stylesheet at Chrome */
964
+ &:focus-visible {
965
+ outline: none;
966
+ }
967
+
968
+ @media (hover: hover) {
969
+ &:hover {
970
+ --my-card-background-color: var(--rs-color-primary-fog-light-theme);
971
+ }
972
+ }
973
+
974
+ &:focus-visible,
975
+ &:active {
976
+ --my-card-background-color: var(--rs-color-primary-fog-light-theme);
977
+ }
978
+
979
+ &:active {
980
+ --my-card-border-width: 2px;
981
+ }
982
+ }
983
+
984
+ .small-cards__card-logo {
985
+ height: 36px;
986
+ width: auto;
987
+ }
988
+ ```
989
+
990
+ ### Newsletter cards
991
+
992
+ ##### Details:
993
+
994
+ - Has a large image
995
+ - Has an action button
996
+ - Border has a non-transparent color
997
+ - Padding controlled by css variables
998
+ - Action buttons are aligned with sibling tiles
999
+ - Use pseudo-element for border to show it above the content
1000
+ - [cardCn implementation](/components/card-cn/#with-image-via-img-tag)
1001
+
1002
+ ```tsx
1003
+ import { textCn } from '@rescui/typography';
1004
+ import { Button } from '@rescui/button';
1005
+ import card1Image from '@/parts/cards/newsletter1.webp';
1006
+ import card2Image from '@/parts/cards/newsletter2.webp';
1007
+
1008
+ const NewsletterCard = ({ title, imageSrc }) => {
1009
+ return (
1010
+ <button type="button" className="newsletter-card">
1011
+ <div className="newsletter-card__header">
1012
+ <img className="newsletter-card__img" alt="keymaps" src={imageSrc} />
1013
+ <h3 className={textCn('rs-h3')}>{title}</h3>
1014
+ </div>
1015
+ <div className="newsletter-card__footer">
1016
+ <Button className="newsletter-card__button" notFocusable>
1017
+ Learn more
1018
+ </Button>
1019
+ </div>
1020
+ </button>
1021
+ );
1022
+ };
1023
+
1024
+ const Demo = () => {
1025
+ return (
1026
+ <div className="newsletter-cards-container">
1027
+ <NewsletterCard
1028
+ title={'JetBrains Community Newsletter (Monthly) - join us now!'}
1029
+ imageSrc={card1Image.src}
1030
+ />
1031
+ <NewsletterCard
1032
+ title={'Java Annotated Monthly'}
1033
+ imageSrc={card2Image.src}
1034
+ />
1035
+ </div>
1036
+ );
1037
+ };
1038
+
1039
+ render(<Demo />);
1040
+ ```
1041
+
1042
+ ```postcss
1043
+ @import '@rescui/button/lib/public-api.p.css';
1044
+
1045
+ .newsletter-cards-container {
1046
+ display: grid;
1047
+ grid-template-rows: auto;
1048
+ grid-template-columns: repeat(2, 1fr);
1049
+ column-gap: 32px;
1050
+ row-gap: 24px;
1051
+
1052
+ @media (--sm) {
1053
+ grid-template-columns: repeat(1, 1fr);
1054
+ }
1055
+ }
1056
+
1057
+ .newsletter-card {
1058
+ --my-card-border-width: 1px;
1059
+ --my-card-border-color: var(--rs-color-grey-20);
1060
+ --my-card-padding: 24px;
1061
+
1062
+ position: relative;
1063
+
1064
+ display: grid;
1065
+ overflow: hidden;
1066
+
1067
+ box-sizing: border-box;
1068
+ min-height: 0;
1069
+ padding: var(--my-card-padding);
1070
+
1071
+ border: none;
1072
+ border-radius: 8px;
1073
+
1074
+ cursor: pointer;
1075
+ text-align: inherit;
1076
+
1077
+ background-color: var(--rs-color-white);
1078
+ grid-template-columns: 1fr;
1079
+ grid-template-rows: subgrid;
1080
+ grid-row: auto / span 2;
1081
+ row-gap: 24px;
1082
+
1083
+ &,
1084
+ &:hover {
1085
+ text-decoration: none;
1086
+ }
1087
+
1088
+ /* reset user agent stylesheet at Chrome */
1089
+ &:focus-visible {
1090
+ outline: none;
1091
+ }
1092
+
1093
+ @media (hover: hover) {
1094
+ &:hover {
1095
+ --my-card-border-color: var(--rs-color-black);
1096
+ }
1097
+ }
1098
+
1099
+ &:focus-visible,
1100
+ &:active {
1101
+ --my-card-border-color: var(--rs-color-black);
1102
+ }
1103
+
1104
+ &:active {
1105
+ --my-card-border-width: 2px;
1106
+ }
1107
+
1108
+ &::after {
1109
+ position: absolute;
1110
+ top: 0;
1111
+ right: 0;
1112
+ bottom: 0;
1113
+ left: 0;
1114
+
1115
+ display: block;
1116
+
1117
+ border-radius: inherit;
1118
+
1119
+ content: '';
1120
+ pointer-events: none;
1121
+
1122
+ box-shadow: inset 0 0 0 var(--my-card-border-width) var(
1123
+ --my-card-border-color
1124
+ );
1125
+ }
1126
+
1127
+ @media (hover: hover) {
1128
+ &:hover &__button {
1129
+ @mixin rs-button-state-hover;
1130
+ }
1131
+ }
1132
+
1133
+ &:active &__button {
1134
+ @mixin rs-button-state-active;
1135
+ }
1136
+
1137
+ &:focus-visible &__button {
1138
+ @mixin rs-button-state-focus-visible;
1139
+ }
1140
+
1141
+ &:active:focus-visible:not(:hover) &__button {
1142
+ @mixin rs-button-state-active-focus-visible-no-hover;
1143
+ }
1144
+ }
1145
+
1146
+ .newsletter-card__img {
1147
+ position: relative;
1148
+ top: calc(-1 * var(--my-card-padding));
1149
+ left: calc(-1 * var(--my-card-padding));
1150
+
1151
+ display: flex;
1152
+ justify-content: center;
1153
+ align-items: center;
1154
+
1155
+ width: calc(100% + 2 * var(--my-card-padding));
1156
+ height: 212px;
1157
+ max-height: 212px;
1158
+ object-fit: cover;
1159
+ object-position: right;
1160
+ }
1161
+
1162
+ .newsletter-card__footer {
1163
+ grid-row: 2;
1164
+ display: flex;
1165
+ flex-wrap: wrap;
1166
+ justify-content: space-between;
1167
+ align-items: center;
1168
+ column-gap: 32px;
1169
+ row-gap: 24px;
1170
+ }
1171
+ ```
1172
+
1173
+ ## Tiles
1174
+
1175
+ ### Base example
1176
+
1177
+ ##### Details:
1178
+
1179
+ - Base example
1180
+ - Has no dynamic states - its a static `div` element
1181
+
1182
+ - [cardCn implementation](/components/card-cn/#default-card)
1183
+
1184
+ ```tsx
1185
+ import cn from 'classnames';
1186
+ import { textCn } from '@rescui/typography';
1187
+
1188
+ const Demo = () => {
1189
+ return (
1190
+ <div className="base-tile" style={{ maxWidth: 400 }}>
1191
+ <h3 className={textCn('rs-h3')}>Card title</h3>
1192
+ <p className={cn(textCn('rs-text-2'), 'rs-docs-offset-top-24')}>
1193
+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab animi
1194
+ aspernatur autem commodi consequuntur esse laudantium nobis numquam
1195
+ officiis omnis, perferendis perspiciatis, possimus qui quidem, quis
1196
+ repellat tempore unde veniam?
1197
+ </p>
1198
+ </div>
1199
+ );
1200
+ };
1201
+
1202
+ render(<Demo />);
1203
+ ```
1204
+
1205
+ ```postcss
1206
+ .base-tile {
1207
+ position: relative;
1208
+
1209
+ display: block;
1210
+
1211
+ box-sizing: border-box;
1212
+ min-height: 0;
1213
+
1214
+ border: none;
1215
+ border-radius: 8px;
1216
+
1217
+ background-color: var(--rs-color-white);
1218
+ box-shadow: inset 0 0 0 1px var(--rs-color-black-t20);
1219
+
1220
+ padding: 24px;
1221
+ }
1222
+ ```
1223
+
1224
+ ### Small tiles
1225
+
1226
+ ##### Details:
1227
+
1228
+ - Without border
1229
+ - With colored background
1230
+ - Static element without states
1231
+ - Text is center aligned
1232
+ - Tiles width is constant
1233
+
1234
+ ```tsx
1235
+ import cn from 'classnames';
1236
+ import { textCn } from '@rescui/typography';
1237
+ import apacheDerbyLogoSvg from '@/parts/cards/db-logos/apache-derby-logo.svg';
1238
+ import azureLogoSvg from '@/parts/cards/db-logos/azure-logo.svg';
1239
+ import h2DatabaseLogoSvg from '@/parts/cards/db-logos/h2-database-logo.svg';
1240
+ import ibmDB2LogoSvg from '@/parts/cards/db-logos/ibm-db2-logo.svg';
1241
+ import mongodbLogoSvg from '@/parts/cards/db-logos/mongodb-logo.svg';
1242
+ import mysqlLogoSvg from '@/parts/cards/db-logos/mysql-logo.svg';
1243
+ import redisLogoSvg from '@/parts/cards/db-logos/redis-logo.svg';
1244
+ import sqliteLogoSvg from '@/parts/cards/db-logos/sqlite-logo.svg';
1245
+ import sybaseLogoSvg from '@/parts/cards/db-logos/sybase-logo.svg';
1246
+ import awsDynamodbLogo from '@/parts/cards/db-logos/aws-dynamodb-logo.svg';
1247
+
1248
+ const data = [
1249
+ { title: 'MySQL', logoSrc: mysqlLogoSvg.src },
1250
+ { title: 'MongoDB', logoSrc: mongodbLogoSvg.src },
1251
+ { title: 'Redis', logoSrc: redisLogoSvg.src },
1252
+ { title: 'Azure SQL', logoSrc: azureLogoSvg.src },
1253
+ { title: 'Amazon DynamoDB', logoSrc: awsDynamodbLogo.src },
1254
+ { title: 'SQLite', logoSrc: sqliteLogoSvg.src },
1255
+ { title: 'IBM Db2', logoSrc: ibmDB2LogoSvg.src },
1256
+ { title: 'H2', logoSrc: h2DatabaseLogoSvg.src },
1257
+ { title: 'Sybase ASE', logoSrc: sybaseLogoSvg.src },
1258
+ { title: 'Apache Derby', logoSrc: apacheDerbyLogoSvg.src }
1259
+ ];
1260
+
1261
+ const Demo = () => {
1262
+ return (
1263
+ <ul className={cn('small-tiles', 'jb-offset-top-32')}>
1264
+ {data.map(item => (
1265
+ <li key={item.title}>
1266
+ <span className={cn('small-tiles__tile')}>
1267
+ <img
1268
+ src={item.logoSrc}
1269
+ alt="Database logo"
1270
+ className="small-tiles__tile-logo"
1271
+ />
1272
+ <h4 className={cn(textCn('rs-h5'), 'rs-docs-offset-top-12')}>
1273
+ {item.title}
1274
+ </h4>
1275
+ </span>
1276
+ </li>
1277
+ ))}
1278
+ </ul>
1279
+ );
1280
+ };
1281
+
1282
+ render(<Demo />);
1283
+ ```
1284
+
1285
+ ```postcss
1286
+ .small-tiles {
1287
+ display: flex;
1288
+ flex-wrap: wrap;
1289
+ column-gap: 24px;
1290
+ row-gap: 24px;
1291
+
1292
+ text-align: center;
1293
+ }
1294
+
1295
+ .small-tiles__tile {
1296
+ position: relative;
1297
+
1298
+ display: block;
1299
+
1300
+ overflow: hidden;
1301
+
1302
+ box-sizing: border-box;
1303
+ width: 150px;
1304
+ min-height: 0;
1305
+ padding: 32px 8px;
1306
+
1307
+ border: none;
1308
+ border-radius: 8px;
1309
+
1310
+ background-color: var(--rs-color-primary-fog-light-theme);
1311
+ }
1312
+
1313
+ .small-tiles__tile-logo {
1314
+ height: 36px;
1315
+ width: auto;
1316
+ }
1317
+ ```
1318
+
1319
+ ### Borderless tile
1320
+
1321
+ ##### Details:
1322
+
1323
+ - Without border
1324
+ - With background
1325
+ - Static element without states
1326
+
1327
+ ```tsx
1328
+ import cn from 'classnames';
1329
+ import { textCn } from '@rescui/typography';
1330
+
1331
+ const Demo = () => {
1332
+ return (
1333
+ <div className="borderless-tile" style={{ maxWidth: 400 }}>
1334
+ <h3 className={textCn('rs-h3')}>Card title</h3>
1335
+ <p className={cn(textCn('rs-text-2'), 'rs-docs-offset-top-24')}>
1336
+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab animi
1337
+ aspernatur autem commodi consequuntur esse laudantium nobis numquam
1338
+ officiis omnis, perferendis perspiciatis, possimus qui quidem, quis
1339
+ repellat tempore unde veniam?
1340
+ </p>
1341
+ </div>
1342
+ );
1343
+ };
1344
+
1345
+ render(<Demo />);
1346
+ ```
1347
+
1348
+ ```postcss
1349
+ .borderless-tile {
1350
+ position: relative;
1351
+
1352
+ display: block;
1353
+
1354
+ box-sizing: border-box;
1355
+ min-height: 0;
1356
+
1357
+ border: none;
1358
+ border-radius: 8px;
1359
+
1360
+ background-color: var(--rs-color-black-t5);
1361
+
1362
+ padding: 24px;
1363
+ }
1364
+ ```
1365
+
1366
+ ### Large content
1367
+
1368
+ ##### Details:
1369
+
1370
+ - Large header, large text
1371
+ - Content is center aligned
1372
+ - Large image below
1373
+
1374
+ ```tsx
1375
+ import cn from 'classnames';
1376
+ import { textCn } from '@rescui/typography';
1377
+ import tileImage from '@/parts/cards/keymaps.webp';
1378
+
1379
+ const Demo = () => {
1380
+ return (
1381
+ <div className="large-content-tile" style={{ maxWidth: 600 }}>
1382
+ <div className="large-content-tile__content">
1383
+ <h2 className={textCn('rs-h2')}>Keymaps</h2>
1384
+ <p className={cn(textCn('rs-text-1'), 'rs-docs-offset-top-24')}>
1385
+ Choose a familiar keymap or set a shortcut for any action.
1386
+ </p>
1387
+ </div>
1388
+ <div className="large-content-tile__img">
1389
+ <img alt="keymaps" src={tileImage.src} />
1390
+ </div>
1391
+ </div>
1392
+ );
1393
+ };
1394
+
1395
+ render(<Demo />);
1396
+ ```
1397
+
1398
+ ```postcss
1399
+ .large-content-tile {
1400
+ position: relative;
1401
+
1402
+ display: block;
1403
+
1404
+ box-sizing: border-box;
1405
+ min-height: 0;
1406
+
1407
+ padding: 24px;
1408
+
1409
+ border: none;
1410
+ border-radius: 8px;
1411
+
1412
+ text-align: center;
1413
+
1414
+ background-color: var(--rs-color-black-t5);
1415
+ }
1416
+
1417
+ .large-content-tile__content {
1418
+ width: 80%;
1419
+ margin: 0 auto;
1420
+
1421
+ @media (--sm) {
1422
+ width: 100%;
1423
+ }
1424
+ }
1425
+
1426
+ .large-content-tile__img {
1427
+ display: flex;
1428
+ overflow: hidden;
1429
+
1430
+ margin: 32px 12px 12px;
1431
+
1432
+ border-radius: 8px;
1433
+ filter: drop-shadow(0 10px 35px rgba(0, 0, 0, 0.2));
1434
+
1435
+ &,
1436
+ img {
1437
+ max-width: 100%;
1438
+ }
1439
+ }
1440
+ ```
1441
+
1442
+ ### Newsletter tiles
1443
+
1444
+ ##### Details:
1445
+
1446
+ - Has a large image
1447
+ - Has a subscribe form
1448
+ - Border has a non-transparent color
1449
+ - Padding and border are controlled by css variables
1450
+ - Subscribe forms are aligned with sibling tiles
1451
+ - Use pseudo-element for border to show it above the content
1452
+
1453
+ ```tsx
1454
+ import cn from 'classnames';
1455
+ import { textCn } from '@rescui/typography';
1456
+ import { Input } from '@rescui/input';
1457
+ import { Button } from '@rescui/button';
1458
+ import tile1Image from '@/parts/cards/newsletter1.webp';
1459
+ import tile2Image from '@/parts/cards/newsletter2.webp';
1460
+
1461
+ const NewsletterTile = ({ title, imageSrc }) => {
1462
+ return (
1463
+ <div className="newsletter-tile">
1464
+ <div className="newsletter-tile__header">
1465
+ <img className="newsletter-tile__img" alt="keymaps" src={imageSrc} />
1466
+ <h3 className={textCn('rs-h3')}>{title}</h3>
1467
+ </div>
1468
+ <div className="newsletter-tile__form">
1469
+ <Input placeholder="Your Email" type="email" />
1470
+ <p className={cn(textCn('rs-text-3'), 'rs-docs-offset-top-16')}>
1471
+ By submitting this form, I acknowledge that JetBrains s.r.o. may
1472
+ process my personal data I provided above for purposes explained above
1473
+ and may engage third parties in such processing. More details about
1474
+ the processing are in JetBrains Privacy Notice.
1475
+ </p>
1476
+ <div className={cn('newsletter-tile__footer', 'rs-docs-offset-top-24')}>
1477
+ <Button>Subscribe</Button>
1478
+ <span className={textCn('rs-text-2')}>
1479
+ <a
1480
+ href="#newsletter-tiles"
1481
+ className={textCn('rs-link', { mode: 'standalone' })}
1482
+ >
1483
+ View sample newsletter
1484
+ </a>
1485
+ </span>
1486
+ </div>
1487
+ </div>
1488
+ </div>
1489
+ );
1490
+ };
1491
+
1492
+ const Demo = () => {
1493
+ return (
1494
+ <div className="newsletter-tiles-container">
1495
+ <NewsletterTile
1496
+ title={'JetBrains Community Newsletter (Monthly)'}
1497
+ imageSrc={tile1Image.src}
1498
+ />
1499
+ <NewsletterTile
1500
+ title={'Java Annotated Monthly'}
1501
+ imageSrc={tile2Image.src}
1502
+ />
1503
+ </div>
1504
+ );
1505
+ };
1506
+
1507
+ render(<Demo />);
1508
+ ```
1509
+
1510
+ ```postcss
1511
+ .newsletter-tiles-container {
1512
+ display: grid;
1513
+ grid-template-rows: auto;
1514
+ grid-template-columns: repeat(2, 1fr);
1515
+ column-gap: 32px;
1516
+ row-gap: 24px;
1517
+
1518
+ @media (--sm) {
1519
+ grid-template-columns: repeat(1, 1fr);
1520
+ }
1521
+ }
1522
+
1523
+ .newsletter-tile {
1524
+ --my-tile-padding: 24px;
1525
+
1526
+ position: relative;
1527
+
1528
+ display: grid;
1529
+ overflow: hidden;
1530
+
1531
+ box-sizing: border-box;
1532
+ min-height: 0;
1533
+ padding: var(--my-tile-padding);
1534
+
1535
+ border: none;
1536
+ border-radius: 8px;
1537
+
1538
+ background-color: var(--rs-color-white);
1539
+ grid-template-columns: 1fr;
1540
+ grid-template-rows: subgrid;
1541
+ grid-row: auto / span 2;
1542
+ row-gap: 16px;
1543
+
1544
+ &::after {
1545
+ position: absolute;
1546
+ top: 0;
1547
+ right: 0;
1548
+ bottom: 0;
1549
+ left: 0;
1550
+
1551
+ display: block;
1552
+
1553
+ border-radius: inherit;
1554
+
1555
+ content: '';
1556
+ pointer-events: none;
1557
+
1558
+ box-shadow: inset 0 0 0 1px var(--rs-color-grey-20);
1559
+ }
1560
+ }
1561
+
1562
+ .newsletter-tile__form {
1563
+ grid-row: 2;
1564
+ }
1565
+
1566
+ .newsletter-tile__img {
1567
+ position: relative;
1568
+ top: calc(-1 * var(--my-tile-padding));
1569
+ left: calc(-1 * var(--my-tile-padding));
1570
+
1571
+ display: flex;
1572
+ justify-content: center;
1573
+ align-items: center;
1574
+
1575
+ width: calc(100% + 2 * var(--my-tile-padding));
1576
+ height: 212px;
1577
+ max-height: 212px;
1578
+ object-fit: cover;
1579
+ object-position: right;
1580
+ }
1581
+
1582
+ .newsletter-tile__footer {
1583
+ display: flex;
1584
+ flex-wrap: wrap;
1585
+ justify-content: space-between;
1586
+ align-items: center;
1587
+ column-gap: 32px;
1588
+ row-gap: 24px;
1589
+ }
1590
+ ```
1591
+
1592
+ ### Masonry grid
1593
+
1594
+ ##### Details:
1595
+
1596
+ - Tiles are aligned using `grid-template-areas` CSS property
1597
+
1598
+ - Grid areas are forwarded to CSS using `--tile-id` CSS variable
1599
+
1600
+ - Flat tiles layout structure allows for easy adaptive changes compared to the two-column flex layout
1601
+
1602
+ ```tsx
1603
+ import cn from 'classnames';
1604
+ import { textCn } from '@rescui/typography';
1605
+
1606
+ const MasonryTile = ({ name, position, children, tileId }) => {
1607
+ return (
1608
+ <div className="masonry-tile" style={{ '--tile-id': tileId }}>
1609
+ <h3 className={textCn('rs-h3')}>{name}</h3>
1610
+ <p className={cn(textCn('rs-text-2'), 'rs-docs-offset-top-8')}>
1611
+ {position}
1612
+ </p>
1613
+ <div
1614
+ className={cn(
1615
+ textCn('rs-text-2', { hardness: 'hard', paragraphOffsetAuto: true }),
1616
+ 'rs-docs-offset-top-24'
1617
+ )}
1618
+ >
1619
+ {children}
1620
+ </div>
1621
+ </div>
1622
+ );
1623
+ };
1624
+
1625
+ const Demo = () => {
1626
+ return (
1627
+ <div className="masonry-tiles-container">
1628
+ <MasonryTile
1629
+ tileId="stephan"
1630
+ name="Stephan Janssen"
1631
+ position="Founder at DevOxx, Co-founder Voxxed"
1632
+ >
1633
+ Fully integrating a chatbot into your favourite IDE significantly
1634
+ increases development productivity. No more toggling between platforms
1635
+ like ChatGPT – now you have an AI assistant embedded directly within
1636
+ your development environment. This allows for real-time code
1637
+ discussions, code reviews, and even intelligent name suggestions during
1638
+ refactoring tasks, among other features. AI Assistant takes software
1639
+ development to a whole new level, and it's clear that JetBrains has only
1640
+ scratched the surface of its potential. Exciting times to be a
1641
+ developer!
1642
+ </MasonryTile>
1643
+ <MasonryTile
1644
+ tileId="duncan"
1645
+ name="Duncan McGregor"
1646
+ position="Software Developer, Author of Java to Kotlin"
1647
+ >
1648
+ <p>
1649
+ It’s hard to know what effect AI will have on our jobs as professional
1650
+ developers, but right now JetBrains AI Assistant is making me more
1651
+ productive by removing drudgery. It’s better than I am at writing Bash
1652
+ to automate build and deployment tasks, and when Bash gets too
1653
+ fragile, it does a good job of converting scripts into Python. It is
1654
+ impressive when refactoring small routines too.
1655
+ </p>
1656
+ <p>
1657
+ Sometimes its results aren’t to my taste, but then we can have a chat
1658
+ about my preferred style and it will modify its suggestions
1659
+ accordingly. I also appreciate that the user interface makes it clear
1660
+ what information is being sent to the model, and that I have control
1661
+ over what is brought into my codebase.
1662
+ </p>
1663
+ </MasonryTile>
1664
+ <MasonryTile
1665
+ tileId="luca"
1666
+ name="Luca Nerlich"
1667
+ position="Tech Consultant, Nerlich / Puls GbR"
1668
+ >
1669
+ Absolute game-changer. Use it daily.
1670
+ </MasonryTile>
1671
+ <MasonryTile
1672
+ tileId="daniel"
1673
+ name="Daniel Lima"
1674
+ position="Senior Data Visualization Specialist"
1675
+ >
1676
+ This plugin is awesome, it is something that I did not know I needed,
1677
+ and now I can't live without it. Superb job, and the recommendations are
1678
+ better or fit better than other AI assistants.
1679
+ </MasonryTile>
1680
+ <MasonryTile
1681
+ tileId="clemens"
1682
+ name="Clemens Mattner"
1683
+ position="Software Engineer"
1684
+ >
1685
+ Really love it! This assistant helped me migrate some controls from
1686
+ Bootstrap to MudBlazor. Sometimes it writes my commit messages, if I'm
1687
+ too lazy. This assistant is a really good helper at work and makes life
1688
+ easier.
1689
+ </MasonryTile>
1690
+ </div>
1691
+ );
1692
+ };
1693
+
1694
+ render(<Demo />);
1695
+ ```
1696
+
1697
+ ```postcss
1698
+ .masonry-tiles-container {
1699
+ display: grid;
1700
+ grid-template-columns: repeat(2, 1fr);
1701
+ grid-template-rows: auto;
1702
+ grid-template-areas:
1703
+ 'stephan luca'
1704
+ 'stephan luca'
1705
+ 'stephan daniel'
1706
+ 'duncan daniel'
1707
+ 'duncan clemens'
1708
+ 'duncan clemens';
1709
+ column-gap: 32px;
1710
+ row-gap: 32px;
1711
+
1712
+ @media (--md) {
1713
+ grid-template-columns: 1fr;
1714
+ grid-template-rows: auto;
1715
+ grid-template-areas: none;
1716
+ }
1717
+ }
1718
+
1719
+ .masonry-tile {
1720
+ --rs-theme-dark: 1;
1721
+
1722
+ position: relative;
1723
+
1724
+ display: block;
1725
+ grid-area: var(--tile-id);
1726
+
1727
+ box-sizing: border-box;
1728
+ min-height: 0;
1729
+ max-width: 600px;
1730
+
1731
+ padding: 24px;
1732
+
1733
+ border: none;
1734
+ border-radius: 8px;
1735
+
1736
+ background-color: var(--rs-color-primary-light-theme);
1737
+
1738
+ @media (--md) {
1739
+ grid-area: auto;
1740
+ }
1741
+ }
1742
+ ```
1743
+
1744
+ ### "Buy" tile
1745
+
1746
+ ##### Details:
1747
+
1748
+ - Big tiles with a lot of content
1749
+ - One or more tiles may have `StickyTag`
1750
+
1751
+ - Content inside tiles is aligned with siblings
1752
+ - `checkbox` aligned using subgrid
1753
+
1754
+ - Actions in the footer are nailed to the tile bottom
1755
+ - Tiles may contain `Collapse` inside
1756
+
1757
+ ```tsx
1758
+ import cn from 'classnames';
1759
+ import { textCn } from '@rescui/typography';
1760
+ import { StickyTag } from '@rescui/tag';
1761
+ import { Collapse } from '@rescui/collapse';
1762
+ import {
1763
+ AquaLogo,
1764
+ ClionLogo,
1765
+ CodeWithMeLogo,
1766
+ DatagripLogo,
1767
+ DataspellLogo,
1768
+ DotcoverLogo,
1769
+ DotmemoryLogo,
1770
+ DottraceLogo,
1771
+ GolandLogo,
1772
+ IntellijIdeaLogo,
1773
+ PhpstormLogo,
1774
+ PycharmLogo,
1775
+ ResharperLogo,
1776
+ ResharperCppLogo,
1777
+ RiderLogo,
1778
+ RubymineLogo,
1779
+ RustroverLogo,
1780
+ WebstormLogo
1781
+ } from '@jetbrains/logos/react';
1782
+ import { Checkbox } from '@rescui/checkbox';
1783
+ import { Button } from '@rescui/button';
1784
+
1785
+ const products = [
1786
+ { title: 'Aqua', logo: <AquaLogo /> },
1787
+ { title: 'CLion', logo: <ClionLogo /> },
1788
+ { title: 'Code With Me', logo: <CodeWithMeLogo /> },
1789
+ { title: 'DataGrip', logo: <DatagripLogo /> },
1790
+ { title: 'DataSpell', logo: <DataspellLogo /> },
1791
+ { title: 'dotCover', logo: <DotcoverLogo /> },
1792
+ { title: 'dotMemory', logo: <DotmemoryLogo /> },
1793
+ { title: 'dotTrace', logo: <DottraceLogo /> },
1794
+ { title: 'GoLand', logo: <GolandLogo /> },
1795
+ { title: 'IntelliJ IDEA', logo: <IntellijIdeaLogo /> },
1796
+ { title: 'PhpStorm', logo: <PhpstormLogo /> },
1797
+ { title: 'PyCharm', logo: <PycharmLogo /> },
1798
+ { title: 'ReSharper', logo: <ResharperLogo /> },
1799
+ { title: 'ReSharper C++', logo: <ResharperCppLogo /> },
1800
+ { title: 'Rider', logo: <RiderLogo /> },
1801
+ { title: 'RubyMine', logo: <RubymineLogo /> },
1802
+ { title: 'RustRover', logo: <RustroverLogo /> },
1803
+ { title: 'WebStorm', logo: <WebstormLogo /> }
1804
+ ];
1805
+
1806
+ const FirstTile = () => {
1807
+ return (
1808
+ <div className="buy-tile">
1809
+ <div className="buy-tile__first-half">
1810
+ <h3 className={textCn('rs-h3')}>CLion</h3>
1811
+ <p
1812
+ className={cn(
1813
+ textCn('rs-text-2', { hardness: 'hard' }),
1814
+ 'rs-docs-offset-top-16'
1815
+ )}
1816
+ >
1817
+ A smart cross-platform IDE for C and C++
1818
+ </p>
1819
+ <div
1820
+ className={cn('buy-tile-content__products', 'rs-docs-offset-top-8')}
1821
+ >
1822
+ <div
1823
+ className={cn(
1824
+ 'buy-tile-content__products-item',
1825
+ textCn('rs-text-2')
1826
+ )}
1827
+ >
1828
+ <span className="buy-tile-content__products-logo">
1829
+ <ClionLogo />
1830
+ </span>
1831
+ <span>CLion</span>
1832
+ </div>
1833
+ </div>
1834
+ </div>
1835
+ <div className={'buy-tile__second-half'}>
1836
+ <Checkbox>
1837
+ Supercharge with{' '}
1838
+ <a href="#buy-tile" className={textCn('rs-link')}>
1839
+ JetBrains AI Pro
1840
+ </a>
1841
+ <p className={textCn('rs-text-2', { hardness: 'pale' })}>£158.00 (incl. VAT £189.60)</p>
1842
+ </Checkbox>
1843
+ <div className="rs-docs-offset-top-24">
1844
+ <div
1845
+ className={cn(
1846
+ 'buy-tile-content__price-row',
1847
+ 'buy-tile-content__price-row_baseline'
1848
+ )}
1849
+ >
1850
+ <div className={textCn('rs-text-3')}>
1851
+ <span className="buy-tile-content__nowrap">per user, </span>
1852
+ <span className="buy-tile-content__nowrap">first year</span>
1853
+ </div>
1854
+ <div
1855
+ className={cn(
1856
+ 'buy-tile-content__nowrap',
1857
+ textCn('rs-subtitle-2')
1858
+ )}
1859
+ >
1860
+ £185<sup className="buy-tile-content__price-sup">.00</sup>
1861
+ </div>
1862
+ </div>
1863
+ <p
1864
+ className={cn('buy-tile-content__text-right', textCn('rs-text-3'))}
1865
+ >
1866
+ incl. VAT £222.80
1867
+ </p>
1868
+ <div
1869
+ className={cn(
1870
+ 'buy-tile-content__price-row',
1871
+ 'rs-docs-offset-top-24'
1872
+ )}
1873
+ >
1874
+ <div
1875
+ className={cn('buy-tile-content__nowrap', textCn('rs-text-3'))}
1876
+ >
1877
+ second year
1878
+ </div>
1879
+ <div className="buy-tile-content__text-right">
1880
+ <p className={textCn('rs-text-3', { hardness: 'hard' })}>
1881
+ £148.00
1882
+ </p>
1883
+ <p className={textCn('rs-text-3')}>incl. VAT £177.60</p>
1884
+ </div>
1885
+ </div>
1886
+ <div
1887
+ className={cn(
1888
+ 'buy-tile-content__price-row',
1889
+ 'rs-docs-offset-top-24'
1890
+ )}
1891
+ >
1892
+ <div
1893
+ className={cn('buy-tile-content__nowrap', textCn('rs-text-3'))}
1894
+ >
1895
+ third year onwards
1896
+ </div>
1897
+ <div className="buy-tile-content__text-right">
1898
+ <p className={textCn('rs-text-3', { hardness: 'hard' })}>
1899
+ £111.00
1900
+ </p>
1901
+ <p className={textCn('rs-text-3')}>incl. VAT £133.20</p>
1902
+ </div>
1903
+ </div>
1904
+ </div>
1905
+ <div
1906
+ className={cn(
1907
+ 'buy-tile-content__footer-wrapper',
1908
+ 'rs-docs-offset-top-48'
1909
+ )}
1910
+ >
1911
+ <div className={'buy-tile-content__footer'}>
1912
+ <Button href="#buy-tile">Buy</Button>
1913
+ <div className={cn(textCn('rs-text-2'), 'buy-tile-content__links')}>
1914
+ <a
1915
+ href="#buy-tile"
1916
+ className={textCn('rs-link', { mode: 'standalone' })}
1917
+ >
1918
+ Get quote
1919
+ </a>
1920
+ </div>
1921
+ </div>
1922
+ </div>
1923
+ </div>
1924
+ </div>
1925
+ );
1926
+ };
1927
+
1928
+ const SecondTile = () => {
1929
+ return (
1930
+ <div className="buy-tile">
1931
+ <div className="buy-tile__first-half">
1932
+ <h3 className={textCn('rs-h3')}>All Products Pack</h3>
1933
+ <p
1934
+ className={cn(
1935
+ textCn('rs-text-2', { hardness: 'hard' }),
1936
+ 'rs-docs-offset-top-16'
1937
+ )}
1938
+ >
1939
+ Get 12 IDEs, 3 extensions, 2 profilers, and a collaborative
1940
+ development service – all in one subscription.
1941
+ </p>
1942
+ <Collapse
1943
+ icon="arrow"
1944
+ title="Includes 18 tools"
1945
+ disableLeftPadding
1946
+ mode="clear"
1947
+ className="rs-docs-offset-top-12"
1948
+ >
1949
+ <div className="buy-tile-content__products">
1950
+ {products.map(({ title, logo }) => (
1951
+ <div
1952
+ key={title}
1953
+ className={cn(
1954
+ 'buy-tile-content__products-item',
1955
+ textCn('rs-text-3')
1956
+ )}
1957
+ >
1958
+ <span className="buy-tile-content__products-logo">{logo}</span>
1959
+ <span>{title}</span>
1960
+ </div>
1961
+ ))}
1962
+ </div>
1963
+ </Collapse>
1964
+ </div>
1965
+ <div className={'buy-tile__second-half'}>
1966
+ <Checkbox>
1967
+ Supercharge with{' '}
1968
+ <a href="#buy-tile" className={textCn('rs-link')}>
1969
+ JetBrains AI Pro
1970
+ </a>
1971
+ <p className={textCn('rs-text-2', { hardness: 'pale' })}>£158.00 (incl. VAT £189.60)</p>
1972
+ </Checkbox>
1973
+ <div className="rs-docs-offset-top-24">
1974
+ <div
1975
+ className={cn(
1976
+ 'buy-tile-content__price-row',
1977
+ 'buy-tile-content__price-row_baseline'
1978
+ )}
1979
+ >
1980
+ <div className={textCn('rs-text-3')}>
1981
+ <span className="buy-tile-content__nowrap">per user, </span>
1982
+ <span className="buy-tile-content__nowrap">first year</span>
1983
+ </div>
1984
+ <div
1985
+ className={cn(
1986
+ 'buy-tile-content__nowrap',
1987
+ textCn('rs-subtitle-2')
1988
+ )}
1989
+ >
1990
+ £639<sup className="buy-tile-content__price-sup">.00</sup>
1991
+ </div>
1992
+ </div>
1993
+ <p
1994
+ className={cn('buy-tile-content__text-right', textCn('rs-text-3'))}
1995
+ >
1996
+ incl. VAT £766.80
1997
+ </p>
1998
+ <div
1999
+ className={cn(
2000
+ 'buy-tile-content__price-row',
2001
+ 'rs-docs-offset-top-24'
2002
+ )}
2003
+ >
2004
+ <div
2005
+ className={cn('buy-tile-content__nowrap', textCn('rs-text-3'))}
2006
+ >
2007
+ second year
2008
+ </div>
2009
+ <div className="buy-tile-content__text-right">
2010
+ <p className={textCn('rs-text-3', { hardness: 'hard' })}>
2011
+ £511.00
2012
+ </p>
2013
+ <p className={textCn('rs-text-3')}>incl. VAT £613.20</p>
2014
+ </div>
2015
+ </div>
2016
+ <div
2017
+ className={cn(
2018
+ 'buy-tile-content__price-row',
2019
+ 'rs-docs-offset-top-24'
2020
+ )}
2021
+ >
2022
+ <div
2023
+ className={cn('buy-tile-content__nowrap', textCn('rs-text-3'))}
2024
+ >
2025
+ third year onwards
2026
+ </div>
2027
+ <div className="buy-tile-content__text-right">
2028
+ <p className={textCn('rs-text-3', { hardness: 'hard' })}>
2029
+ £383.00
2030
+ </p>
2031
+ <p className={textCn('rs-text-3')}>incl. VAT £459.60</p>
2032
+ </div>
2033
+ </div>
2034
+ </div>
2035
+ <div
2036
+ className={cn(
2037
+ 'buy-tile-content__footer-wrapper',
2038
+ 'rs-docs-offset-top-48'
2039
+ )}
2040
+ >
2041
+ <div className={'buy-tile-content__footer'}>
2042
+ <Button href="#buy-tile">Buy</Button>
2043
+ <div className={cn(textCn('rs-text-2'), 'buy-tile-content__links')}>
2044
+ <a
2045
+ href="#buy-tile"
2046
+ className={textCn('rs-link', { mode: 'standalone' })}
2047
+ >
2048
+ Get quote
2049
+ </a>
2050
+ <a
2051
+ href="#buy-tile"
2052
+ className={textCn('rs-link', { mode: 'standalone' })}
2053
+ >
2054
+ Learn more
2055
+ </a>
2056
+ </div>
2057
+ </div>
2058
+ </div>
2059
+ </div>
2060
+ </div>
2061
+ );
2062
+ };
2063
+
2064
+ const Demo = () => {
2065
+ return (
2066
+ <div className={cn('buy-tiles-container', 'rs-docs-offset-top-16')}>
2067
+ <FirstTile />
2068
+ <StickyTag
2069
+ content={'Best offer'}
2070
+ color="white"
2071
+ backgroundColor="var(--rs-color-danger)"
2072
+ wrapperClassName="buy-tiles-container__tile-wrapper"
2073
+ >
2074
+ <SecondTile />
2075
+ </StickyTag>
2076
+ </div>
2077
+ );
2078
+ };
2079
+
2080
+ render(<Demo />);
2081
+ ```
2082
+
2083
+ ```postcss attached
2084
+ /* container styles */
2085
+
2086
+ .buy-tiles-container {
2087
+ display: grid;
2088
+ column-gap: 32px;
2089
+ row-gap: 32px;
2090
+ grid-template-columns: 5fr 5fr 2fr;
2091
+ grid-template-rows: auto;
2092
+
2093
+ @media (--md) {
2094
+ grid-template-columns: repeat(2, 1fr);
2095
+ }
2096
+
2097
+ @media (--sm) {
2098
+ grid-template-columns: repeat(1, 1fr);
2099
+ }
2100
+ }
2101
+
2102
+ .buy-tiles-container__tile-wrapper {
2103
+ display: grid;
2104
+ grid-template-rows: subgrid;
2105
+ grid-row: auto / span 2;
2106
+ }
2107
+
2108
+ /* tile styles */
2109
+
2110
+ .buy-tile {
2111
+ position: relative;
2112
+
2113
+ display: grid;
2114
+
2115
+ box-sizing: border-box;
2116
+ min-height: 0;
2117
+
2118
+ padding: 24px;
2119
+
2120
+ border: none;
2121
+ border-radius: 8px;
2122
+
2123
+ background-color: var(--rs-color-white);
2124
+ grid-template-columns: 1fr;
2125
+ grid-template-rows: subgrid;
2126
+ grid-row: auto / span 2;
2127
+ row-gap: 24px;
2128
+ box-shadow: inset 0 0 0 1px var(--rs-color-black-t20);
2129
+ }
2130
+
2131
+ .buy-tile__first-half {
2132
+ grid-row: 1 / span 1;
2133
+ }
2134
+ .buy-tile__second-half {
2135
+ grid-row: 2 / span 1;
2136
+
2137
+ display: flex;
2138
+ flex-direction: column;
2139
+ }
2140
+
2141
+ /* tile content styles */
2142
+
2143
+ .buy-tile-content__footer-wrapper {
2144
+ display: flex;
2145
+ flex: 1 0 auto;
2146
+ align-items: flex-end;
2147
+ }
2148
+
2149
+ .buy-tile-content__products {
2150
+ column-count: 2;
2151
+ }
2152
+
2153
+ .buy-tile-content__products-item {
2154
+ display: flex;
2155
+ column-gap: 8px;
2156
+
2157
+ align-items: center;
2158
+
2159
+ padding-top: 8px;
2160
+ }
2161
+
2162
+ .buy-tile-content__products-logo {
2163
+ width: 20px;
2164
+ height: 20px;
2165
+ }
2166
+
2167
+ .buy-tile-content__nowrap {
2168
+ white-space: nowrap;
2169
+ }
2170
+
2171
+ .buy-tile-content__text-right {
2172
+ text-align: right;
2173
+ }
2174
+
2175
+ .buy-tile-content__price-row {
2176
+ display: flex;
2177
+ justify-content: space-between;
2178
+
2179
+ &_baseline {
2180
+ align-items: baseline;
2181
+ }
2182
+ }
2183
+
2184
+ .buy-tile-content__price-sup {
2185
+ vertical-align: super;
2186
+
2187
+ font-size: medium;
2188
+ font-weight: 400;
2189
+ }
2190
+
2191
+ .buy-tile-content__footer {
2192
+ display: flex;
2193
+ flex-wrap: wrap;
2194
+ justify-content: space-between;
2195
+ align-items: center;
2196
+
2197
+ width: 100%;
2198
+ column-gap: 32px;
2199
+ row-gap: 24px;
2200
+ }
2201
+
2202
+ .buy-tile-content__links {
2203
+ display: flex;
2204
+ column-gap: 16px;
2205
+ row-gap: 12px;
2206
+
2207
+ flex-wrap: wrap;
2208
+ }
2209
+ ```
2210
+
2211
+ ### Alternative "buy" tile
2212
+
2213
+ ##### Details:
2214
+
2215
+ - Border has a non-transparent color
2216
+ - Align content between sibling tiles using subgrids
2217
+ - Use empty grid rows to support large forms in particular tiles
2218
+ - Use margins instead of `row-gap` because of empty grid rows
2219
+ - Change primary colors using css variables
2220
+ - Use pseudo-element for border to show it above content
2221
+
2222
+ ```tsx
2223
+ import { useState } from 'react';
2224
+ import cn from 'classnames';
2225
+ import { textCn } from '@rescui/typography';
2226
+ import { Tag, presets } from '@rescui/tag';
2227
+ import { Button } from '@rescui/button';
2228
+ import { Checkbox } from '@rescui/checkbox';
2229
+ import { DownOutlineIcon, UpOutlineIcon } from '@rescui/icons';
2230
+ import { RustroverLogo } from '@jetbrains/logos/react';
2231
+ import { Input } from '@rescui/input';
2232
+
2233
+ const FirstTile = () => {
2234
+ const [isFormOpened, setIsFormOpened] = useState(false);
2235
+
2236
+ const ButtonIcon = isFormOpened ? UpOutlineIcon : DownOutlineIcon;
2237
+
2238
+ return (
2239
+ <div
2240
+ className={cn(
2241
+ 'alternative-buy-tile',
2242
+ isFormOpened && 'alternative-buy-tile_with-form'
2243
+ )}
2244
+ >
2245
+ <div className="alternative-buy-tile__header">
2246
+ <div className="alternative-buy-tile-content__flex-row">
2247
+ <h3 className={textCn('rs-h3')}>RustRover</h3>
2248
+ <Tag {...presets['filled-light']}>non-commercial use</Tag>
2249
+ </div>
2250
+ </div>
2251
+ <div className="alternative-buy-tile__price">
2252
+ <div className={textCn('rs-digits-2')}>Free</div>
2253
+ </div>
2254
+ <div className="alternative-buy-tile__get">
2255
+ <Button
2256
+ mode="outline"
2257
+ className="alternative-buy-tile-content__button"
2258
+ icon={<ButtonIcon />}
2259
+ onClick={() => setIsFormOpened(v => !v)}
2260
+ >
2261
+ Get
2262
+ </Button>
2263
+ {isFormOpened && (
2264
+ <div>
2265
+ <Input
2266
+ placeholder="First name"
2267
+ size="m"
2268
+ className="rs-docs-offset-top-16"
2269
+ />
2270
+ <Input
2271
+ placeholder="Last name"
2272
+ size="m"
2273
+ className="rs-docs-offset-top-16"
2274
+ />
2275
+ <Input
2276
+ placeholder="Email"
2277
+ type="email"
2278
+ size="m"
2279
+ className="rs-docs-offset-top-16"
2280
+ />
2281
+ <Input
2282
+ placeholder="Select country"
2283
+ size="m"
2284
+ className="rs-docs-offset-top-16"
2285
+ />
2286
+ <p className={cn(textCn('rs-text-3'), 'rs-docs-offset-top-16')}>
2287
+ JetBrains may ask you for clarification on whether the license is
2288
+ used solely for non-commercial purposes. In the event of any
2289
+ doubts, JetBrains reserves the right to revoke the license for
2290
+ non-compliance with the non-commercial use conditions.
2291
+ </p>
2292
+ <Checkbox size="s" className="rs-docs-offset-top-8">
2293
+ I agree to the Toolbox Subscription Agreement for Non-Commercial
2294
+ Use
2295
+ </Checkbox>
2296
+ <Button mode="rock" size="s" className="rs-docs-offset-top-16">
2297
+ Submit
2298
+ </Button>
2299
+ </div>
2300
+ )}
2301
+ </div>
2302
+ <div className="alternative-buy-tile__footer">
2303
+ <div className="alternative-buy-tile-content__flex-row">
2304
+ <RustroverLogo className="alternative-buy-tile-content__footer-logo" />
2305
+ <span className={textCn('rs-text-2')}>RustRover</span>
2306
+ </div>
2307
+ <p
2308
+ className={cn(
2309
+ textCn('rs-text-2', { hardness: 'hard' }),
2310
+ 'rs-docs-offset-top-8'
2311
+ )}
2312
+ >
2313
+ A powerhouse IDE for Rust developers
2314
+ </p>
2315
+ <ul className={cn(textCn('rs-text-2'), textCn('rs-ul'))}>
2316
+ <li>Requires JetBrains Account</li>
2317
+ <li>Anonymous data is collected</li>
2318
+ </ul>
2319
+ </div>
2320
+ </div>
2321
+ );
2322
+ };
2323
+
2324
+ const SecondTile = () => {
2325
+ return (
2326
+ <div className="alternative-buy-tile">
2327
+ <div className="alternative-buy-tile__header">
2328
+ <h3 className={textCn('rs-h3')}>RustRover</h3>
2329
+ <Checkbox className="rs-docs-offset-top-24">
2330
+ Supercharge with{' '}
2331
+ <a href="#alternative-buy-tile" className={textCn('rs-link')}>
2332
+ JetBrains AI Pro
2333
+ </a>
2334
+ <p className={textCn('rs-text-2', { hardness: 'pale' })}>€100.00 (incl. VAT €119.00)</p>
2335
+ </Checkbox>
2336
+ </div>
2337
+ <div className="alternative-buy-tile__price">
2338
+ <div className="alternative-buy-tile-content__price-row">
2339
+ <div
2340
+ className={cn(
2341
+ 'alternative-buy-tile-content__nowrap',
2342
+ textCn('rs-digits-2')
2343
+ )}
2344
+ >
2345
+ €69
2346
+ <sup className="alternative-buy-tile-content__price-sup">.00</sup>
2347
+ </div>
2348
+ <span className={textCn('rs-text-3')}>a year</span>
2349
+ </div>
2350
+ <p className={textCn('rs-text-3')}>incl. VAT €82.11</p>
2351
+ </div>
2352
+ <div className="alternative-buy-tile__get">
2353
+ <Button mode="classic" className="alternative-buy-tile-content__button">
2354
+ Buy
2355
+ </Button>
2356
+ <p
2357
+ className={cn(
2358
+ textCn('rs-text-2'),
2359
+ 'rs-docs-offset-top-12',
2360
+ 'alternative-buy-tile-content__text-center'
2361
+ )}
2362
+ >
2363
+ <a
2364
+ href="#alternative-buy-tile"
2365
+ className={textCn('rs-link', { mode: 'standalone' })}
2366
+ >
2367
+ Get quote
2368
+ </a>
2369
+ </p>
2370
+ </div>
2371
+ <div className="alternative-buy-tile__footer">
2372
+ <div className="alternative-buy-tile-content__flex-row">
2373
+ <RustroverLogo className="alternative-buy-tile-content__footer-logo" />
2374
+ <span className={textCn('rs-text-2')}>RustRover</span>
2375
+ </div>
2376
+ <p
2377
+ className={cn(
2378
+ textCn('rs-text-2', { hardness: 'hard' }),
2379
+ 'rs-docs-offset-top-8'
2380
+ )}
2381
+ >
2382
+ A powerhouse IDE for Rust developers
2383
+ </p>
2384
+ <ul className={cn(textCn('rs-text-2'), textCn('rs-ul'))}>
2385
+ <li>
2386
+ Activated with a JetBrains Account, offline key, or license server
2387
+ </li>
2388
+ <li>Fine-grained control over shared data</li>
2389
+ </ul>
2390
+ </div>
2391
+ </div>
2392
+ );
2393
+ };
2394
+
2395
+ const ThirdTile = () => {
2396
+ return (
2397
+ <div className="alternative-buy-tile">
2398
+ <div className="alternative-buy-tile__header">
2399
+ <h3 className={textCn('rs-h3')}>Rust plugin for CLion</h3>
2400
+ </div>
2401
+ <div className="alternative-buy-tile__price">
2402
+ <div className="alternative-buy-tile-content__price-row">
2403
+ <div
2404
+ className={cn(
2405
+ 'alternative-buy-tile-content__nowrap',
2406
+ textCn('rs-digits-2')
2407
+ )}
2408
+ >
2409
+ €29
2410
+ <sup className="alternative-buy-tile-content__price-sup">.00</sup>
2411
+ </div>
2412
+ <span className={textCn('rs-text-3')}>a year</span>
2413
+ </div>
2414
+ <p className={textCn('rs-text-3')}>incl. VAT €34.51</p>
2415
+ </div>
2416
+ <div className="alternative-buy-tile__get">
2417
+ <Button mode="classic" className="alternative-buy-tile-content__button">
2418
+ Buy
2419
+ </Button>
2420
+ <p
2421
+ className={cn(
2422
+ textCn('rs-text-2'),
2423
+ 'rs-docs-offset-top-12',
2424
+ 'alternative-buy-tile-content__text-center'
2425
+ )}
2426
+ >
2427
+ <a
2428
+ href="#alternative-buy-tile"
2429
+ className={textCn('rs-link', { mode: 'standalone' })}
2430
+ >
2431
+ Get quote
2432
+ </a>
2433
+ </p>
2434
+ </div>
2435
+ <div className="alternative-buy-tile__footer">
2436
+ <div className="alternative-buy-tile-content__flex-row">
2437
+ <RustroverLogo className="alternative-buy-tile-content__footer-logo" />
2438
+ <span className={textCn('rs-text-2')}>RustRover</span>
2439
+ </div>
2440
+ <p
2441
+ className={cn(
2442
+ textCn('rs-text-2', { hardness: 'hard' }),
2443
+ 'rs-docs-offset-top-8'
2444
+ )}
2445
+ >
2446
+ The plugin provides Rust language support
2447
+ </p>
2448
+ <ul className={cn(textCn('rs-text-2'), textCn('rs-ul'))}>
2449
+ <li>30-day trial available</li>
2450
+ </ul>
2451
+ </div>
2452
+ </div>
2453
+ );
2454
+ };
2455
+
2456
+ const Demo = () => {
2457
+ return (
2458
+ <div className="alternative-buy-tiles-container">
2459
+ <div className="alternative-buy-tiles-container__tile-wrapper">
2460
+ <FirstTile />
2461
+ </div>
2462
+ <div className="alternative-buy-tiles-container__tile-wrapper">
2463
+ <SecondTile />
2464
+ </div>
2465
+ <div className="alternative-buy-tiles-container__tile-wrapper">
2466
+ <ThirdTile />
2467
+ </div>
2468
+ <div className="alternative-buy-tiles-container__tile-wrapper">
2469
+ <ThirdTile />
2470
+ </div>
2471
+ </div>
2472
+ );
2473
+ };
2474
+
2475
+ render(<Demo />);
2476
+ ```
2477
+
2478
+ ```postcss
2479
+ /* container styles */
2480
+
2481
+ .alternative-buy-tiles-container {
2482
+ --rs-color-primary-light-theme: #00874d;
2483
+ --rs-color-primary-dim-light-theme: #cce7db;
2484
+ --rs-color-primary-fog-light-theme: #339f71;
2485
+ --rs-color-primary-t-dim-light-theme: rgba(0, 135, 77, 0.8);
2486
+ --rs-color-primary-t-fog-light-theme: rgba(0, 135, 77, 0.2);
2487
+ --rs-color-primary-dark-theme: #00874d;
2488
+ --rs-color-primary-dim-dark-theme: #142f26;
2489
+ --rs-color-primary-fog-dark-theme: #057143;
2490
+ --rs-color-primary-t-dim-dark-theme: rgba(0, 135, 77, 0.8);
2491
+ --rs-color-primary-t-fog-dark-theme: rgba(0, 135, 77, 0.2);
2492
+
2493
+ display: grid;
2494
+ grid-template-columns: repeat(3, 1fr);
2495
+ grid-template-rows: auto;
2496
+
2497
+ column-gap: 32px;
2498
+ row-gap: 0;
2499
+
2500
+ margin-top: -32px;
2501
+
2502
+ @media (--md) {
2503
+ grid-template-columns: repeat(2, 1fr);
2504
+ }
2505
+
2506
+ @media (--sm) {
2507
+ grid-template-columns: 1fr;
2508
+ }
2509
+ }
2510
+
2511
+ .alternative-buy-tiles-container__tile-wrapper {
2512
+ display: grid;
2513
+ grid-template-columns: 1fr;
2514
+ grid-template-rows: subgrid;
2515
+ grid-row: auto / span 6;
2516
+
2517
+ margin-top: 32px;
2518
+ }
2519
+
2520
+ /* tile styles */
2521
+
2522
+ .alternative-buy-tile {
2523
+ --my-tile-padding: 24px;
2524
+
2525
+ position: relative;
2526
+
2527
+ display: grid;
2528
+ overflow: hidden;
2529
+
2530
+ box-sizing: border-box;
2531
+ min-height: 0;
2532
+
2533
+ padding: var(--my-tile-padding);
2534
+
2535
+ border-radius: 8px;
2536
+
2537
+ background-color: transparent;
2538
+ grid-template-columns: 1fr;
2539
+ grid-template-rows: subgrid;
2540
+ grid-row: auto / span 4;
2541
+
2542
+ &_with-form {
2543
+ grid-row: auto / span 6;
2544
+ }
2545
+
2546
+ &::after {
2547
+ position: absolute;
2548
+ top: 0;
2549
+ right: 0;
2550
+ bottom: 0;
2551
+ left: 0;
2552
+
2553
+ display: block;
2554
+
2555
+ border-radius: inherit;
2556
+
2557
+ content: '';
2558
+ pointer-events: none;
2559
+
2560
+ box-shadow: inset 0 0 0 1px var(--rs-color-grey-20);
2561
+ }
2562
+ }
2563
+
2564
+ .alternative-buy-tile__header {
2565
+ grid-row: 1;
2566
+ }
2567
+
2568
+ .alternative-buy-tile__price {
2569
+ grid-row: 2;
2570
+
2571
+ margin-top: 16px;
2572
+ }
2573
+
2574
+ .alternative-buy-tile__get {
2575
+ margin-top: 24px;
2576
+ margin-bottom: 24px;
2577
+
2578
+ grid-row: 3;
2579
+
2580
+ .alternative-buy-tile_with-form & {
2581
+ grid-row: 3 / span 3;
2582
+ }
2583
+ }
2584
+
2585
+ .alternative-buy-tile__footer {
2586
+ grid-row: 4;
2587
+
2588
+ margin: 0 calc(-1 * var(--my-tile-padding)) calc(-1 * var(--my-tile-padding));
2589
+ padding: var(--my-tile-padding);
2590
+
2591
+ background: var(--rs-color-grey-5);
2592
+
2593
+ .alternative-buy-tile_with-form & {
2594
+ grid-row: 6;
2595
+ }
2596
+ }
2597
+
2598
+ /* tile content styles */
2599
+
2600
+ .alternative-buy-tile-content__flex-row {
2601
+ display: flex;
2602
+ flex-wrap: wrap;
2603
+ column-gap: 8px;
2604
+ row-gap: 8px;
2605
+ }
2606
+
2607
+ .alternative-buy-tile-content__footer-logo {
2608
+ height: 24px;
2609
+ width: auto;
2610
+ }
2611
+
2612
+ .alternative-buy-tile-content__button {
2613
+ width: 100%;
2614
+ }
2615
+
2616
+ .alternative-buy-tile-content__text-center {
2617
+ text-align: center;
2618
+ }
2619
+
2620
+ .alternative-buy-tile-content__nowrap {
2621
+ white-space: nowrap;
2622
+ }
2623
+
2624
+ .alternative-buy-tile-content__price-row {
2625
+ display: flex;
2626
+ column-gap: 8px;
2627
+
2628
+ align-items: baseline;
2629
+ }
2630
+
2631
+ .alternative-buy-tile-content__price-sup {
2632
+ vertical-align: super;
2633
+
2634
+ font-size: medium;
2635
+ font-weight: 400;
2636
+ }
2637
+ ```
2638
+
2639
+ ## Controls
2640
+
2641
+ ### Carousel vertical controls
2642
+
2643
+ ##### Details:
2644
+
2645
+ - Without border
2646
+ - With `selected` state
2647
+
2648
+ - With filling progress bar
2649
+ - With hover and focus-visible states
2650
+ - Control autoplay animation duration with `--carousel-vertical-control-autoplay-duration` css variable
2651
+
2652
+ - With react state management
2653
+
2654
+ ```tsx
2655
+ import { useState } from 'react';
2656
+ import cn from 'classnames';
2657
+ import { textCn } from '@rescui/typography';
2658
+
2659
+ const slidesTitles = [
2660
+ 'First slide',
2661
+ 'Second slide',
2662
+ 'Third slide',
2663
+ 'Fourth slide'
2664
+ ];
2665
+
2666
+ const AUTOPLAY_DURATION = '8s';
2667
+
2668
+ const Demo = () => {
2669
+ const [currentItem, setCurrentItem] = useState(0);
2670
+ const [isAutoplayEnabled, setIsAutoplayEnabled] = useState(true);
2671
+
2672
+ return (
2673
+ <ul
2674
+ className={cn(
2675
+ 'carousel-vertical-controls',
2676
+ isAutoplayEnabled && 'carousel-vertical-controls_autoplay'
2677
+ )}
2678
+ style={{
2679
+ '--carousel-vertical-control-autoplay-duration': AUTOPLAY_DURATION
2680
+ }}
2681
+ >
2682
+ {slidesTitles.map((title, i) => (
2683
+ <li key={i}>
2684
+ <button
2685
+ type="button"
2686
+ className={cn(
2687
+ 'carousel-vertical-controls__item',
2688
+ i === currentItem && 'carousel-vertical-controls__item_selected'
2689
+ )}
2690
+ onAnimationEnd={() => {
2691
+ setCurrentItem(curVal =>
2692
+ curVal >= slidesTitles.length - 1 ? 0 : curVal + 1
2693
+ );
2694
+ }}
2695
+ onClick={() => {
2696
+ setCurrentItem(i);
2697
+ setIsAutoplayEnabled(false);
2698
+ }}
2699
+ >
2700
+ <p className={'carousel-vertical-controls__item-title'}>{title}</p>
2701
+ <div className={'carousel-vertical-controls__item-content'}>
2702
+ <p className={textCn('rs-text-2', { hardness: 'hard' })}>
2703
+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab
2704
+ aliquid asperiores aut earum fuga in incidunt ipsam maiores
2705
+ officia possimus quo repudiandae sequi similique temporibus.
2706
+ </p>
2707
+ </div>
2708
+ </button>
2709
+ </li>
2710
+ ))}
2711
+ </ul>
2712
+ );
2713
+ };
2714
+
2715
+ render(<Demo />);
2716
+ ```
2717
+
2718
+ ```postcss attached
2719
+ @import '@rescui/typography/lib/public-api.p.css';
2720
+
2721
+ @keyframes carousel-vertical-controls-progress-bar-fill {
2722
+ 0% {
2723
+ transform: translateX(-100%);
2724
+ }
2725
+ 100% {
2726
+ transform: translateX(0%);
2727
+ }
2728
+ }
2729
+
2730
+ .carousel-vertical-controls {
2731
+ display: flex;
2732
+ flex-direction: column;
2733
+ row-gap: 8px;
2734
+
2735
+ width: 350px;
2736
+ }
2737
+
2738
+ .carousel-vertical-controls__item-title {
2739
+ @mixin rs-typography-text-2-full;
2740
+ }
2741
+
2742
+ .carousel-vertical-controls__item-content {
2743
+ display: none;
2744
+
2745
+ margin-top: 12px;
2746
+ }
2747
+
2748
+ .carousel-vertical-controls__item {
2749
+ position: relative;
2750
+
2751
+ display: block;
2752
+
2753
+ overflow: hidden;
2754
+
2755
+ box-sizing: border-box;
2756
+
2757
+ width: 100%;
2758
+ min-height: 0;
2759
+ padding: 12px 24px;
2760
+
2761
+ border: none;
2762
+ border-radius: 24px;
2763
+
2764
+ cursor: pointer;
2765
+ text-align: inherit;
2766
+
2767
+ background-color: transparent;
2768
+
2769
+ appearance: none;
2770
+
2771
+ &,
2772
+ &:hover {
2773
+ text-decoration: none;
2774
+ }
2775
+
2776
+ /* reset user agent stylesheet at Chrome */
2777
+ &:focus-visible {
2778
+ outline: none;
2779
+
2780
+ box-shadow: 0 0 0 4px var(--rs-color-primary-dim-light-theme);
2781
+ }
2782
+
2783
+ @media (hover: hover) {
2784
+ &:hover {
2785
+ & .carousel-vertical-controls__item-title {
2786
+ @mixin rs-typography-hardness-hard;
2787
+ }
2788
+ }
2789
+ }
2790
+
2791
+ &:active {
2792
+ & .carousel-vertical-controls__item-title {
2793
+ @mixin rs-typography-hardness-hard;
2794
+ }
2795
+ }
2796
+ }
2797
+
2798
+ .carousel-vertical-controls__item_selected {
2799
+ --rs-theme-dark: 1;
2800
+
2801
+ overflow: hidden;
2802
+
2803
+ padding: 24px;
2804
+
2805
+ background-color: #5848cf;
2806
+
2807
+ & > * {
2808
+ position: relative;
2809
+ }
2810
+
2811
+ & .carousel-vertical-controls__item-title {
2812
+ @mixin rs-typography-h4;
2813
+ }
2814
+
2815
+ & .carousel-vertical-controls__item-content {
2816
+ display: block;
2817
+ }
2818
+
2819
+ .carousel-vertical-controls_autoplay &::before {
2820
+ position: absolute;
2821
+ top: 0;
2822
+ right: 0;
2823
+ bottom: 0;
2824
+ left: 0;
2825
+
2826
+ display: block;
2827
+
2828
+ border-radius: 0;
2829
+
2830
+ content: '';
2831
+ pointer-events: none;
2832
+
2833
+ background-color: var(--rs-color-primary-light-theme);
2834
+
2835
+ animation: var(--carousel-vertical-control-autoplay-duration, 8s) linear both
2836
+ carousel-vertical-controls-progress-bar-fill;
2837
+ }
2838
+ }
2839
+ ```
2840
+
2841
+ ### Carousel horizontal controls
2842
+
2843
+ ##### Details:
2844
+
2845
+ - Without border
2846
+ - With `selected` state
2847
+
2848
+ - With filling progress bar
2849
+ - With hover and focus-visible states
2850
+ - Control autoplay animation duration with `--carousel-horizontal-control-autoplay-duration` css variable
2851
+
2852
+ - With react state management
2853
+
2854
+ ```tsx
2855
+ import { useState } from 'react';
2856
+ import cn from 'classnames';
2857
+ import { textCn } from '@rescui/typography';
2858
+
2859
+ const slidesTitles = [
2860
+ 'First slide',
2861
+ 'Second slide',
2862
+ 'Third slide',
2863
+ 'Fourth slide'
2864
+ ];
2865
+
2866
+ const AUTOPLAY_DURATION = '8s';
2867
+
2868
+ const Demo = () => {
2869
+ const [currentItem, setCurrentItem] = useState(0);
2870
+ const [isAutoplayEnabled, setIsAutoplayEnabled] = useState(true);
2871
+
2872
+ return (
2873
+ <ul
2874
+ className={cn(
2875
+ 'carousel-horizontal-controls',
2876
+ isAutoplayEnabled && 'carousel-horizontal-controls_autoplay',
2877
+ 'jb-offset-top-32'
2878
+ )}
2879
+ style={{
2880
+ '--carousel-vertical-control-autoplay-duration': AUTOPLAY_DURATION
2881
+ }}
2882
+ >
2883
+ {slidesTitles.map((title, i) => (
2884
+ <li key={i}>
2885
+ <button
2886
+ type="button"
2887
+ className={cn(
2888
+ 'carousel-horizontal-controls__item',
2889
+ i === currentItem && 'carousel-horizontal-controls__item_selected'
2890
+ )}
2891
+ onAnimationEnd={() => {
2892
+ setCurrentItem(curVal =>
2893
+ curVal >= slidesTitles.length - 1 ? 0 : curVal + 1
2894
+ );
2895
+ }}
2896
+ onClick={() => {
2897
+ setCurrentItem(i);
2898
+ setIsAutoplayEnabled(false);
2899
+ }}
2900
+ >
2901
+ <p
2902
+ className={cn(
2903
+ textCn('rs-text-2'),
2904
+ 'carousel-horizontal-controls__item-title'
2905
+ )}
2906
+ >
2907
+ {title}
2908
+ </p>
2909
+ </button>
2910
+ </li>
2911
+ ))}
2912
+ </ul>
2913
+ );
2914
+ };
2915
+
2916
+ render(<Demo />);
2917
+ ```
2918
+
2919
+ ```postcss
2920
+ @import '@rescui/typography/lib/public-api.p.css';
2921
+
2922
+ @keyframes carousel-horizontal-controls-progress-bar-fill {
2923
+ 0% {
2924
+ transform: translateX(-100%);
2925
+ }
2926
+ 80% {
2927
+ transform: translateX(0%);
2928
+ }
2929
+ 100% {
2930
+ transform: translateX(0%);
2931
+ }
2932
+ }
2933
+
2934
+ .carousel-horizontal-controls {
2935
+ display: flex;
2936
+ column-gap: 8px;
2937
+ }
2938
+
2939
+ .carousel-horizontal-controls__item {
2940
+ --rs-theme-dark: 1;
2941
+ position: relative;
2942
+
2943
+ display: block;
2944
+
2945
+ overflow: hidden;
2946
+
2947
+ box-sizing: border-box;
2948
+
2949
+ min-height: 0;
2950
+ padding: 24px;
2951
+
2952
+ border: none;
2953
+ border-radius: 24px;
2954
+
2955
+ cursor: pointer;
2956
+ text-align: inherit;
2957
+
2958
+ background-color: var(--rs-color-black-t80);
2959
+
2960
+ appearance: none;
2961
+
2962
+ &,
2963
+ &:hover {
2964
+ text-decoration: none;
2965
+ }
2966
+
2967
+ /* reset user agent stylesheet at Chrome */
2968
+ &:focus-visible {
2969
+ outline: none;
2970
+
2971
+ box-shadow: 0 0 0 4px var(--rs-color-primary-dim-light-theme);
2972
+ }
2973
+
2974
+ @media (hover: hover) {
2975
+ &:hover {
2976
+ & .carousel-horizontal-controls__item-title {
2977
+ @mixin rs-typography-hardness-hard;
2978
+ }
2979
+ }
2980
+ }
2981
+
2982
+ &:active {
2983
+ & .carousel-horizontal-controls__item-title {
2984
+ @mixin rs-typography-hardness-hard;
2985
+ }
2986
+ }
2987
+ }
2988
+
2989
+ .carousel-horizontal-controls__item_selected {
2990
+ overflow: hidden;
2991
+
2992
+ background-color: var(--rs-color-black-t90);
2993
+
2994
+ & .carousel-horizontal-controls__item-title {
2995
+ @mixin rs-typography-hardness-hard;
2996
+ }
2997
+
2998
+ & > * {
2999
+ position: relative;
3000
+ }
3001
+
3002
+ .carousel-horizontal-controls_autoplay &::before {
3003
+ position: absolute;
3004
+ top: 0;
3005
+ right: 0;
3006
+ bottom: 0;
3007
+ left: 0;
3008
+
3009
+ display: block;
3010
+
3011
+ border-radius: 0;
3012
+
3013
+ content: '';
3014
+ pointer-events: none;
3015
+
3016
+ background-color: var(--rs-color-primary-light-theme);
3017
+
3018
+ animation: var(--carousel-horizontal-control-autoplay-duration, 8s) linear both
3019
+ carousel-vertical-controls-progress-bar-fill;
3020
+ }
3021
+ }
3022
+ ```
3023
+
3024
+ ## Guide
3025
+
3026
+ ### Description
3027
+
3028
+ This guide provides a detailed explanation of how to create various types of cards from scratch.
3029
+
3030
+ The process is not strictly linear, so you can skip steps that are currently not relevant to you.
3031
+
3032
+ Additionally, this guide includes complete examples for the following types of cards:
3033
+
3034
+ - [Clickable cards](/cards/clickable/)
3035
+ - [Tiles](/cards/tiles/)
3036
+ - [Controls](/cards/controls/)
3037
+
3038
+ ### Clickable cards
3039
+
3040
+ #### 1\. Template
3041
+
3042
+ Clickable cards should always be implemented using either a `<button>` or an `<a>` element. This requires manually resetting browser user-agent styles. Use the following CSS styles to create the foundation:
3043
+
3044
+ ```postcss
3045
+ .my-card {
3046
+ position: relative;
3047
+
3048
+ display: block;
3049
+
3050
+ overflow: hidden;
3051
+
3052
+ box-sizing: border-box;
3053
+ min-height: 0;
3054
+
3055
+ border: none;
3056
+ border-radius: 0;
3057
+
3058
+ cursor: pointer;
3059
+ text-align: inherit;
3060
+
3061
+ background-color: var(--rs-color-white);
3062
+
3063
+ appearance: none;
3064
+ padding: 0;
3065
+
3066
+ &,
3067
+ &:hover {
3068
+ text-decoration: none;
3069
+ }
3070
+
3071
+ &:focus-visible {
3072
+ outline: none;
3073
+ }
3074
+ }
3075
+ ```
3076
+
3077
+ Some styles may require further updates in the next steps.
3078
+
3079
+ #### 2\. Add paddings and `border-radius`
3080
+
3081
+ In this step, update the padding and `border-radius` for your card:
3082
+
3083
+ ```postcss
3084
+ .my-card {
3085
+ /* ... */
3086
+
3087
+ border-radius: 8px;
3088
+ padding: 24px;
3089
+
3090
+ /* ... */
3091
+ }
3092
+ ```
3093
+
3094
+ #### 3\. Add a border
3095
+
3096
+ Due to [browser incompatibility issues with `border` styles in scaled UI](https://stackoverflow.com/questions/46105681/get-wrong-border-width-on-firefox), it is best to avoid the `border` property if you plan to dynamically change its width. Instead, use the `box-shadow` property for better consistency.
3097
+
3098
+ Alternatively, you can use a `::before` pseudo-element for the border, but using `box-shadow` is recommended whenever possible.
3099
+
3100
+ Here’s how to add a border using the `box-shadow` property:
3101
+
3102
+ ```postcss
3103
+ .my-card {
3104
+ /* ... */
3105
+
3106
+ box-shadow: inset 0 0 0 var(--my-card-border-width) var(
3107
+ --my-card-border-color
3108
+ );
3109
+ transition: box-shadow 100ms;
3110
+
3111
+ /* ... */
3112
+ }
3113
+ ```
3114
+
3115
+ Next step, we add card state changes for the border. To make styling for various card states easier to manage, utilize CSS variables for border width and color:
3116
+
3117
+ ```postcss
3118
+ .my-card {
3119
+ --my-card-border-width: 1px;
3120
+ --my-card-border-color: var(--rs-color-black-t20);
3121
+
3122
+ @media (hover: hover) {
3123
+ &:hover {
3124
+ --my-card-border-color: var(--rs-color-black);
3125
+ }
3126
+ }
3127
+
3128
+ &:focus-visible,
3129
+ &:active {
3130
+ --my-card-border-color: var(--rs-color-black);
3131
+ }
3132
+
3133
+ &:active {
3134
+ --my-card-border-width: 2px;
3135
+ }
3136
+ }
3137
+ ```
3138
+
3139
+ By combining these updates with previous steps, the complete code will look like this:
3140
+
3141
+ ```postcss
3142
+ .my-card {
3143
+ --my-card-border-width: 1px;
3144
+ --my-card-border-color: var(--rs-color-black-t20);
3145
+
3146
+ position: relative;
3147
+
3148
+ display: block;
3149
+
3150
+ overflow: hidden;
3151
+
3152
+ box-sizing: border-box;
3153
+ min-height: 0;
3154
+ padding: 24px;
3155
+
3156
+ border: none;
3157
+ border-radius: 8px;
3158
+
3159
+ cursor: pointer;
3160
+ text-align: inherit;
3161
+
3162
+ background-color: var(--rs-color-white);
3163
+ box-shadow: inset 0 0 0 var(--my-card-border-width) var(
3164
+ --my-card-border-color
3165
+ );
3166
+
3167
+ transition: box-shadow 100ms;
3168
+ appearance: none;
3169
+
3170
+ &,
3171
+ &:hover {
3172
+ text-decoration: none;
3173
+ }
3174
+
3175
+ &:focus-visible {
3176
+ outline: none;
3177
+ }
3178
+
3179
+ @media (hover: hover) {
3180
+ &:hover {
3181
+ --my-card-border-color: var(--rs-color-black);
3182
+ }
3183
+ }
3184
+
3185
+ &:focus-visible,
3186
+ &:active {
3187
+ --my-card-border-color: var(--rs-color-black);
3188
+ }
3189
+
3190
+ &:active {
3191
+ --my-card-border-width: 2px;
3192
+ }
3193
+ }
3194
+ ```
3195
+
3196
+ ##### 3.1 Animate border color only
3197
+
3198
+ If you want only to animate border color, you can utilise `@property` at-rule. But keep in mind, it works only for [newer browsers](https://developer.mozilla.org/en-US/docs/Web/CSS/@property#browser_compatibility):
3199
+
3200
+ ```postcss
3201
+ @property --my-card-border-color {
3202
+ syntax: '<color>';
3203
+ inherits: true;
3204
+ initial-value: transparent;
3205
+ }
3206
+
3207
+ .my-card {
3208
+ /* ... */
3209
+
3210
+ box-shadow: inset 0 0 0 var(--my-card-border-width) var(
3211
+ --my-card-border-color
3212
+ );
3213
+ transition: --my-card-border-color 100ms;
3214
+
3215
+ /* ... */
3216
+ }
3217
+ ```
3218
+
3219
+ ##### 3.2 Border as a pseudo-element
3220
+
3221
+ When parts of the card, such as large header images, need to stretch fully to the edges, the `box-shadow` border may not render correctly. To fix this, you can use a `::after` pseudo-element to place the border:
3222
+
3223
+ ```postcss
3224
+ .my-card {
3225
+ /* ... */
3226
+
3227
+ &::after {
3228
+ position: absolute;
3229
+ top: 0;
3230
+ right: 0;
3231
+ bottom: 0;
3232
+ left: 0;
3233
+
3234
+ display: block;
3235
+
3236
+ border-radius: inherit;
3237
+
3238
+ content: '';
3239
+ pointer-events: none;
3240
+
3241
+ box-shadow: inset 0 0 0 var(--my-card-border-width) var(
3242
+ --my-card-border-color
3243
+ );
3244
+ transition: box-shadow 100ms;
3245
+ }
3246
+ }
3247
+ ```
3248
+
3249
+ Check out [the corresponding example](/cards/tiles/#newsletter-tiles).
3250
+
3251
+ #### 4\. Optional additions
3252
+
3253
+ ##### 4.1 Glow hover effect
3254
+
3255
+ When applying hover effects like the `useGlowHover` effect, ensure the card's state changes are done correctly.
3256
+
3257
+ For example, instead of modifying the border for `:hover` and `:active` states, use the `enableBurst` option of the `useGlowHover` hook for smoother transitions:
3258
+
3259
+ ```jsx
3260
+ import cn from 'classnames';
3261
+ import { textCn } from '@rescui/typography';
3262
+ import { useGlowHover } from '@rescui/use-glow-hover';
3263
+
3264
+ const Demo = () => {
3265
+ const ref = useGlowHover({ preset: 'transparent-blue', enableBurst: true });
3266
+
3267
+ return (
3268
+ <button
3269
+ type="button"
3270
+ ref={ref}
3271
+ className="my-card"
3272
+ style={{ maxWidth: 400 }}
3273
+ >
3274
+ <h3 className={textCn('rs-h3')}>Card title</h3>
3275
+ <p className={cn(textCn('rs-text-2'), 'rs-docs-offset-top-24')}>
3276
+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab animi
3277
+ aspernatur autem commodi consequuntur esse laudantium nobis numquam
3278
+ officiis omnis, perferendis perspiciatis, possimus qui quidem, quis
3279
+ repellat tempore unde veniam?
3280
+ </p>
3281
+ </button>
3282
+ );
3283
+ };
3284
+
3285
+ render(<Demo />);
3286
+ ```
3287
+
3288
+ Remove the need to adjust borders for hover states in the CSS:
3289
+
3290
+ ```postcss
3291
+ .my-card {
3292
+ --my-card-border-width: 1px;
3293
+ --my-card-border-color: var(--rs-color-black-t20);
3294
+
3295
+ position: relative;
3296
+
3297
+ display: block;
3298
+
3299
+ overflow: hidden;
3300
+
3301
+ box-sizing: border-box;
3302
+ min-height: 0;
3303
+
3304
+ border: none;
3305
+ border-radius: 8px;
3306
+
3307
+ cursor: pointer;
3308
+ text-align: inherit;
3309
+
3310
+ background-color: var(--rs-color-white);
3311
+ box-shadow: inset 0 0 0 var(--my-card-border-width) var(
3312
+ --my-card-border-color
3313
+ );
3314
+
3315
+ appearance: none;
3316
+ padding: 24px;
3317
+
3318
+ &,
3319
+ &:hover {
3320
+ text-decoration: none;
3321
+ }
3322
+
3323
+ &:focus-visible {
3324
+ outline: none;
3325
+ }
3326
+ }
3327
+ ```
3328
+
3329
+ Check out [the corresponding example](/cards/clickable/#with-glow-hover).
3330
+
3331
+ ##### 4.2 Forward states to the action element inside
3332
+
3333
+ It is recommended to forward states such as `:hover`, `:active`, and `:focus-visible` from the clickable card to the interactive elements inside, such as `button` or `rs-link`. This ensures that the inner elements behave consistently with the overall card's interaction states.
3334
+
3335
+ This can be achieved using [PostCSS mixins](https://github.com/postcss/postcss-mixins).
3336
+
3337
+ ##### Button inside the card
3338
+
3339
+ Here is an example of using a `Button` component within the card:
3340
+
3341
+ ```postcss
3342
+ @import '@rescui/button/lib/public-api.p.css';
3343
+
3344
+ .my-card {
3345
+ /* ... */
3346
+
3347
+ @media (hover: hover) {
3348
+ &:hover &__button {
3349
+ @mixin rs-button-state-hover;
3350
+ }
3351
+ }
3352
+
3353
+ &:active &__button {
3354
+ @mixin rs-button-state-active;
3355
+ }
3356
+
3357
+ &:focus-visible &__button {
3358
+ @mixin rs-button-state-focus-visible;
3359
+ }
3360
+
3361
+ &:active:focus-visible:not(:hover) &__button {
3362
+ @mixin rs-button-state-active-focus-visible-no-hover;
3363
+ }
3364
+ }
3365
+ ```
3366
+
3367
+ Additionally, remember to make the `Button` a static element inside the card:
3368
+
3369
+ ```jsx
3370
+ import { Button } from '@rescui/button';
3371
+
3372
+ render(
3373
+ <Button notFocusable className="my-card__button">
3374
+ Download
3375
+ </Button>
3376
+ );
3377
+ ```
3378
+
3379
+ Check out [the corresponding example](/cards/clickable/#with-action-button-inside).
3380
+
3381
+ ##### rs-link inside the card
3382
+
3383
+ Similar behavior can also be applied when using an `rs-link` element within the card:
3384
+
3385
+ ```postcss
3386
+ @import '@rescui/typography/lib/public-api.p.css';
3387
+
3388
+ .my-card {
3389
+ /* ... */
3390
+
3391
+ @media (hover: hover) {
3392
+ &:hover &__link {
3393
+ @mixin rs-typography-link-state-hover;
3394
+ }
3395
+ }
3396
+
3397
+ &:focus-visible &__link {
3398
+ @mixin rs-typography-link-state-focus-visible;
3399
+ }
3400
+ }
3401
+ ```
3402
+
3403
+ As with the `Button`, ensure the `rs-link` is a static element:
3404
+
3405
+ ```jsx
3406
+ import cn from 'classnames';
3407
+ import { textCn } from '@rescui/typography';
3408
+
3409
+ render(
3410
+ <span className={cn(textCn('rs-link'), 'my-card__link')}>Download</span>
3411
+ );
3412
+ ```
3413
+
3414
+ Check out [the corresponding example](/cards/clickable/#with-action-link-inside).
3415
+
3416
+ ### Tiles
3417
+
3418
+ Tiles are non-clickable and generally non-interactive cards.
3419
+
3420
+ They are simpler to create compared to clickable cards. However, their complexity may arise from additional elements such as interaction with `StickyTag`, content alignment with subgrids, or using flexbox.
3421
+
3422
+ #### Template: With Border
3423
+
3424
+ To maintain consistent presentation across all cards, it is recommended to use the `box-shadow` property as a border for tiles:
3425
+
3426
+ ```postcss
3427
+ .my-tile {
3428
+ position: relative;
3429
+
3430
+ display: block;
3431
+
3432
+ box-sizing: border-box;
3433
+ min-height: 0;
3434
+
3435
+ border: none;
3436
+ border-radius: 8px;
3437
+
3438
+ background-color: var(--rs-color-white);
3439
+ box-shadow: inset 0 0 0 1px var(--rs-color-black-t20);
3440
+
3441
+ appearance: none;
3442
+ padding: 24px;
3443
+ }
3444
+ ```
3445
+
3446
+ #### Template: Without Border
3447
+
3448
+ If you decide not to use a border, the tile should have a contrasting background to differentiate it from the surrounding content:
3449
+
3450
+ ```postcss
3451
+ .my-tile {
3452
+ position: relative;
3453
+
3454
+ display: block;
3455
+
3456
+ box-sizing: border-box;
3457
+ min-height: 0;
3458
+
3459
+ border: none;
3460
+ border-radius: 8px;
3461
+
3462
+ background-color: var(--rs-color-grey-5);
3463
+
3464
+ padding: 24px;
3465
+ }
3466
+ ```
3467
+
3468
+ #### Template: With a stretched image
3469
+
3470
+ This approach aims to stretch the image to cover the edges of the tile. Using CSS variables here makes it easier to manage the layout.
3471
+
3472
+ To display the border above stretched content, use a pseudo-element instead of a simple `box-shadow`. Also, do not use a transparent color for the border — use a solid one instead.
3473
+
3474
+ ```tsx
3475
+ import cn from 'classnames';
3476
+ import { textCn } from '@rescui/typography';
3477
+ import tileImage from '@/parts/cards/newsletter1.webp';
3478
+
3479
+ const Demo = () => {
3480
+ return (
3481
+ <div className="my-tile-with-image" style={{ maxWidth: 450 }}>
3482
+ <img
3483
+ className="my-tile-with-image__img"
3484
+ alt="keymaps"
3485
+ src={tileImage.src}
3486
+ />
3487
+ <h3 className={textCn('rs-h3')}>
3488
+ JetBrains Community Newsletter (Monthly)
3489
+ </h3>
3490
+ <p className={cn(textCn('rs-text-2'), 'rs-docs-offset-top-16')}>
3491
+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab ad aperiam
3492
+ atque aut debitis, distinctio harum nam nobis nulla pariatur
3493
+ perspiciatis.
3494
+ </p>
3495
+ </div>
3496
+ );
3497
+ };
3498
+
3499
+ render(<Demo />);
3500
+ ```
3501
+
3502
+ ```postcss attached
3503
+ .my-tile-with-image {
3504
+ --my-tile-padding: 24px;
3505
+
3506
+ position: relative;
3507
+
3508
+ overflow: hidden;
3509
+
3510
+ box-sizing: border-box;
3511
+ min-height: 0;
3512
+ padding: var(--my-tile-padding);
3513
+
3514
+ border: none;
3515
+ border-radius: 8px;
3516
+
3517
+ background-color: var(--rs-color-white);
3518
+
3519
+ &__img {
3520
+ position: relative;
3521
+ top: calc(-1 * var(--my-tile-padding));
3522
+ left: calc(-1 * var(--my-tile-padding));
3523
+
3524
+ display: flex;
3525
+ justify-content: center;
3526
+ align-items: center;
3527
+
3528
+ width: calc(100% + 2 * var(--my-tile-padding));
3529
+ height: 212px;
3530
+ max-height: 212px;
3531
+ object-fit: cover;
3532
+ object-position: right;
3533
+ }
3534
+
3535
+ &::after {
3536
+ position: absolute;
3537
+ top: 0;
3538
+ right: 0;
3539
+ bottom: 0;
3540
+ left: 0;
3541
+
3542
+ display: block;
3543
+
3544
+ border-radius: inherit;
3545
+
3546
+ content: '';
3547
+ pointer-events: none;
3548
+
3549
+ box-shadow: inset 0 0 0 1px var(--rs-color-grey-20);
3550
+ }
3551
+ }
3552
+ ```
3553
+
3554
+ #### Template: Aligning Content Between Sibling Cards
3555
+
3556
+ In certain scenarios, content across sibling cards needs to be aligned so that it appears at the same height.
3557
+
3558
+ One way to achieve this is by using flexbox with `flex-direction: column`. However, this method works only for basic cases. When the content has dynamic or differing heights between cards, it may not be sufficient.
3559
+
3560
+ The preferred approach for more complex layouts is to use [CSS Grid’s subgrid feature](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_grid_layout/Subgrid). Keep in mind this approach has certain [browser support limitations](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_grid_layout/Subgrid#browser_compatibility). Although subgrid layouts remain functional in unsupported browsers, you should carefully test their regression and viability in older browsers.
3561
+
3562
+ ```tsx
3563
+ import { textCn } from '@rescui/typography';
3564
+
3565
+ const AlignedTile = ({ title, children }) => {
3566
+ return (
3567
+ <div className="my-aligned-tile">
3568
+ <div className="my-aligned-tile__content">
3569
+ <h3 className={textCn('rs-h3')}>{title}</h3>
3570
+ </div>
3571
+ <div className="my-aligned-tile__footer">
3572
+ <p className={textCn('rs-text-2')}>{children}</p>
3573
+ </div>
3574
+ </div>
3575
+ );
3576
+ };
3577
+
3578
+ const Demo = () => {
3579
+ return (
3580
+ <div className="my-aligned-tiles-container">
3581
+ <AlignedTile
3582
+ title={'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'}
3583
+ >
3584
+ Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
3585
+ enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
3586
+ aliquip ex ea commodo consequat.
3587
+ </AlignedTile>
3588
+ <AlignedTile title={'Where are nebulae?'}>
3589
+ The closest known nebula to Earth is called the Helix Nebula. It is the
3590
+ remnant of a dying star — possibly one like the Sun. It is approximately
3591
+ 700 light-years away from Earth.
3592
+ </AlignedTile>
3593
+ <AlignedTile title={'The drive to develop'}>
3594
+ We help developers work faster by automating common, repetitive tasks to
3595
+ enable them to stay focused on code design and the big picture. We
3596
+ provide tools to explore and familiarize with code bases faster.
3597
+ </AlignedTile>
3598
+ </div>
3599
+ );
3600
+ };
3601
+
3602
+ render(<Demo />);
3603
+ ```
3604
+
3605
+ ```postcss
3606
+ .my-aligned-tiles-container {
3607
+ display: grid;
3608
+ grid-template-rows: auto;
3609
+ grid-template-columns: repeat(3, 1fr);
3610
+ column-gap: 32px;
3611
+ row-gap: 24px;
3612
+
3613
+ @media (--md) {
3614
+ grid-template-columns: repeat(2, 1fr);
3615
+ }
3616
+
3617
+ @media (--sm) {
3618
+ grid-template-columns: repeat(1, 1fr);
3619
+ }
3620
+ }
3621
+
3622
+ .my-aligned-tile {
3623
+ position: relative;
3624
+
3625
+ display: grid;
3626
+ overflow: hidden;
3627
+
3628
+ box-sizing: border-box;
3629
+ min-height: 0;
3630
+ padding: 24px;
3631
+
3632
+ border: none;
3633
+ border-radius: 8px;
3634
+
3635
+ background-color: var(--rs-color-white);
3636
+ grid-template-columns: 1fr;
3637
+ grid-template-rows: subgrid;
3638
+ grid-row: auto / span 2;
3639
+ row-gap: 16px;
3640
+ box-shadow: inset 0 0 0 1px var(--rs-color-black-t20);
3641
+
3642
+ &__content {
3643
+ grid-row: 1;
3644
+ }
3645
+
3646
+ &__footer {
3647
+ grid-row: 2;
3648
+ }
3649
+ }
3650
+ ```