@transferwise/components 46.166.0 → 46.167.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/build/CardContainer/CardContainer.js +34 -0
  2. package/build/CardContainer/CardContainer.js.map +1 -0
  3. package/build/CardContainer/CardContainer.mjs +30 -0
  4. package/build/CardContainer/CardContainer.mjs.map +1 -0
  5. package/build/CardWidget/CardWidget.js +98 -0
  6. package/build/CardWidget/CardWidget.js.map +1 -0
  7. package/build/CardWidget/CardWidget.mjs +94 -0
  8. package/build/CardWidget/CardWidget.mjs.map +1 -0
  9. package/build/CardWidget/CardWidgetAction.js +73 -0
  10. package/build/CardWidget/CardWidgetAction.js.map +1 -0
  11. package/build/CardWidget/CardWidgetAction.mjs +68 -0
  12. package/build/CardWidget/CardWidgetAction.mjs.map +1 -0
  13. package/build/ListItem/ListItem.js +2 -0
  14. package/build/ListItem/ListItem.js.map +1 -1
  15. package/build/ListItem/ListItem.mjs +2 -0
  16. package/build/ListItem/ListItem.mjs.map +1 -1
  17. package/build/index.js +4 -0
  18. package/build/index.js.map +1 -1
  19. package/build/index.mjs +2 -0
  20. package/build/index.mjs.map +1 -1
  21. package/build/main.css +194 -0
  22. package/build/styles/CardContainer/CardContainer.css +108 -0
  23. package/build/styles/CardWidget/CardWidget.css +78 -0
  24. package/build/styles/ListItem/ListItem.css +8 -0
  25. package/build/styles/main.css +194 -0
  26. package/build/types/Body/Body.d.ts +2 -2
  27. package/build/types/CardContainer/CardContainer.d.ts +8 -0
  28. package/build/types/CardContainer/CardContainer.d.ts.map +1 -0
  29. package/build/types/CardContainer/CardContainer.types.d.ts +55 -0
  30. package/build/types/CardContainer/CardContainer.types.d.ts.map +1 -0
  31. package/build/types/CardContainer/_stories/helpers.d.ts +3 -0
  32. package/build/types/CardContainer/_stories/helpers.d.ts.map +1 -0
  33. package/build/types/CardContainer/index.d.ts +3 -0
  34. package/build/types/CardContainer/index.d.ts.map +1 -0
  35. package/build/types/CardWidget/CardWidget.d.ts +10 -0
  36. package/build/types/CardWidget/CardWidget.d.ts.map +1 -0
  37. package/build/types/CardWidget/CardWidget.types.d.ts +55 -0
  38. package/build/types/CardWidget/CardWidget.types.d.ts.map +1 -0
  39. package/build/types/CardWidget/CardWidgetAction.d.ts +54 -0
  40. package/build/types/CardWidget/CardWidgetAction.d.ts.map +1 -0
  41. package/build/types/CardWidget/index.d.ts +4 -0
  42. package/build/types/CardWidget/index.d.ts.map +1 -0
  43. package/build/types/IconButton/IconButton.d.ts +1 -1
  44. package/build/types/ListItem/ListItem.d.ts +1 -1
  45. package/build/types/ListItem/ListItem.d.ts.map +1 -1
  46. package/build/types/MoneyInput/MoneyInput.d.ts +1 -1
  47. package/build/types/MoneyInput/MoneyInput.d.ts.map +1 -1
  48. package/build/types/Title/Title.d.ts +2 -2
  49. package/build/types/Upload/Steps/UploadImageStep/UploadImageStep.d.ts +1 -1
  50. package/build/types/UploadInput/UploadItem/UploadItemLink.d.ts +1 -1
  51. package/build/types/index.d.ts +4 -0
  52. package/build/types/index.d.ts.map +1 -1
  53. package/package.json +3 -3
  54. package/src/CardContainer/CardContainer.css +108 -0
  55. package/src/CardContainer/CardContainer.less +171 -0
  56. package/src/CardContainer/CardContainer.test.tsx +150 -0
  57. package/src/CardContainer/CardContainer.tsx +51 -0
  58. package/src/CardContainer/CardContainer.types.ts +62 -0
  59. package/src/CardContainer/_stories/CardContainer.story.tsx +167 -0
  60. package/src/CardContainer/_stories/CardContainer.test.story.tsx +103 -0
  61. package/src/CardContainer/_stories/helpers.tsx +50 -0
  62. package/src/CardContainer/index.ts +6 -0
  63. package/src/CardWidget/CardWidget.css +78 -0
  64. package/src/CardWidget/CardWidget.less +107 -0
  65. package/src/CardWidget/CardWidget.test.tsx +221 -0
  66. package/src/CardWidget/CardWidget.tsx +78 -0
  67. package/src/CardWidget/CardWidget.types.ts +63 -0
  68. package/src/CardWidget/CardWidgetAction.tsx +128 -0
  69. package/src/CardWidget/_stories/CardWidget.story.tsx +238 -0
  70. package/src/CardWidget/_stories/CardWidget.test.story.tsx +350 -0
  71. package/src/CardWidget/index.ts +3 -0
  72. package/src/ListItem/ListItem.css +8 -0
  73. package/src/ListItem/ListItem.less +8 -0
  74. package/src/ListItem/ListItem.test.tsx +20 -0
  75. package/src/ListItem/ListItem.tsx +3 -1
  76. package/src/ListItem/_stories/ListItem.layout.test.story.tsx +1 -1
  77. package/src/ListItem/_stories/ListItem.story.tsx +1 -1
  78. package/src/Prompt/CriticalBanner/_stories/CriticalBanner.docs.mdx +122 -0
  79. package/src/Prompt/CriticalBanner/_stories/CriticalBanner.story.tsx +0 -2
  80. package/src/index.ts +13 -0
  81. package/src/main.css +194 -0
  82. package/src/main.less +2 -0
  83. package/src/FlowNavigation/_stories/FlowNavigation.CriticalBanner.docs.mdx +0 -66
@@ -0,0 +1,167 @@
1
+ import { Meta, StoryObj } from '@storybook/react-webpack5';
2
+
3
+ import {
4
+ createSandboxStory,
5
+ globalScope,
6
+ } from '../../../.storybook/components/sandbox/SandboxEditor';
7
+ import Body from '../../Body';
8
+ import Link from '../../Link';
9
+ import CardContainer from '..';
10
+ import type { CardContainerPadding, CardContainerSpotlight } from '..';
11
+ import { CardsList, SampleContent } from './helpers';
12
+
13
+ /**
14
+ * A bounded surface that groups related content. It has no slots of its own: any content passed
15
+ * as children is stacked with no gap, inside the card's padding.
16
+ *
17
+ * For a card with a title, a title link or actions, use `CardWidget`.
18
+ */
19
+ const meta: Meta<typeof CardContainer> = {
20
+ component: CardContainer,
21
+ title: 'Layouts/CardContainer',
22
+ tags: ['early-access'],
23
+ argTypes: {
24
+ spotlight: {
25
+ control: 'radio',
26
+ options: [undefined, 'active', 'inactive'],
27
+ table: {
28
+ defaultValue: { summary: 'undefined' },
29
+ type: { summary: 'CardContainerSpotlight' },
30
+ },
31
+ },
32
+ padding: {
33
+ control: 'radio',
34
+ options: ['md', 'none'],
35
+ table: { defaultValue: { summary: "'md'" }, type: { summary: 'CardContainerPadding' } },
36
+ },
37
+ as: {
38
+ control: 'radio',
39
+ options: ['div', 'section', 'article'],
40
+ table: { defaultValue: { summary: "'div'" }, type: { summary: 'string' } },
41
+ },
42
+ interactive: {
43
+ control: 'boolean',
44
+ table: { defaultValue: { summary: 'false' }, type: { summary: 'boolean' } },
45
+ },
46
+ children: {
47
+ control: false,
48
+ table: { type: { summary: 'ReactNode' } },
49
+ },
50
+ className: { control: 'text', table: { type: { summary: 'string' } } },
51
+ 'data-testid': { control: 'text', table: { type: { summary: 'string' } } },
52
+ },
53
+ args: {
54
+ spotlight: 'active',
55
+ padding: 'md',
56
+ as: 'div',
57
+ },
58
+ };
59
+
60
+ export default meta;
61
+
62
+ type Story = StoryObj<typeof CardContainer>;
63
+
64
+ /** Experiment with the card's props. */
65
+ export const Playground: Story = {
66
+ render: (args) => (
67
+ <CardContainer {...args}>
68
+ <SampleContent />
69
+ </CardContainer>
70
+ ),
71
+ };
72
+
73
+ export const Sandbox = createSandboxStory({
74
+ code: `<CardContainer spotlight="active">
75
+ <Body>Your virtual card is ready to use.</Body>
76
+ <Body>Spend in 40+ currencies with the real exchange rate.</Body>
77
+ </CardContainer>`,
78
+ scope: { ...globalScope, CardContainer },
79
+ });
80
+
81
+ /**
82
+ * `spotlight` matches `ListItem`'s: `active` is raised, for live content, and `inactive` is
83
+ * recessed with a dashed stroke, for a feature not set up yet. Without a spotlight the card is
84
+ * flat.
85
+ */
86
+ export const Spotlight: Story = {
87
+ parameters: { controls: { disable: true } },
88
+ render: () => (
89
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '24px', maxWidth: '480px' }}>
90
+ {([undefined, 'active', 'inactive'] as (CardContainerSpotlight | undefined)[]).map(
91
+ (spotlight) => (
92
+ <CardContainer key={spotlight ?? 'none'} spotlight={spotlight}>
93
+ <Body>Spotlight: {spotlight ?? 'not set'}</Body>
94
+ </CardContainer>
95
+ ),
96
+ )}
97
+ </div>
98
+ ),
99
+ };
100
+
101
+ /**
102
+ * `padding="md"` insets the content by 8px, and by 16px from the `md` breakpoint up.
103
+ * `padding="none"` lets content such as images or charts run to the card's edges, clipped to its
104
+ * rounded corners. The grey boxes fill the content area, so their edges show where the padding
105
+ * ends.
106
+ *
107
+ * Focus rings are clipped at the edges too, so keep `none` for non-interactive content, and lists
108
+ * on `md`.
109
+ */
110
+ export const Padding: Story = {
111
+ parameters: { controls: { disable: true } },
112
+ render: () => (
113
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '24px', maxWidth: '480px' }}>
114
+ {(['md', 'none'] as CardContainerPadding[]).map((padding) =>
115
+ ([undefined, 'active', 'inactive'] as (CardContainerSpotlight | undefined)[]).map(
116
+ (spotlight) => (
117
+ <CardContainer
118
+ key={`${padding}-${spotlight ?? 'none'}`}
119
+ padding={padding}
120
+ spotlight={spotlight}
121
+ >
122
+ <div
123
+ style={{ height: '120px', background: 'var(--color-background-screen-active)' }}
124
+ />
125
+ </CardContainer>
126
+ ),
127
+ ),
128
+ )}
129
+ </div>
130
+ ),
131
+ };
132
+
133
+ /**
134
+ * `interactive` shows the card as hovered or pressed while a direct child link or button is, and
135
+ * as hovered while that child has keyboard focus. The card's padding, and a link nested deeper
136
+ * such as inside text, don't trigger the states.
137
+ */
138
+ export const Interactive: Story = {
139
+ parameters: { controls: { disable: true } },
140
+ render: () => (
141
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '24px', maxWidth: '480px' }}>
142
+ {([undefined, 'active', 'inactive'] as (CardContainerSpotlight | undefined)[]).map(
143
+ (spotlight) => (
144
+ <CardContainer key={spotlight ?? 'none'} spotlight={spotlight} interactive>
145
+ <Link href="#cards">Direct child link, spotlight: {spotlight ?? 'not set'}</Link>
146
+ </CardContainer>
147
+ ),
148
+ )}
149
+ <CardContainer spotlight="active" interactive>
150
+ <Body>
151
+ A <Link href="#details">nested link</Link> isn&apos;t a direct child, so this card has no
152
+ states.
153
+ </Body>
154
+ </CardContainer>
155
+ </div>
156
+ ),
157
+ };
158
+
159
+ /** A `List` placed directly in the card. */
160
+ export const WithList: Story = {
161
+ parameters: { controls: { disable: true } },
162
+ render: () => (
163
+ <CardContainer>
164
+ <CardsList />
165
+ </CardContainer>
166
+ ),
167
+ };
@@ -0,0 +1,103 @@
1
+ import { Meta, StoryObj } from '@storybook/react-webpack5';
2
+ import { useTheme } from '@wise/components-theming';
3
+
4
+ import { useRtlDiffI18n, withRTLDiff } from '../../../.storybook/decorators/RtlDiffDecorator';
5
+ import { withVariantConfig } from '../../../.storybook/helpers';
6
+ import Body from '../../Body';
7
+ import Title from '../../Title';
8
+ import CardContainer from '..';
9
+ import type { CardContainerProps, CardContainerSpotlight } from '..';
10
+
11
+ const meta: Meta<typeof CardContainer> = {
12
+ component: CardContainer,
13
+ tags: ['!autodocs', '!manifest'],
14
+ title: 'Layouts/CardContainer/Tests',
15
+ };
16
+ export default meta;
17
+ type Story = StoryObj<CardContainerProps>;
18
+
19
+ const SPOTLIGHTS: (CardContainerSpotlight | undefined)[] = [undefined, 'active', 'inactive'];
20
+
21
+ const ThemeTitle = () => {
22
+ const { theme, screenMode } = useTheme();
23
+ return (
24
+ <Title type="title-subsection">
25
+ {theme} / {screenMode}
26
+ </Title>
27
+ );
28
+ };
29
+
30
+ /** Fills the content area, so a padding-none card's corners show the clipping. */
31
+ const FullBleedContent = () => (
32
+ <div style={{ height: '80px', background: 'var(--color-background-screen-active)' }} />
33
+ );
34
+
35
+ /**
36
+ * Every spotlight against every theme, with and without padding. Without padding, the content
37
+ * should be clipped to the rounded corners, and the `inactive` dashed stroke should stay whole.
38
+ *
39
+ * The `active` spotlight's resting shadow is only applied in the light `personal`, `business` and
40
+ * `platform` themes; every `dark`, `bright-green` and `forest-green` column should show a flat
41
+ * surface.
42
+ */
43
+ export const AllVariants: Story = {
44
+ ...withVariantConfig([
45
+ 'default',
46
+ 'dark',
47
+ 'bright-green',
48
+ 'forest-green',
49
+ 'business',
50
+ 'business--bright-green',
51
+ 'business--forest-green',
52
+ 'platform',
53
+ 'platform--forest-green',
54
+ ]),
55
+ render: () => (
56
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '24px', maxWidth: '420px' }}>
57
+ <ThemeTitle />
58
+ {SPOTLIGHTS.map((spotlight) => (
59
+ <CardContainer key={spotlight ?? 'none'} spotlight={spotlight}>
60
+ <Body>Spotlight: {spotlight ?? 'not set'}</Body>
61
+ <Body>Second line, to show the content stacks flush.</Body>
62
+ </CardContainer>
63
+ ))}
64
+ {SPOTLIGHTS.map((spotlight) => (
65
+ <CardContainer key={`none-${spotlight ?? 'none'}`} spotlight={spotlight} padding="none">
66
+ <FullBleedContent />
67
+ </CardContainer>
68
+ ))}
69
+ </div>
70
+ ),
71
+ };
72
+
73
+ /**
74
+ * The inactive spotlight's dashed stroke and the padding should mirror in RTL. Without padding,
75
+ * the text should start at the inline-start edge.
76
+ */
77
+ export const RTLDiff: Story = {
78
+ render: () => {
79
+ const i18n = useRtlDiffI18n();
80
+ return (
81
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '24px', width: 420 }}>
82
+ {SPOTLIGHTS.map((spotlight) => (
83
+ <CardContainer key={spotlight ?? 'none'} spotlight={spotlight}>
84
+ <Body>{i18n.content}</Body>
85
+ </CardContainer>
86
+ ))}
87
+ {SPOTLIGHTS.map((spotlight) => (
88
+ <CardContainer key={`none-${spotlight ?? 'none'}`} spotlight={spotlight} padding="none">
89
+ <Body>{i18n.content}</Body>
90
+ </CardContainer>
91
+ ))}
92
+ </div>
93
+ );
94
+ },
95
+ ...withRTLDiff({
96
+ i18n: {
97
+ content: {
98
+ en: 'Your virtual card is ready to use.',
99
+ ar: 'بطاقتك الافتراضية جاهزة للاستخدام.',
100
+ },
101
+ },
102
+ }),
103
+ };
@@ -0,0 +1,50 @@
1
+ import { Card, Jar, Travel } from '@transferwise/icons';
2
+
3
+ import Body from '../../Body';
4
+ import List from '../../List';
5
+ import ListItem from '../../ListItem';
6
+
7
+ export const SampleContent = () => (
8
+ <>
9
+ <Body>Your virtual card is ready to use.</Body>
10
+ <Body>Spend in 40+ currencies with the real exchange rate.</Body>
11
+ </>
12
+ );
13
+
14
+ export const CardsList = () => (
15
+ <List>
16
+ <ListItem
17
+ title="•••• 1582"
18
+ subtitle="Current account"
19
+ valueTitle="£0 spent"
20
+ media={
21
+ <ListItem.AvatarView>
22
+ <Card />
23
+ </ListItem.AvatarView>
24
+ }
25
+ control={<ListItem.Navigation href="#card-1582" />}
26
+ />
27
+ <ListItem
28
+ title="•••• 9982"
29
+ subtitle="Jar"
30
+ valueTitle="£410 / £500 day"
31
+ media={
32
+ <ListItem.AvatarView>
33
+ <Jar />
34
+ </ListItem.AvatarView>
35
+ }
36
+ control={<ListItem.Navigation href="#card-9982" />}
37
+ />
38
+ <ListItem
39
+ title="•••• 5507"
40
+ subtitle="Travel"
41
+ valueTitle="£200 / £500 day"
42
+ media={
43
+ <ListItem.AvatarView>
44
+ <Travel />
45
+ </ListItem.AvatarView>
46
+ }
47
+ control={<ListItem.Navigation href="#card-5507" />}
48
+ />
49
+ </List>
50
+ );
@@ -0,0 +1,6 @@
1
+ export { default } from './CardContainer';
2
+ export type {
3
+ CardContainerProps,
4
+ CardContainerSpotlight,
5
+ CardContainerPadding,
6
+ } from './CardContainer.types';
@@ -0,0 +1,78 @@
1
+ .wds-card-widget {
2
+ --CardWidget-inset: var(--size-8);
3
+ --CardWidget-header-spacing: var(--size-8);
4
+ --CardWidget-actions-gap: var(--size-8);
5
+ }
6
+ .wds-card-widget__header {
7
+ align-items: center;
8
+ -moz-column-gap: var(--size-24);
9
+ column-gap: var(--size-24);
10
+ container-type: inline-size;
11
+ display: flex;
12
+ flex-wrap: wrap;
13
+ padding-inline-start: var(--CardWidget-inset);
14
+ row-gap: var(--size-8);
15
+ }
16
+ .wds-card-widget__header + .wds-card-widget__content {
17
+ margin-block-start: var(--CardWidget-header-spacing);
18
+ }
19
+ .wds-card-widget__title {
20
+ align-items: center;
21
+ color: var(--color-content-secondary);
22
+ display: grid;
23
+ flex: 1 1 0;
24
+ margin: 0;
25
+ min-block-size: var(--size-32);
26
+ }
27
+ @container (width <= 360px) {
28
+ .wds-card-widget__title {
29
+ flex-basis: auto;
30
+ }
31
+ }
32
+ .wds-card-widget__title-link.np-link {
33
+ color: inherit;
34
+ display: inline;
35
+ font: inherit;
36
+ justify-self: start;
37
+ letter-spacing: inherit;
38
+ text-align: start;
39
+ -webkit-text-decoration: none;
40
+ text-decoration: none;
41
+ }
42
+ .wds-card-widget__title-link.np-link:hover,
43
+ .wds-card-widget__title-link.np-link:focus-visible {
44
+ -webkit-text-decoration: underline;
45
+ text-decoration: underline;
46
+ }
47
+ .wds-card-widget__title-link.np-link .tw-icon {
48
+ display: inline-block;
49
+ vertical-align: text-bottom;
50
+ }
51
+ .wds-card-widget__title-link.np-link .wds-card-widget__title-chevron {
52
+ white-space: nowrap;
53
+ }
54
+ .wds-card-widget__title-link.np-link .wds-card-widget__title-chevron .tw-icon {
55
+ margin: 0;
56
+ }
57
+ .wds-card-widget__actions {
58
+ align-self: start;
59
+ align-items: center;
60
+ display: flex;
61
+ flex: none;
62
+ gap: var(--CardWidget-actions-gap);
63
+ }
64
+ .wds-card-widget--with-header .wds-card-widget__content {
65
+ padding-inline: var(--CardWidget-inset);
66
+ }
67
+ .wds-card-widget:has(.wds-card-widget__title-link:hover, .wds-card-widget__title-link:focus-visible).wds-card-container--active {
68
+ --CardContainer-border-color: color-mix(in srgb, var(--color-border-neutral) 75%, transparent);
69
+ }
70
+ .wds-card-widget:has(.wds-card-widget__title-link:hover, .wds-card-widget__title-link:focus-visible).wds-card-container--inactive {
71
+ --CardContainer-background-color: color-mix(in srgb, var(--color-background-neutral-hover) 25%, transparent);
72
+ }
73
+ .wds-card-widget:has(.wds-card-widget__title-link:active).wds-card-container--active {
74
+ --CardContainer-border-color: var(--color-border-neutral);
75
+ }
76
+ .wds-card-widget:has(.wds-card-widget__title-link:active).wds-card-container--inactive {
77
+ --CardContainer-background-color: color-mix(in srgb, var(--color-background-neutral-active) 25%, transparent);
78
+ }
@@ -0,0 +1,107 @@
1
+ @import (reference) "../CardContainer/CardContainer.less";
2
+
3
+ // Above this header width a long title wraps beside the action corner. At or below it, the title
4
+ // only shares a line with the actions while it fits on one line, so it keeps room to read.
5
+ @wds-card-widget-actions-stack-width: 360px;
6
+
7
+ .wds-card-widget {
8
+ --CardWidget-inset: var(--size-8);
9
+ --CardWidget-header-spacing: var(--size-8);
10
+ --CardWidget-actions-gap: var(--size-8);
11
+
12
+ // The header's title row is at least as tall as the 32px action buttons, so cards side by side
13
+ // line up their titles and content whether or not their headers have actions, or have stacked
14
+ // them. The actions wrap under the title when they don't fit beside it.
15
+ &__header {
16
+ align-items: center;
17
+ column-gap: var(--size-24);
18
+ container-type: inline-size;
19
+ display: flex;
20
+ flex-wrap: wrap;
21
+ padding-inline-start: var(--CardWidget-inset);
22
+ row-gap: var(--size-8);
23
+ }
24
+
25
+ // On the content rather than the header, as an inactive card's decorative border comes after the
26
+ // header, so a header-only widget's header isn't its last child.
27
+ &__header + &__content {
28
+ margin-top: var(--CardWidget-header-spacing);
29
+ }
30
+
31
+ &__title {
32
+ align-items: center;
33
+ color: var(--color-content-secondary);
34
+ display: grid;
35
+ flex: 1 1 0;
36
+ margin: 0;
37
+ min-height: var(--size-32);
38
+
39
+ @container (width <= @wds-card-widget-actions-stack-width) {
40
+ flex-basis: auto;
41
+ }
42
+ }
43
+
44
+ // The title is a `Link`, but keeps the title's typography and colour rather than the link
45
+ // text style, and flows inline so the chevron can follow its last word.
46
+ &__title-link.np-link {
47
+ color: inherit;
48
+ display: inline;
49
+ font: inherit;
50
+ // The title is a grid, which would stretch the link to the full width up to the actions, so
51
+ // its hover and click area would reach past the text.
52
+ justify-self: start;
53
+ letter-spacing: inherit;
54
+ // `Link` renders a button when there is no `href`, which would otherwise centre the title.
55
+ text-align: start;
56
+ text-decoration: none;
57
+
58
+ &:hover,
59
+ &:focus-visible {
60
+ text-decoration: underline;
61
+ }
62
+
63
+ // Inline, so the icons follow the title's last word: the chevron, and the new-tab icon `Link`
64
+ // adds for `target="_blank"`, which `Link` itself lays out as a flex box. Nested so it
65
+ // outweighs `Link`'s own `.np-link .tw-icon`, which loads later.
66
+ .tw-icon {
67
+ display: inline-block;
68
+ vertical-align: text-bottom;
69
+ }
70
+
71
+ .wds-card-widget__title-chevron {
72
+ white-space: nowrap;
73
+
74
+ .tw-icon {
75
+ margin: 0;
76
+ }
77
+ }
78
+ }
79
+
80
+ // Beside the title, the growing title pushes the actions to the inline end. When they wrap onto
81
+ // their own row under the title, they start at the inline start, under it.
82
+ &__actions {
83
+ // Stays in the top corner when a long title wraps onto more lines.
84
+ align-self: start;
85
+ align-items: center;
86
+ display: flex;
87
+ flex: none;
88
+ gap: var(--CardWidget-actions-gap);
89
+ }
90
+
91
+ // With a header, the content is inset further from both inline edges.
92
+ &--with-header &__content {
93
+ padding-inline: var(--CardWidget-inset);
94
+ }
95
+
96
+ // The card's hover and pressed states follow the title link alone, as it leads to the widget's
97
+ // own screen. Hovering or pressing the rest of the card, or its actions, leaves it at rest.
98
+ // Keyboard focus on the link shows the hover state too, so keyboard users see the card react.
99
+ &:has(.wds-card-widget__title-link:hover, .wds-card-widget__title-link:focus-visible) {
100
+ .wds-card-container-hover-state();
101
+ }
102
+
103
+ // After the hover rule, so pressed wins while the pointer is down.
104
+ &:has(.wds-card-widget__title-link:active) {
105
+ .wds-card-container-pressed-state();
106
+ }
107
+ }